diff --git a/.gitignore b/.gitignore index a1b8c921e..51b59868c 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,9 @@ xcode/Barony/Barony.xcodeproj/xcuserdata/* *.ps1 !LICENSE.nativefiledialog.txt /VS.2015/Barony/x64/Debug +*.sarif +*.i +*.enc +*.ilk +/VS.2015/x64 +*.sqlite-journal diff --git a/INSTALL.md b/INSTALL.md index dca7a88be..f63df4103 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -14,12 +14,13 @@ You will need the following libraries to build Barony: * CMake (on Windows, use versions at least as new as 3.8.0) OPTIONAL dependencies: - * One of FMOD Core API 2.00.08 or OpenAL for audio support. + * FMOD Core API 2.02.14. FMOD Studio API can be downloaded at https://www.fmod.com/download (you do need an account to download it). You can disable FMOD by running cmake with -DFMOD_ENABLED=OFF (it's also disabled if not found). OpenAL can be used with -DOPENAL_ENABLED + * Note - OpenAL support has been deprecated and is currently unmaintained. You will also need the following tools: diff --git a/VS.2015/Barony.sln b/VS.2015/Barony.sln index a6bebcf43..c25a9763c 100644 --- a/VS.2015/Barony.sln +++ b/VS.2015/Barony.sln @@ -21,6 +21,8 @@ Global EOS Debug|x64 = EOS Debug|x64 EOS|Win32 = EOS|Win32 EOS|x64 = EOS|x64 + EOSDebugInstrumentation|Win32 = EOSDebugInstrumentation|Win32 + EOSDebugInstrumentation|x64 = EOSDebugInstrumentation|x64 NODRM|Win32 = NODRM|Win32 NODRM|x64 = NODRM|x64 Release|Win32 = Release|Win32 @@ -55,6 +57,10 @@ Global {A4A170FF-C081-4F33-94FE-5C60E89C5990}.EOS|Win32.Build.0 = EOS|Win32 {A4A170FF-C081-4F33-94FE-5C60E89C5990}.EOS|x64.ActiveCfg = EOS|x64 {A4A170FF-C081-4F33-94FE-5C60E89C5990}.EOS|x64.Build.0 = EOS|x64 + {A4A170FF-C081-4F33-94FE-5C60E89C5990}.EOSDebugInstrumentation|Win32.ActiveCfg = EOSDebugInstrumentation|Win32 + {A4A170FF-C081-4F33-94FE-5C60E89C5990}.EOSDebugInstrumentation|Win32.Build.0 = EOSDebugInstrumentation|Win32 + {A4A170FF-C081-4F33-94FE-5C60E89C5990}.EOSDebugInstrumentation|x64.ActiveCfg = EOSDebugInstrumentation|x64 + {A4A170FF-C081-4F33-94FE-5C60E89C5990}.EOSDebugInstrumentation|x64.Build.0 = EOSDebugInstrumentation|x64 {A4A170FF-C081-4F33-94FE-5C60E89C5990}.NODRM|Win32.ActiveCfg = NODRM|Win32 {A4A170FF-C081-4F33-94FE-5C60E89C5990}.NODRM|Win32.Build.0 = NODRM|Win32 {A4A170FF-C081-4F33-94FE-5C60E89C5990}.NODRM|x64.ActiveCfg = NODRM|x64 @@ -92,6 +98,8 @@ Global {8D0CEB30-DFEC-462D-B107-1EA02D317151}.EOS|Win32.Build.0 = EOS|Win32 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.EOS|x64.ActiveCfg = EOS|x64 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.EOS|x64.Build.0 = EOS|x64 + {8D0CEB30-DFEC-462D-B107-1EA02D317151}.EOSDebugInstrumentation|Win32.ActiveCfg = EOSDebugInstrumentation|Win32 + {8D0CEB30-DFEC-462D-B107-1EA02D317151}.EOSDebugInstrumentation|x64.ActiveCfg = EOSDebugInstrumentation|x64 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.NODRM|Win32.ActiveCfg = NODRM|Win32 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.NODRM|Win32.Build.0 = NODRM|Win32 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.NODRM|x64.ActiveCfg = NODRM|x64 @@ -103,6 +111,7 @@ Global {8D0CEB30-DFEC-462D-B107-1EA02D317151}.Steam Crossplay|Win32.ActiveCfg = Steam Crossplay|Win32 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.Steam Crossplay|Win32.Build.0 = Steam Crossplay|Win32 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.Steam Crossplay|x64.ActiveCfg = Steam Crossplay|x64 + {8D0CEB30-DFEC-462D-B107-1EA02D317151}.Steam Crossplay|x64.Build.0 = Steam Crossplay|x64 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.Steam Debug|Win32.ActiveCfg = Steam Debug|Win32 {8D0CEB30-DFEC-462D-B107-1EA02D317151}.Steam Debug|x64.ActiveCfg = Steam Debug|x64 EndGlobalSection diff --git a/VS.2015/Barony/Barony.vcxproj b/VS.2015/Barony/Barony.vcxproj index fe0e0b2be..c608f4ed8 100644 --- a/VS.2015/Barony/Barony.vcxproj +++ b/VS.2015/Barony/Barony.vcxproj @@ -41,6 +41,14 @@ EOS Debug x64 + + EOSDebugInstrumentation + Win32 + + + EOSDebugInstrumentation + x64 + EOS Win32 @@ -180,6 +188,13 @@ true MultiByte + + Application + false + v143 + true + MultiByte + Application false @@ -195,6 +210,14 @@ MultiByte false + + Application + false + v143 + true + MultiByte + false + Application false @@ -272,12 +295,18 @@ + + + + + + @@ -385,6 +414,13 @@ true barony + + false + $(VC_IncludePath);$(WindowsSDK_IncludePath);C:\Program Files (x86)\FMOD SoundSystem\FMOD Programmers API Windows\api\inc;C:\programming\turning_wheel\GameLibs\SDL2-2.0.4\include;C:\programming\turning_wheel\GameLibs\SDL2_image-2.0.0\include;C:\programming\turning_wheel\GameLibs\SDL2_net-2.0.0\include;C:\programming\turning_wheel\GameLibs\libpng-1.2.37-lib\include;C:\programming\turning_wheel\GameLibs\zlib-1.2.3-lib\include;C:\programming\turning_wheel\GameLibs\steamworks_sdk_138a\sdk\public;C:\programming\turning_wheel\GameLibs\SDL2_ttf-2.0.14\include;C:\programming\turning_wheel\GameLibs\missing_windows_includes;P:\Ben\Documents\Visual Studio 2015\Projects\libphysfs;P:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\SDL2;P:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\physfs + $(VC_LibraryPath_x86);$(WindowsSDK_LibraryPath_x86);C:\Program Files (x86)\FMOD SoundSystem\FMOD Programmers API Windows\api\lib;C:\programming\turning_wheel\GameLibs\SDL2-2.0.4\lib\x86;C:\programming\turning_wheel\GameLibs\SDL2_image-2.0.0\lib\x86;C:\programming\turning_wheel\GameLibs\SDL2_net-2.0.0\lib\x86;C:\programming\turning_wheel\GameLibs\libpng-1.2.37-lib\lib;C:\programming\turning_wheel\GameLibs\zlib-1.2.3-lib\lib;C:\programming\turning_wheel\GameLibs\steamworks_sdk_138a\sdk\redistributable_bin;C:\programming\turning_wheel\GameLibs\SDL2_ttf-2.0.14\lib\x86;P:\Ben\Documents\Visual Studio 2015\Projects\libphysfs;$(SolutionDir)..\VS-libs + true + barony + false $(VC_IncludePath);$(WindowsSDK_IncludePath);C:\Program Files (x86)\FMOD SoundSystem\FMOD Programmers API Windows\api\inc;C:\programming\turning_wheel\GameLibs\SDL2-2.0.4\include;C:\programming\turning_wheel\GameLibs\SDL2_image-2.0.0\include;C:\programming\turning_wheel\GameLibs\SDL2_net-2.0.0\include;C:\programming\turning_wheel\GameLibs\libpng-1.2.37-lib\include;C:\programming\turning_wheel\GameLibs\zlib-1.2.3-lib\include;C:\programming\turning_wheel\GameLibs\steamworks_sdk_138a\sdk\public;C:\programming\turning_wheel\GameLibs\SDL2_ttf-2.0.14\include;C:\programming\turning_wheel\GameLibs\missing_windows_includes;P:\Ben\Documents\Visual Studio 2015\Projects\libphysfs;P:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\SDL2;P:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\physfs @@ -393,7 +429,14 @@ barony - false + true + $(VC_IncludePath);$(WindowsSDK_IncludePath) + $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(SolutionDir)..\VS-libs + false + barony + + + true $(VC_IncludePath);$(WindowsSDK_IncludePath) $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(SolutionDir)..\VS-libs false @@ -660,6 +703,9 @@ if %errorlevel% leq 1 exit 0 else exit %errorlevel% $(IntDir)%(RelativeDir) /DBUILD_PR="$(BARONY_BUILD_PR)" /DBUILD_CC="$(BARONY_BUILD_CC)" /DBUILD_CS="$(BARONY_BUILD_CS)" /DBUILD_DE="$(BARONY_BUILD_DE)" /DBUILD_SA="$(BARONY_BUILD_SA)" /DBUILD_GSE="$(BARONY_BUILD_GSTATS)" /DBUILD_PFTID="$(BARONY_BUILD_PFTID)" /DBUILD_PFHID="$(BARONY_BUILD_PFHID)" %(AdditionalOptions) stdcpp17 + + + Windows @@ -669,6 +715,8 @@ if %errorlevel% leq 1 exit 0 else exit %errorlevel% OpenGL32.lib;glu32.lib;SDL2.lib;SDL2main.lib;SDL2_net.lib;SDL2_image.lib;fmod_vc.lib;libpng16.lib;zlib.lib;steam_api64.lib;SDL2_ttf.lib;physfs.lib;EOSSDK-Win64-Shipping.lib;libtheoraplayer.lib;glew32.lib;nfd.lib;libcurl.lib;ws2_32.lib;Normaliz.lib;Crypt32.lib;Wldap32.lib;XPlatCppWindows.lib;lib_json.lib;opus.lib;%(AdditionalDependencies) false true + + copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)Release\barony_test.exe" @@ -839,6 +887,45 @@ if %errorlevel% leq 1 exit 0 else exit %errorlevel% copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS\barony_test.exe" copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 3\barony_test.exe" +copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 4\barony_test.exe" + + + true + + + + + Level3 + + + Disabled + false + true + _CRT_SECURE_NO_WARNINGS;WIN32;_WINDOWS;%(PreprocessorDefinitions);USE_EOS;USE_IMGUI + "$(SolutionDir)\..\VS-includes" + true + + + false + ProgramDatabase + MultiThreadedDebugDLL + false + /DBUILD_PR="$(BARONY_BUILD_PR)" /DBUILD_CC="$(BARONY_BUILD_CC)" /DBUILD_CS="$(BARONY_BUILD_CS)" /DBUILD_DE="$(BARONY_BUILD_DE)" /DBUILD_SA="$(BARONY_BUILD_SA)" /DBUILD_GSE="$(BARONY_BUILD_GSTATE)" %(AdditionalOptions) + $(IntDir)%(RelativeDir) + false + + + Windows + true + true + true + OpenGL32.lib;glu32.lib;SDL2.lib;SDL2main.lib;SDL2_net.lib;SDL2_image.lib;fmod_vc.lib;libpng.lib;zlib.lib;SDL2_ttf.lib;physfs.lib;EOSSDK-Win32-Shipping.lib;steam_api.lib;libtheoraplayer.lib;glew32.lib;%(AdditionalDependencies) + false + true + + + copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS\barony_test.exe" +copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 3\barony_test.exe" copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 4\barony_test.exe" @@ -890,7 +977,7 @@ copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 4\barony_test.exe" Disabled - false + true true _CRT_SECURE_NO_WARNINGS;WIN32;_WINDOWS;%(PreprocessorDefinitions);USE_EOS;USE_IMGUI;USE_THEORA_VIDEO;CURL_STATICLIB;USE_LIBCURL;USE_PLAYFAB;USE_OPUS $(SolutionDir)\..\VS-includes @@ -898,7 +985,7 @@ copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 4\barony_test.exe" false - ProgramDatabase + EditAndContinue MultiThreadedDebugDLL false $(IntDir)%(RelativeDir) @@ -910,11 +997,56 @@ copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 4\barony_test.exe" Windows true - true - true + false + false + OpenGL32.lib;glu32.lib;SDL2.lib;SDL2main.lib;SDL2_net.lib;SDL2_image.lib;fmod_vc.lib;libpng16.lib;zlib.lib;SDL2_ttf.lib;physfs.lib;EOSSDK-Win64-Shipping.lib;steam_api64.lib;libtheoraplayer.lib;glew32.lib;nfd.lib;libcurl.lib;ws2_32.lib;Normaliz.lib;Crypt32.lib;Wldap32.lib;XPlatCppWindows_debug.lib;lib_json_debug.lib;opus.lib;%(AdditionalDependencies) + false + false + + + + + copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS\barony_test.exe" +copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 3\barony_test.exe" +copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS 4\barony_test.exe" + + + true + + + + + Level3 + + + Disabled + true + true + _CRT_SECURE_NO_WARNINGS;WIN32;_WINDOWS;%(PreprocessorDefinitions);USE_EOS;USE_IMGUI;USE_THEORA_VIDEO;CURL_STATICLIB;USE_LIBCURL;USE_PLAYFAB;USE_OPUS + $(SolutionDir)\..\VS-includes + true + + + false + EditAndContinue + MultiThreadedDebugDLL + false + $(IntDir)%(RelativeDir) + /DBUILD_PR="$(BARONY_BUILD_PR)" /DBUILD_CC="$(BARONY_BUILD_CC)" /DBUILD_CS="$(BARONY_BUILD_CS)" /DBUILD_DE="$(BARONY_BUILD_DE)" /DBUILD_SA="$(BARONY_BUILD_SA)" /DBUILD_GSE="$(BARONY_BUILD_GSTATE)" /DBUILD_PFTID="$(BARONY_BUILD_PFTID)" /DBUILD_PFHID="$(BARONY_BUILD_PFHID)" %(AdditionalOptions) + stdcpp17 + false + true + + + Windows + true + false + false OpenGL32.lib;glu32.lib;SDL2.lib;SDL2main.lib;SDL2_net.lib;SDL2_image.lib;fmod_vc.lib;libpng16.lib;zlib.lib;SDL2_ttf.lib;physfs.lib;EOSSDK-Win64-Shipping.lib;steam_api64.lib;libtheoraplayer.lib;glew32.lib;nfd.lib;libcurl.lib;ws2_32.lib;Normaliz.lib;Crypt32.lib;Wldap32.lib;XPlatCppWindows_debug.lib;lib_json_debug.lib;opus.lib;%(AdditionalDependencies) false true + + copy "$(TargetDir)$(TargetFileName)" "$(SolutionDir)EOS\barony_test.exe" @@ -1190,8 +1322,11 @@ if %errorlevel% leq 1 exit 0 else exit %errorlevel% + + + @@ -1204,9 +1339,12 @@ if %errorlevel% leq 1 exit 0 else exit %errorlevel% + + + @@ -1217,6 +1355,7 @@ if %errorlevel% leq 1 exit 0 else exit %errorlevel% + @@ -1256,6 +1395,7 @@ if %errorlevel% leq 1 exit 0 else exit %errorlevel% + diff --git a/VS.2015/Barony/Barony.vcxproj.filters b/VS.2015/Barony/Barony.vcxproj.filters index 494cd70a2..8a534d382 100644 --- a/VS.2015/Barony/Barony.vcxproj.filters +++ b/VS.2015/Barony/Barony.vcxproj.filters @@ -501,6 +501,27 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + @@ -683,6 +704,9 @@ Header Files + + Header Files + diff --git a/VS.2015/editor/editor.vcxproj b/VS.2015/editor/editor.vcxproj index c458b24e2..8a683d11a 100644 --- a/VS.2015/editor/editor.vcxproj +++ b/VS.2015/editor/editor.vcxproj @@ -41,6 +41,14 @@ EOS Debug x64 + + EOSDebugInstrumentation + Win32 + + + EOSDebugInstrumentation + x64 + EOS Win32 @@ -177,6 +185,13 @@ true MultiByte + + Application + false + v143 + true + MultiByte + Application false @@ -191,6 +206,13 @@ true MultiByte + + Application + false + v143 + true + MultiByte + Application false @@ -268,12 +290,18 @@ + + + + + + @@ -357,6 +385,13 @@ P:\Program Files (x86)\Microsoft Visual Studio 14.0\Team Tools\Static Analysis Tools\Rule Sets\NativeRecommendedRules.ruleset false + + $(VC_IncludePath);$(WindowsSDK_IncludePath);C:\Program Files (x86)\FMOD SoundSystem\FMOD Programmers API Windows\api\inc;C:\programming\turning_wheel\GameLibs\SDL2-2.0.4\include;C:\programming\turning_wheel\GameLibs\SDL2_image-2.0.0\include;C:\programming\turning_wheel\GameLibs\SDL2_net-2.0.0\include;C:\programming\turning_wheel\GameLibs\libpng-1.2.37-lib\include;C:\programming\turning_wheel\GameLibs\zlib-1.2.3-lib\include;C:\programming\turning_wheel\GameLibs\steamworks_sdk_138a\sdk\public;C:\programming\turning_wheel\GameLibs\SDL2_ttf-2.0.14\include;C:\programming\turning_wheel\GameLibs\missing_windows_includes;P:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\SDL2;P:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\physfs + $(VC_LibraryPath_x86);$(WindowsSDK_LibraryPath_x86);C:\Program Files (x86)\FMOD SoundSystem\FMOD Programmers API Windows\api\lib;C:\programming\turning_wheel\GameLibs\SDL2-2.0.4\lib\x86;C:\programming\turning_wheel\GameLibs\SDL2_image-2.0.0\lib\x86;C:\programming\turning_wheel\GameLibs\SDL2_net-2.0.0\lib\x86;C:\programming\turning_wheel\GameLibs\libpng-1.2.37-lib\lib;C:\programming\turning_wheel\GameLibs\zlib-1.2.3-lib\lib;C:\programming\turning_wheel\GameLibs\steamworks_sdk_138a\sdk\redistributable_bin;C:\programming\turning_wheel\GameLibs\SDL2_ttf-2.0.14\lib\x86;P:\Ben\Documents\Visual Studio 2015\Projects\libphysfs;$(SolutionDir)..\VS-libs + $(ProjectName) + P:\Program Files (x86)\Microsoft Visual Studio 14.0\Team Tools\Static Analysis Tools\Rule Sets\NativeRecommendedRules.ruleset + false + $(VC_IncludePath);$(WindowsSDK_IncludePath);C:\Program Files (x86)\FMOD SoundSystem\FMOD Programmers API Windows\api\inc;C:\programming\turning_wheel\GameLibs\SDL2-2.0.4\include;C:\programming\turning_wheel\GameLibs\SDL2_image-2.0.0\include;C:\programming\turning_wheel\GameLibs\SDL2_net-2.0.0\include;C:\programming\turning_wheel\GameLibs\libpng-1.2.37-lib\include;C:\programming\turning_wheel\GameLibs\zlib-1.2.3-lib\include;C:\programming\turning_wheel\GameLibs\steamworks_sdk_138a\sdk\public;C:\programming\turning_wheel\GameLibs\SDL2_ttf-2.0.14\include;C:\programming\turning_wheel\GameLibs\missing_windows_includes;P:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\SDL2;P:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\physfs $(VC_LibraryPath_x86);$(WindowsSDK_LibraryPath_x86);C:\Program Files (x86)\FMOD SoundSystem\FMOD Programmers API Windows\api\lib;C:\programming\turning_wheel\GameLibs\SDL2-2.0.4\lib\x86;C:\programming\turning_wheel\GameLibs\SDL2_image-2.0.0\lib\x86;C:\programming\turning_wheel\GameLibs\SDL2_net-2.0.0\lib\x86;C:\programming\turning_wheel\GameLibs\libpng-1.2.37-lib\lib;C:\programming\turning_wheel\GameLibs\zlib-1.2.3-lib\lib;C:\programming\turning_wheel\GameLibs\steamworks_sdk_138a\sdk\redistributable_bin;C:\programming\turning_wheel\GameLibs\SDL2_ttf-2.0.14\lib\x86;P:\Ben\Documents\Visual Studio 2015\Projects\libphysfs;$(SolutionDir)..\VS-libs @@ -369,6 +404,11 @@ $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(SolutionDir)..\VS-libs $(ProjectName) + + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(SolutionDir)..\VS-libs + $(ProjectName) + $(VC_IncludePath);$(WindowsSDK_IncludePath); $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(SolutionDir)..\VS-libs @@ -681,6 +721,31 @@ Windows + + + Level3 + Disabled + true + true + true + _CRT_SECURE_NO_WARNINGS;EDITOR;BARONY_DRM_FREE;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + "$(SolutionDir)\..\VS-includes" + + + true + $(IntDir)%(RelativeDir) + false + false + + + true + true + true + OpenGL32.lib;glu32.lib;SDL2.lib;SDL2main.lib;SDL2_net.lib;SDL2_image.lib;fmod_vc.lib;libpng.lib;zlib.lib;steam_api.lib;SDL2_ttf.lib;physfs.lib;glew32.lib;%(AdditionalDependencies) + Windows + + Level3 @@ -720,13 +785,46 @@ true stdcpp17 + EditAndContinue + false true - true - true + false + false + OpenGL32.lib;glu32.lib;SDL2.lib;SDL2main.lib;SDL2_net.lib;SDL2_image.lib;fmod_vc.lib;libpng16.lib;zlib.lib;SDL2_ttf.lib;physfs.lib;glew32.lib;%(AdditionalDependencies) + Windows + Default + + + + + + + + + Level3 + Disabled + true + true + true + _CRT_SECURE_NO_WARNINGS;_SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING;BARONY_DRM_FREE;EDITOR;%(PreprocessorDefinitions) + MultiThreaded + "$(SolutionDir)\..\VS-includes" + + + true + stdcpp17 + EditAndContinue + false + + + true + false + false OpenGL32.lib;glu32.lib;SDL2.lib;SDL2main.lib;SDL2_net.lib;SDL2_image.lib;fmod_vc.lib;libpng16.lib;zlib.lib;SDL2_ttf.lib;physfs.lib;glew32.lib;%(AdditionalDependencies) Windows + Default diff --git a/lang/compendium_lang/contents_codex.json b/lang/compendium_lang/contents_codex.json index 9fd03269d..eb0da7c5e 100644 --- a/lang/compendium_lang/contents_codex.json +++ b/lang/compendium_lang/contents_codex.json @@ -9,6 +9,7 @@ {"LEVELING UP": "leveling up"}, {"STATS & METASTATS": "stats metastats"}, {"SKILLS": "skills"}, + {"GOLD": "gold"}, {" ACTIONS": "-"}, {"MELEE ATTACKS": "melee"}, {"CRITICAL STRIKES": "crits"}, @@ -17,6 +18,7 @@ {"SNEAKING": "sneaking"}, {"LEGENDARY STRIKES": "legendary strikes"}, {"BLOCKING": "blocking"}, + {"OFF-HAND CASTING": "offhand casting"}, {"STRAFE & BACKPEDAL": "strafing"}, {"MEMORIZED CASTING": "memorized"}, {"SPELLBOOK CASTING": "spellbook casting"}, @@ -47,10 +49,10 @@ {"STEALTH SKILL": "stealth skill"}, {"BLOCKING SKILL": "blocking skill"}, {"RANGED SKILL": "ranged skill"}, - {"MAGIC SKILL": "magic skill"}, - {"CASTING SKILL": "casting skill"}, - {"APPRAISAL SKILL": "appraisal skill"}, - {"SWIMMING SKILL": "swimming skill"}, + {"SORCERY SKILL": "sorcery skill"}, + {"MYSTICISM SKILL": "mysticism skill"}, + {"THAUMATURGY SKILL": "thaumaturgy skill"}, + {"LORE SKILL": "lore skill"}, {"LEADERSHIP SKILL": "leadership skill"}, {"TRADING SKILL": "trading skill"}, {"ALCHEMY SKILL": "alchemy skill"}, @@ -58,14 +60,12 @@ ], "contents_alphabetical": [ {"ALCHEMY SKILL": "alchemy skill"}, - {"APPRAISAL SKILL": "appraisal skill"}, {"ARMOR CLASS (AC)": "ac"}, {"AXE SKILL": "axe skill"}, {"BACKSTABBING": "backstabs"}, {"BLOCKING SKILL": "blocking skill"}, {"BLOCKING": "blocking"}, {"CASTING MEMORIZED SPELLS": "memorized"}, - {"CASTING SKILL": "casting skill"}, {"CHARISMA (CHR)": "chr"}, {"CLASS": "class"}, {"CLASSES LIST": "classes list"}, @@ -74,19 +74,22 @@ {"DEXTERITY (DEX)": "dex"}, {"EXPERIENCE POINTS (XP)": "xp"}, {"FLANKING STRIKES": "flanking"}, + {"GOLD": "gold"}, {"HEALTH POINTS (HP)": "hp"}, {"INTELLIGENCE (INT)": "int"}, {"ITEM DURABILITY": "equipment degradation"}, {"LEADERSHIP SKILL": "leadership skill"}, {"LEGENDARY STRIKES": "legendary strikes"}, {"LEVELING UP": "leveling up"}, + {"LORE SKILL": "lore skill"}, {"MACE SKILL": "mace skill"}, {"MAGIC POINTS (MP)": "mp"}, {"MAGIC POWER (PWR)": "pwr"}, {"MAGIC RESIST (RES)": "res"}, - {"MAGIC SKILL": "magic skill"}, {"MELEE ATTACKS": "melee"}, {"MISSILE ATTACKS": "missiles"}, + {"MYSTICISM SKILL": "mysticism skill"}, + {"OFF-HAND CASTING": "offhand casting"}, {"PERCEPTION (PER)": "per"}, {"POLEARM SKILL": "polearm skill"}, {"RACE & ALIGNMENT": "races"}, @@ -94,13 +97,14 @@ {"REGENERATION (RGN)": "rgn"}, {"SKILLS": "skills"}, {"SNEAKING": "sneaking"}, + {"SORCERY SKILL": "sorcery skill"}, {"SPELLBOOK CASTING": "spellbook casting"}, {"STATS & METASTATS": "stats metastats"}, {"STEALTH SKILL": "stealth skill"}, {"STRAFE & BACKPEDAL": "strafing"}, {"STRENGTH (STR)": "str"}, - {"SWIMMING SKILL": "swimming skill"}, {"SWORD SKILL": "sword skill"}, + {"THAUMATURGY SKILL": "thaumaturgy skill"}, {"THROWN ATTACKS": "thrown"}, {"TINKERING SKILL": "tinkering skill"}, {"TRADING SKILL": "trading skill"}, diff --git a/lang/compendium_lang/contents_items.json b/lang/compendium_lang/contents_items.json index 0960805bc..df0f7eadb 100644 --- a/lang/compendium_lang/contents_items.json +++ b/lang/compendium_lang/contents_items.json @@ -4,6 +4,8 @@ {" ARMOR": "-"}, {"SHIELDS": "shields"}, {"LEATHER ARMOR": "leather armor"}, + {"QUILTED ARMOR": "quilted armor"}, + {"CHAIN ARMOR": "chain armor"}, {"IRON ARMOR": "iron armor"}, {"STEEL ARMOR": "steel armor"}, {"CRYSTAL ARMOR": "crystal armor"}, @@ -17,12 +19,16 @@ {"CROWNS & HEADDRESSES": "crowns & headdresses"}, {"FACE ACCESSORIES": "face accessories"}, {"MASKS & VISORS": "masks & visors"}, - {"CLOAKS & SHIRTS": "cloaks & clothing"}, + {"CLOAKS & CLOTHING": "cloaks & clothing"}, {"BACKPACKS": "backpacks"}, {" MELEE WEAPONS": "-"}, {"SWORDS": "swords"}, + {"GREATSWORDS": "greatswords"}, + {"RAPIER": "rapier"}, {"AXES": "axes"}, {"MACES": "maces"}, + {"FLAILS": "flails"}, + {"SHILLELAGH": "shillelagh"}, {"POLEARMS": "polearms"}, {"DYRNWYN": "dyrnwyn"}, {"PARASHU": "parashu"}, @@ -44,15 +50,18 @@ {"TOMAHAWKS & DAGGERS": "tomahawks & daggers"}, {"CHAKRAMS & SHURIKENS": "chakrams and shurikens"}, {"BOOMERANG": "boomerang"}, + {"BOLAS": "bolas"}, + {"SLUDGE BALLS": "sludge balls"}, {" CRYSTALS": "-"}, {"ROCKS & GEMS": "rocks & gems"}, {"CRYSTAL SHARD": "crystal shard"}, - {"MYSTIC ORB": "mystic orb"}, + {"MYSTIC ORB": "mystic orb"}, {" FOOD": "-"}, {"MORSELS": "cheese and apples"}, {"CREAM PIE": "cream pie"}, {"TOMALLEY": "tomalley"}, {"MEALS": "meat, fish, and bread"}, + {"RATIONS": "rations"}, {"TIN & TIN OPENER": "tin and tin opener"}, {"BLOOD VIALS": "blood vials"}, {" POTIONS & DRINKS": "-"}, @@ -67,7 +76,9 @@ {"TOWEL": "towel"}, {"MINING PICK": "mining pick"}, {"TINKERING KIT": "tinkering kit"}, + {"FRYPAN": "frypan"}, {"LOCKPICK": "lockpick"}, + {"KEYS": "keys"}, {"SKELETON KEY": "skeleton key"}, {" GADGETS": "-"}, {"BEARTRAP": "beartrap"}, @@ -79,39 +90,48 @@ {"TELEPORTATION TRAP": "teleportation trap"}, {" OTHER": "-"}, {"MAIL & LORE BOOKS": "mail & lore books"}, - {"DEATH BOX": "death box"} + {"DEATH BOX": "death box"}, + {"DUCK": "duck"} ], "contents_alphabetical": [ {"ALEMBIC": "alembic"}, {"ARBALEST": "arbalest"}, {"AXES": "axes"}, {"BACKPACKS": "backpacks"}, - {"BEARTRAP": "beartrap"}, - {"BLOOD VIALS": "blood vials"}, + {"BEARTRAP": "beartrap"}, + {"BLOOD VIALS": "blood vials"}, + {"BOLAS": "bolas"}, {"BOOMERANG": "boomerang"}, {"BOWS": "bows"}, + {"CHAIN ARMOR": "chain armor"}, {"CHAKRAMS & SHURIKENS": "chakrams and shurikens"}, - {"CLOAKS & SHIRTS": "cloaks & clothing"}, + {"CLOAKS & CLOTHING": "cloaks & clothing"}, {"CREAM PIE": "cream pie"}, {"CROSSBOW": "crossbow"}, {"CROWNS & HEADDRESSES": "crowns & headdresses"}, {"CRYSTAL AMMO": "crystal ammo"}, {"CRYSTAL ARMOR": "crystal armor"}, {"CRYSTAL SHARD": "crystal shard"}, + {"DARTS & PLUMBATA": "darts and plumbata"}, {"DEATH BOX": "death box"}, {"DEFENSIVE POTIONS": "defensive potions"}, {"DJINNI'S BRACE": "djinnis brace"}, {"DRAGON'S MAIL": "dragons mail"}, + {"DUCK": "duck"}, {"DUMMYBOT": "dummybot"}, {"DYRNWYN": "dyrnwyn"}, {"EMPTY BOTTLE": "empty bottle"}, {"FACE ACCESSORIES": "face accessories"}, {"FIRE & HUNTING AMMO": "fire and hunting ammo"}, {"FIST WEAPONS": "fist weapons"}, + {"FLAILS": "flails"}, + {"FRYPAN": "frypan"}, + {"GREATSWORDS": "greatswords"}, {"GUNGNIR": "gungnir"}, {"GYROBOT": "gyrobot"}, {"HATS & HOODS": "hats & hoods"}, {"IRON ARMOR": "iron armor"}, + {"KEYS": "keys"}, {"KHRYSELAKATOS": "khryselakatos"}, {"LEATHER ARMOR": "leather armor"}, {"LOCKPICK": "lockpick"}, @@ -128,11 +148,15 @@ {"ORACLE'S TREADS": "oracles treads"}, {"PARASHU": "parashu"}, {"POLEARMS": "polearms"}, + {"QUILTED ARMOR": "quilted armor"}, + {"RAPIER": "rapier"}, + {"RATIONS": "rations"}, {"ROCKS & GEMS": "rocks & gems"}, {"SENTRY BOT & SPELLBOT": "sentrybot and magic sentry"}, {"SHARUR": "sharur"}, {"SHIELDS": "shields"}, - {"SILVER & PIERCING AMMO": "silver and piercing ammo"}, + {"SHILLELAGH": "shillelagh"}, + {"SILVER & PIERCING AMMO": "silver and piercing ammo"}, {"SKELETON KEY": "skeleton key"}, {"SLINGSHOT": "slingshot"}, {"SPHINX'S CASQUE": "sphinxs casque"}, diff --git a/lang/compendium_lang/contents_magic.json b/lang/compendium_lang/contents_magic.json index 16219383f..04ea4a751 100644 --- a/lang/compendium_lang/contents_magic.json +++ b/lang/compendium_lang/contents_magic.json @@ -3,16 +3,22 @@ "contents": [ {" MAGIC ITEMS": "-"}, {"MAGICSTAFFS": "magicstaffs"}, + {"SCEPTER": "scepter"}, + {"SOURCE FOCI": "source_foci"}, + {"HOLY SYMBOLS": "holy_symbols"}, + {"DARK ICONS": "dark_icons"}, + {"MUSICAL INSTRUMENTS": "instruments"}, {"ENCHANTED RINGS": "enchanted rings"}, {"ENCHANTED AMULETS": "enchanted amulets"}, + {"JEWELS": "jewels"}, {" ARCANA": "-"}, {"SCROLLS": "scrolls"}, {"ENCHANTED FEATHER": "enchanted feather"}, {" SORCERY": "-"}, {"EVOCATION": "evocation"}, {"CONTRAVENTION": "contravention"}, - {"KINESIS": "kinesis"}, - {"TEMULTURGY": "temulturgy"}, + {"KINESIS": "kinesis"}, + {"TEMULTURGY": "temulturgy"}, {" THAUMATURGY": "-"}, {"BLESSINGS": "blessings"}, {"DIVINATIONS": "divinations"}, @@ -25,22 +31,28 @@ {"IMPETURIA": "impeturia"} ], "contents_alphabetical": [ + {"SCEPTER": "scepter"}, {"BLESSINGS": "blessings"}, {"CONTRAVENTION": "contravention"}, {"DAIMONIA": "daimonia"}, + {"DARK ICONS": "dark_icons"}, {"DIVINATIONS": "divinations"}, {"ENCHANTED AMULETS": "enchanted amulets"}, {"ENCHANTED FEATHER": "enchanted feather"}, {"ENCHANTED RINGS": "enchanted rings"}, {"EVOCATION": "evocation"}, + {"HOLY SYMBOLS": "holy_symbols"}, {"IMPETURIA": "impeturia"}, + {"JEWELS": "jewels"}, {"KINESIS": "kinesis"}, {"MAGICSTAFFS": "magicstaffs"}, {"MEDITATIONS": "meditations"}, {"MIRACULOUS FEATS": "miraculous feats"}, + {"MUSICAL INSTRUMENTS": "instruments"}, {"PSIANIMUS": "psianimus"}, {"SARKOMANCY": "sarkomancy"}, {"SCROLLS": "scrolls"}, + {"SOURCE FOCI": "source_foci"}, {"TEMULTURGY": "temulturgy"} ] } \ No newline at end of file diff --git a/lang/compendium_lang/contents_monsters.json b/lang/compendium_lang/contents_monsters.json index 68d47edb3..aa2cf3a78 100644 --- a/lang/compendium_lang/contents_monsters.json +++ b/lang/compendium_lang/contents_monsters.json @@ -12,13 +12,13 @@ {" BEASTS": "-"}, {"RAT": "rat"}, {"ALGERNON": "algernon"}, + {"BAT": "bat"}, {"SPIDER": "spider"}, {"CRAB": "crab"}, {"SHELOB": "shelob"}, {"BUBBLES": "bubbles"}, {"TROLL": "troll"}, {"THUMPUS": "thumpus"}, - {"BAT": "bat"}, {"SCORPION": "scorpion"}, {"SKRABBLAG": "skrabblag"}, {"SCARAB": "scarab"}, @@ -38,6 +38,7 @@ {"GHOULS": "ghoul"}, {"CORAL GRIMES": "coral grimes"}, {"ENSLAVED GHOULS": "enslaved ghoul"}, + {"REVENANT SKULL": "revenant_skull"}, {"VAMPIRE": "vampire"}, {"BRAM KINDLY": "bram kindly"}, {"SHADOW": "shadow"}, @@ -57,13 +58,14 @@ {"BAPHOMET": "devil"}, {" CONSTRUCTS": "-"}, {"AUTOMATON": "automaton"}, + {"CRYSTAL GOLEM": "crystalgolem"}, {"SENTRYBOT": "sentrybot"}, {"SPELLBOT": "spellbot"}, {"DUMMYBOT": "dummybot"}, - {" ELEMENTALS": "-"}, + {"MIMIC": "mimic"}, + {" ELEMENTALS": "-"}, {"SLIMES": "slime"}, - {"CRYSTAL GOLEM": "crystalgolem"}, - {"MIMIC": "mimic"} + {"EARTH SPRITE": "earth_elemental"} ], "contents_alphabetical": [ {"ALGERNON": "algernon"}, @@ -83,6 +85,7 @@ {"DEMON": "demon"}, {"DEU DE'BREAU": "deudebreau"}, {"DUMMYBOT": "dummybot"}, + {"EARTH SPRITE": "earth sprite"}, {"ENSLAVED GHOULS": "enslaved ghoul"}, {"ERUDYCE": "lichice"}, {"FUNNY BONES": "funny bones"}, @@ -104,6 +107,7 @@ {"MYSTERIOUS MERCHANT": "mysterious shop"}, {"ORPHEUS": "lichfire"}, {"RAT": "rat"}, + {"REVENANT SKULL": "revenant_skull"}, {"SCARAB": "scarab"}, {"SCORPION": "scorpion"}, {"SENTRYBOT": "sentrybot"}, diff --git a/lang/compendium_lang/contents_world.json b/lang/compendium_lang/contents_world.json index 66cf4c1f9..aca454de4 100644 --- a/lang/compendium_lang/contents_world.json +++ b/lang/compendium_lang/contents_world.json @@ -4,6 +4,8 @@ {" FURNISHINGS": "-"}, {"GATE": "portcullis"}, {"LEVER": "lever"}, + {"BUTTON": "button"}, + {"WALL LOCKS": "wall locks"}, {"DOOR": "door"}, {"MURKY WATER": "murky water"}, {"LAVA": "lava"}, @@ -22,20 +24,23 @@ {"MINES": "mines"}, {"SWAMPS": "swamps"}, {"LABYRINTH": "labyrinth"}, - {"RUINS": "ruins"}, {"UNDERWORLD": "underworld"}, + {"RUINS": "ruins"}, {"HELL": "hell"}, {"CRYSTAL CAVES": "crystal caves"}, - {"ARCANE CITADEL": "arcane citadel"}, + {"ARCANE CITADEL": "arcane citadel"}, {" STATIONS": "-"}, {"SHOP": "shop"}, {"SINK": "sink"}, + {"CAULDRON": "cauldron"}, + {"WORKBENCH": "workbench"}, {"FOUNTAIN": "fountain"}, - {"CHEST": "chest"}, + {"CHEST": "chest"}, {"GRAVESTONE": "gravestone"}, + {"ASSIST SHRINE": "assist shrine"}, {"DAEDALUS SHRINE": "daedalus"}, {"OBELISK": "obelisk"}, - {"BELL": "bell"}, + {"BELL": "bell"}, {" TRAPS": "-"}, {"BOULDER TRAP": "boulder trap"}, {"ARROW TRAP": "arrow trap"}, @@ -43,6 +48,7 @@ {"MAGIC TRAP": "magic trap"}, {"SUMMONING TRAP": "summoning trap"}, {"BRIMSTONE BOULDER": "brimstone boulder"}, + {"ARCANE BOULDER": "arcane boulder"}, {"CEILING TRAP": "ceiling trap"}, {" SECRET PLACES": "-"}, {"GNOMISH MINES": "gnomish mines"}, @@ -51,9 +57,9 @@ {"HAUNTED CASTLE": "haunted castle"}, {"SOKOBAN": "sokoban"}, {"MINOTAUR MAZE": "minotaur maze"}, - {"MYSTIC LIBRARY": "mystic library"}, + {"MYSTIC LIBRARY": "mystic library"}, {"COCKATRICE LAIR": "cockatrice lair"}, - {"BRAM'S CASTLE": "brams castle"}, + {"BRAM'S CASTLE": "brams castle"}, {" GUILD LORE": "-"}, {"MERCHANTS' GUILD": "merchants guild"}, {"MAGICIANS' GUILD": "magicians guild"}, @@ -62,13 +68,17 @@ {"MASONS' GUILD": "masons guild"} ], "contents_alphabetical": [ + {"ARCANE BOULDER": "arcane boulder"}, {"ARCANE CITADEL": "arcane citadel"}, {"ARROW TRAP": "arrow trap"}, + {"ASSIST SHRINE": "assist shrine"}, {"BELL": "bell"}, {"BOULDER TRAP": "boulder trap"}, {"BRAM'S CASTLE": "brams castle"}, {"BREAKABLE BARRIERS": "breakable barriers"}, {"BRIMSTONE BOULDER": "brimstone boulder"}, + {"BUTTON": "button"}, + {"CAULDRON": "cauldron"}, {"CEILING TRAP": "ceiling trap"}, {"CHEST": "chest"}, {"CITADEL SANCTUM": "citadel sanctum"}, @@ -113,6 +123,8 @@ {"THE CHURCH": "the church"}, {"THE MOLTEN THRONE": "molten throne"}, {"TRANSITION FLOOR": "transition floor"}, - {"UNDERWORLD": "underworld"} + {"UNDERWORLD": "underworld"}, + {"WALL LOCKS": "wall locks"}, + {"WORKBENCH": "cauldron"} ] } \ No newline at end of file diff --git a/lang/compendium_lang/lang_codex.json b/lang/compendium_lang/lang_codex.json index 50c301f28..943e1bf41 100644 --- a/lang/compendium_lang/lang_codex.json +++ b/lang/compendium_lang/lang_codex.json @@ -49,28 +49,30 @@ ], "details_line_highlights": [] }, - "appraisal skill": { + "lore skill": { "blurb": [ - "While any adventurer can see what an item", - "is just by looking at it, APPRAISAL reveals", - "details, especially magical ones. The more", - "valuable the item, the more challenging it", - "is to appraise." + "While any adventurer can see what an", + "item is just by looking at it, skill in LORE", + "can reveal important magical details.", + "The more valuable the item, the more", + "challenging it is to appraise.", + "Skilled loremasters are great with song." ], "details": [ - "Appraising inexpensive items, like", - "cheese or leather items, is usually", - "great for untrained practice.", - "", - "A few classes start without the ability", + "As adventurers grow in Lore skill,", + "manually managing which items are", + "selected for appraisal can become", + "more important. Higher value items", + "may take much more time to identify.", + "Focus on items that may be of actual", + "use and discard the rest to spend", + "less time fiddling with inventory.", + "", + "A few classes start with poor ability", "to appraise, thanks to very poor", - "perception.", - "", - "Using unidentified potions or scrolls", - "may also yield an increase to", - "appraisal skill, up to Basic (20) skill.", - "", - "Glasses may also help." + "perception. Using unidentified potions", + "or scrolls may yield an increase to", + "lore skill, up to Basic (20) skill." ], "details_line_highlights": [] }, @@ -167,26 +169,6 @@ ], "details_line_highlights": [] }, - "casting skill": { - "blurb": [ - "Improving CASTING SKILL reduces the", - "chances that a spell fizzles, and increases", - "MP RGN. Upon reaching legendary casting,", - "an adventurer gains the forcebolt", - "spell as a cantrip." - ], - "details": [ - "When your casting skill is low, casting", - "low-cost spells will be the most", - "reliable path to growth.", - "", - "It may be tempting to cast a big spell,", - "but it may fizzle and rob you of many", - "chances to cast more, cheaper spells", - "by emptying your MP/EN/ST supply." - ], - "details_line_highlights": [] - }, "chr": { "blurb": [ "Each point in CHARISMA (CHR) improves the", @@ -211,7 +193,7 @@ "Gaining early access to a shopkeeper's", "private stock can mean the difference", "between a great adventure and a", - "desparate one." + "desperate one." ], "details_line_highlights": [] }, @@ -505,27 +487,6 @@ ], "details_line_highlights": [] }, - "magic skill": { - "blurb": [ - "Improving MAGIC SKILL allows an", - "adventurer to learn more advanced spells", - "and increases PWR. A legend in magic", - "also learns the dominate spell." - ], - "details": [ - "A good spellcaster always stays in", - "practice!", - "", - "For those with MP/ST regeneration, a", - "full bar means you're losing out on", - "mana you could be recuperating.", - "", - "While it's good to save it for when you", - "need it, chip away at the top of a full", - "bar to keep training." - ], - "details_line_highlights": [] - }, "melee": { "blurb": [ "Simply wield your fist or a melee weapon,", @@ -898,16 +859,16 @@ "and penalties." ], "details": [ - "Most monster races can recruit their", - "own kind. Only humans and automatons", - "are welcome inside human-run shops.", - "", - "When selecting your race during", - "character creation, use the \"Show", - "Race Info\" button to reveal all of", - "your strengths, weaknesses, abilities,", - "and allegiances.", - "", + "Most races can recruit their own kind.", + "While human shopkeepers will", + "attack threats on sight, salamanders,", + "myconids, dryads, gnomes, and", + "automatons are welcome in shops.", + "", + "When selecting your race, use", + "the \"Show Race Info\" button to", + "reveal all strengths, weaknesses,", + "abilities, and allegiances.", "To make the dungeons easier, pair", "each race with a class that minimizes", "the downsides, and doubles-down on", @@ -1150,26 +1111,6 @@ ], "details_line_highlights": [] }, - "swimming skill": { - "blurb": [ - "Swimming isn't usually the most important", - "skill, but when you're fleeing from a", - "dangerous threat, it might just be the most", - "important skill of your life. Especially", - "useful in the swamps." - ], - "details": [ - "While the waterwalking provided by", - "the swimming skill's legendary bonus", - "can be useful in many situations, some", - "adventurers may find themselves", - "wishing they could take a quick dip to", - "put out burning flames.", - "", - "Consider your next soak carefully!" - ], - "details_line_highlights": [] - }, "sword skill": { "blurb": [ "Slashing against foes with a sword", @@ -1392,5 +1333,370 @@ "you can keep gaining experience." ], "details_line_highlights": [] + }, + "sorcery skill": { + "blurb": [ + "The SORCERY SKILL allows casters to", + "learn more advanced Sorcery spells and", + "increases PWR using them.", + "A Legend in Sorcery has increased", + "casting speeds and even greater PWR", + "while using their Sorcery spells." + ], + "details": [ + "Sorcery is the most direct of all", + "magic disciplines for adventuring.", + "No other school provides such", + "prolific access to combat spells", + "at all levels of skill. In addition,", + "powerful utility and defensive magic", + "are found in the sorcerer's grimoire.", + "", + "Like all magic users, sorcerers", + "should trade out spells as they grow", + "in skill. But they'll do well to", + "remember their most reliable spells", + "when facing greater challenges." + ], + "details_line_highlights": [] + }, + "thaumaturgy skill": { + "blurb": [ + "The THAUMATURGY SKILL allows casters", + "to learn more advanced Thaumaturgy", + "spells and increases PWR using them.", + "A Legend in Thaumaturgy has increased", + "casting speeds and even greater PWR", + "while using their Thaumaturgy spells." + ], + "details": [ + "The preferred school for devout", + "adventurers, thaumaturges provide", + "protection, restoration, and access to", + "divine wisdom or providence. These", + "offer assistance both to individual", + "adventurers and their parties.", + "", + "Though most reliable for utility uses,", + "some spells offer divine wrath against", + "undead and demonic foes.", + "", + "Thaumaturges tend to succeed when", + "magic is used to supplement other", + "practical adventuring skills." + ], + "details_line_highlights": [] + }, + "mysticism skill": { + "blurb": [ + "The MYSTICISM SKILL allows casters", + "to learn more advanced Mysticism", + "spells and increases PWR using them.", + "A Legend in Mysticism has increased", + "casting speeds and even greater PWR", + "while using their Mysticism spells." + ], + "details": [ + "Spells available to mystics tend to", + "provide methods of control. Though", + "often indirect compared to the other", + "schools, a successful mystic prefers", + "to create and exploit opportunity.", + "", + "While not as reliable as sorcery for", + "combat spells, mysticism attack spells", + "may open up or pay off on other kinds", + "of opportunities to devastating effect.", + "", + "Mysticism reliably allows dominion", + "over objects and minds for the", + "more strategic adventurer." + ], + "details_line_highlights": [] + }, + "offhand casting": { + "blurb": [ + "Different kinds of magic items can be", + "held for OFF-HAND CASTING, allowing the", + "wielder to channel a magic effect.", + "Channeling may rapidly recast the item's", + "effect, or build up a spell's charge,", + "until channeling is released." + ], + "details": [ + "Three categories of off-hand casting", + "items align with the three main schools", + "of magic. These benefit the spells cast", + "from that school with additional PWR,", + "but they also provide their own magic", + "effects. The fourth category benefits", + "from the Lore skill.", + "", + "SOURCE FOCI:", + "Particularly for sorcerers who like", + "to keep cleverer spells prepared, an", + "elemental source focus provides fast", + "access to quick blasts of damage.", + "", + "HOLY SYMBOL:", + "Carried faithfully by thaumaturges,", + "holy symbols reward preparation with", + "largely preventative benefits.", + "", + "DARK ICON:", + "Ideal for when one might have allies", + "to buy time in a fight, dark icons", + "afflict powerful curses at some", + "distance. The curse applies to an", + "area, potentially giving greater", + "control over enemy combatants.", + "", + "BARD INSTRUMENTS:", + "Continually adding powerful buffs", + "to the player and any nearby allies", + "or followers, Lore and Charisma aids", + "the musician as they pour MP into", + "their song for supernatural benefits." + ], + "details_line_highlights": [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + { + "color": { + "r": 198, + "g": 190, + "b": 179, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + {}, + { + "color": { + "r": 198, + "g": 190, + "b": 179, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + {}, + { + "color": { + "r": 198, + "g": 190, + "b": 179, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 198, + "g": 190, + "b": 179, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + }, + { + "color": { + "r": 157, + "g": 129, + "b": 93, + "a": 255 + } + } + ] + }, + "gold": { + "blurb": [ + "GOLD is a resource used most often", + "by adventurers for trade at shops.", + "It has weight, so it's wise not to", + "lug it around without intention.", + "Some spells can consume gold to", + "power a material-altering effect." + ], + "details": [ + "Gold may often purchase great", + "opportunity for victory. Whether", + "an adventurer is in need of healing,", + "a new spell, food, or a new piece of", + "equipment, gold may be the sole", + "resource which could solve all of", + "these problems, with the right vendor.", + "", + "While gold contributes to the score", + "of any given run at the dungeon,", + "monster races may find little use for", + "the beloved metal, given how rarely", + "they may be able to interact with", + "shops." + + ], + "details_line_highlights": [] } } \ No newline at end of file diff --git a/lang/compendium_lang/lang_items.json b/lang/compendium_lang/lang_items.json index 40e96bd63..ef27feb89 100644 --- a/lang/compendium_lang/lang_items.json +++ b/lang/compendium_lang/lang_items.json @@ -681,5 +681,125 @@ "The traveler carried the spirit out,", "but only its clothes crossed over." ] + }, + "quilted armor": { + "blurb": [ + "While often used for padding underneath", + "metal armors, tightly stitched layers", + "of fine cloth make for good protection", + "on their own.", + "Stacks of fabric can reduce the", + "penetration of arrows and spearheads." + ] + }, + "chain armor": { + "blurb": [ + "Fashioned by the finest smiths, chain armor", + "is a rare commodity.", + "While its flexibility makes it less protective", + "than other metal armors, it is particularly", + "adept at deflecting the cutting edges of ", + "swords and axes." + ] + }, + "frypan": { + "blurb": [ + "Frontier cooks swear by this iron", + "cookware's indestructibility, so long", + "as the metal is treated properly!", + "A campfire is all that's needed for", + "this portable kitchen to function.", + "While unwieldy, it can be used as a shield!" + ] + }, + "rations": { + "blurb": [ + "Connoisseurs of the humble ration are", + "content not knowing what goes into", + "their ready-packed meals.", + "", + "A skilled cook can make them tastier,", + "even for the pickiest of palates." + ] + }, + "sludge balls": { + "blurb": [ + "Despite being a byproduct of food,", + "SLUDGE BALLS have no nutritional value.", + "", + "Resourceful adventurers use them", + "as thrown ammunition to humiliate and", + "debilitate their foes simultaneously." + ] + }, + "keys": { + "blurb": [ + "While KEYS are typically found somewhere", + "near their matching lock, some rare", + "keys are hidden much more carefully.", + "", + "Those made from more precious metals", + "give access to more precious treasures." + ] + }, + "flails": { + "blurb": [ + "Long-chain varieties of FLAIL are", + "rare, but the extra range provided is a", + "substantial advantage for a mace.", + "While whirling it overhead to build", + "momentum for a critical strike,", + "enemies face rapid light attacks." + ] + }, + "greatswords": { + "blurb": [ + "Larger blades like the claymore", + "are made of fine, sturdy steel,", + "whose weight can parry other attacks.", + "Their broad and heavy cutting blades", + "can be used to cleave through multiple", + "nearby foes with a critical strike." + ] + }, + "rapier": { + "blurb": [ + "In the hands of a dextrous fencer, the", + "nimble RAPIER can riposte when parrying,", + "allowing a talented swordsman to defeat", + "foes with a patient and consistent defense.", + "Their fine craftsmanship makes them an", + "exceptionally rare favorite of Bards." + ] + }, + "duck": { + "blurb": [ + "Like many birds, DUCKS are territorial", + "and loyal to their partners for life. They'll", + "courageously pester deadly threats, and", + "are surprisingly deft at avoiding harm.", + "They may become distracted by fishing", + "opportunities." + ] + }, + "bolas": { + "blurb": [ + "Simple BOLAS are a favorite tool for", + "guerilla skirmishers, using weights at", + "the end of cord to make a projectile", + "with one job: Entangling its target.", + "While they deal little damage, victims", + "are very vulnerable while wrapped." + ] + }, + "shillelagh": { + "blurb": [ + "A knobby length of wood is cured", + "according to folk tradition, making", + "SHILLELAGHS nearly unbreakable.", + "Hermits hollow and fill the clubbed", + "end with dense magically-reactive metal,", + "empowering strikes against hindered foes." + ] } } \ No newline at end of file diff --git a/lang/compendium_lang/lang_magic.json b/lang/compendium_lang/lang_magic.json index b143f3973..4f9ae0683 100644 --- a/lang/compendium_lang/lang_magic.json +++ b/lang/compendium_lang/lang_magic.json @@ -13,7 +13,7 @@ "blurb": [ "While most magic could be described as", "breaking the rules of reality, the study", - "of CONTRAVENTION allows sorcerors to", + "of CONTRAVENTION allows sorcerers to", "change how rules effect one's self.", "This requires ongoing magic energy to", "sustain, but can be disabled at will." @@ -71,7 +71,7 @@ }, "evocation": { "blurb": [ - "Sorcerors practice EVOCATION to conjure", + "Sorcerers practice EVOCATION to conjure", "energies and material using their minds.", "Largely this is used to launch harmful", "projectiles at enemies by adventurers,", @@ -84,7 +84,7 @@ "Mystics who practice IMPETURIA do so by", "calling to the spirit of the object they", "wish to manipulate, and compel it to alter", - "its state. Sorcerors attempting to do the", + "its state. Sorcerers attempting to do the", "same thing with kinesis have so far been", "unsuccessful." ] @@ -164,8 +164,68 @@ "not simply hurl one force at another.", "The study of TEMULTURGY is specialized", "toward disrupting an object's structure.", - "So far sorcerors haven't figured out", + "So far sorcerers haven't figured out", "how to practice this on living targets." ] - } + }, + "scepter": { + "blurb": [ + "With its incredible destructive potential", + "the ARCHON SCEPTER is fed by the wielder's", + "surrendered memories.", + "Those found wielding one usually claim", + "they own it by birthright. Others cannot", + "recall where they obtained it." + ] + }, + "source_foci": { + "blurb": [ + "Sorcerers often carry raw elemental", + "manifestations as SOURCE FOCI, improving", + "their connection to magical energies.", + "Each one emits a glow, and readily", + "provides a channel for rapid evocation", + "when needed for dealing with threats." + ] + }, + "holy_symbols": { + "blurb": [ + "Casting out darkness wherever they go,", + "devout thaumaturgists wield HOLY SYMBOLS.", + "Dedicating a prayer with one of these in", + "hand provides its blessing to any willing", + "soul when the magical chant concludes.", + "Taking more time improves the effect." + ] + }, + "dark_icons": { + "blurb": [ + "DARK ICONS are used by mystics to", + "channel their magical arts to lay", + "curses against their enemies.", + "Each one provides its own malus that", + "the caster invokes with whispers,", + "hindering enemies from some distance." + ] + }, + "instruments": { + "blurb": [ + "While mundane on their own, MUSICAL", + "INSTRUMENTS channel magic from the", + "musician's spirit, uplifting friends", + "who are present to hear it.", + "When played with dedication and care,", + "songs hang in the air for a while." + ] + }, + "jewels": { + "blurb": [ + "JEWELS are valuable and alluring to", + "most intelligent creatures, serving", + "as fine payment for loyal followers.", + "Many jewelers embed their gemstones", + "in gold, enhancing their beauty", + "in addition to their value." + ] + } } \ No newline at end of file diff --git a/lang/compendium_lang/lang_monsters.json b/lang/compendium_lang/lang_monsters.json index 456d3011c..b35e64306 100644 --- a/lang/compendium_lang/lang_monsters.json +++ b/lang/compendium_lang/lang_monsters.json @@ -781,7 +781,9 @@ "Their slimy meat is unappealing to some", "but it is quite nutritious." ], - "abilities": [], + "abilities": [ + "- Weakness Cursing Attacks" + ], "inventory": [ "- Tomalleys", "- Low value gemstones" @@ -1029,7 +1031,7 @@ }, "xyggi": { "blurb": [ - "The cult of sorcerors believes that all", + "The cult of sorcerers believes that all", "intellect is born of magic, claiming that", "where you find one, the other will follow.", "", @@ -1045,5 +1047,149 @@ "- Tomalleys", "- Low value gemstones" ] - } + }, + "gremlin": { + "blurb": [ + "GREMLINS are creatures with such taste", + "for chaos that they could not coexist", + "with the denizens of Hell itself.", + "Only goblins suffer their company,", + "and even then, at arm's length.", + "NOT TO BE TRUSTED." + ], + "abilities": [ + "- Deface", + "- Vandalize" + ], + "inventory": [ + "- Bone and Blackiron Weapons", + "- Shawls and Hoods" + ] + }, + "dryad": { + "blurb": [ + "While the origins are unclear, a forest", + "of sufficient size and age is often under", + "the protection of DRYAD conservators.", + "Sightings in any realm are rare, but it", + "is best to remain in their good", + "graces if one finds you." + ], + "abilities": [ + "- Roots", + "- Thorns" + ], + "inventory": [ + "- Nuts", + "- Branch Weapons" + ] + }, + "myconid": { + "blurb": [ + "Wherever you find decay in dryad-", + "protected forests, there you will also", + "find MYCONIDS. While the two aren't", + "hostile to one another, they also do", + "not appear to cohabitate. Beware caves", + "full of mushrooms that may be guarded." + ], + "abilities": [ + "- Spores" + ], + "inventory": [ + "- Mushroom Slices", + "- Bone Weapons" + ] + }, + "salamander": { + "blurb": [ + "Legends say that SALAMANDERS carry the", + "fires of loyalty and justice with fierce", + "devotion to the Eternals in their hearts.", + "When enraged, they appear as blazing", + "angels, but rely on sturdy tradition", + "during times of lower passion." + ], + "abilities": [ + "- Breathe Fire", + "- Radiant Form", + "- Stone Form" + ], + "inventory": [ + "- Silver Weapons and Armor", + "- Claymores", + "- Holy Symbols" + ] + }, + "revenant_skull": { + "blurb": [ + "A product of dark mysticism, the", + "REVENANT SKULL is summoned by piercing", + "void just as a cursed soul crosses over.", + "Though useful, it's considered by the", + "cult of sorcerers to be an", + "incomplete spell." + ], + "abilities": [ + "- Levitate", + "- Slow" + ], + "inventory": [ + + ] + }, + "fire sprite": { + "blurb": [ + "Whether through naivety or mischief,", + "FIRE SPRITES like to bless others with", + "a gift of burning flames. Novice", + "sorcerers can learn a lot from them, as", + "their temprament is the calmest of the", + "fire spirits. A slow burn, if you will." + ], + "abilities": [ + "- Levitate", + "- Fire Dart" + ], + "inventory": [ + + ] + }, + "earth_elemental": { + "blurb": [ + "The stubborn EARTH SPRITE requires the", + "talents of a skilled mystic to conjure.", + "After being lured into local earth, it", + "crashes down and serves as a", + "durable guardian for as long", + "as its form holds shape." + ], + "abilities": [ + "- Cave In", + "- Levitation", + "- Spin (self)", + "- Block" + ], + "inventory": [ + + ] + }, + "duck": { + "blurb": [ + "Like many birds, DUCKS are territorial,", + "and loyal to their partners for life.", + "They will courageously pester deadly", + "threats, and are surprisingly deft at", + "avoiding harm. Ducks may become", + "distracted by fishing opportunities." + ], + "abilities": [ + "- Distract", + "- Levitation", + "- Invulnerable" + ], + "inventory": [ + + ] + } } \ No newline at end of file diff --git a/lang/compendium_lang/lang_world.json b/lang/compendium_lang/lang_world.json index b9e7db6a5..c0934c28a 100644 --- a/lang/compendium_lang/lang_world.json +++ b/lang/compendium_lang/lang_world.json @@ -846,10 +846,10 @@ "This also reveals the map between the", "shrine and the exit!", "", - "If activated within 30 seconds of the", - "minotaur's arrival, the shrine boosts", - "user's escape with an additional", - "blessing of speed.", + "If activated within 30 seconds before", + "the minotaur's arrival, the shrine", + "boosts the user's escape with an", + "additional blessing of speed.", "", "Adventurers exploring together may", "find a bit more confidence splitting", @@ -1986,19 +1986,16 @@ ], "details": [ "SECRET LEVEL TREASURES:", - "- Red Mystic Orb", "- Gungnir", "", - "For most, it is wise to grab the orb", - "and outrun the minotaur to the exit.", "Look for nearby gatehouse levers if", "closed gates block the way. A spell", "of opening, or digging, can help!", "", - "The pedestal at the end of the maze", - "cripples the minotaur temporarily", - "when the orb is placed upon it.", - "Don't forget to reclaim the orb!" + "The silver key at the start of the", + "maze gives access to Gungnir in the", + "center. But the key may be useful if", + "you don't have need of the spear!" ], "details_line_highlights": [ { @@ -2090,7 +2087,6 @@ ], "details": [ "SECRET LEVEL TREASURES:", - "- Blue Mystic Orb", "- Chests of random magic items", "- Several humans to recruit", "", @@ -2504,7 +2500,7 @@ ], "details": [ "SECRET LEVEL TREASURES:", - "- Djinn's Brace", + "- Djinni's Brace", "- A fortune in gold pieces", "", "To solve this challenge, one must rid", @@ -2672,13 +2668,12 @@ ], "details": [ "SECRET LEVEL TREASURES:", - "- Green Mystic Orb", "- 4 Potions of strength", "- A hoard of gold pieces", "- Several treasure chests", "", - "Hidden rooms make use of the orb's", - "power, revealing more treasures.", + "Hidden rooms make use of a silver", + "key, revealing more treasures.", "The Temple's traps may shut down", "access to secret areas if explorers", "are careless, or prisoners are freed.", @@ -2800,7 +2795,7 @@ "SECRET LEVEL TREASURES:", "- Khryselakatos (Lvl 7)", "", - "The underworld can be accessed tiwce", + "The underworld can be accessed twice", "via secret entrances on floor 5 and 18.", "In both cases, adventurers who find", "a trapdoor down to a second floor of", @@ -2852,5 +2847,163 @@ } } ] + }, + "cauldron": { + "blurb": [ + "Whether you need to boil potion", + "mixtures, or cook up a hot meal,", + "the sturdy CAULDRON is a practical", + "fixture for any facility.", + "Shame it's too heavy", + "to take along with you!" + ], + "details": [ + "Finding a cauldron is an opportunity", + "for any adventurer, whether or not", + "they are skilled in alchemy, to ease", + "pressure on inventory space. Cooking", + "consolidates any food into rations,", + "while decanting consolidates potions.", + "", + "Combining potions and rations allows", + "for the creation of flavored food", + "which can enhance adventurer stats.", + "", + "Those with 80 or more skill in alchemy", + "are able to make fresh rations, safe", + "from the perils of indigestion." + ], + "details_line_highlights": [] + }, + "workbench": { + "blurb": [ + "While not as convenient as the portable", + "tinkering kit, the WORKBENCH has all the", + "same tools available for resourceful", + "adventurers to fashion useful items for", + "the obstacles which lie ahead." + ], + "details": [ + "As a sturdier table and set of heftier", + "tools, workbenches do not degrade,", + "making it an efficient find, even if", + "carrying a tinkering kit.", + "", + "Many adventurers may benefit from", + "occasional tinkering, particularly", + "for keeping tools handy, like", + "empty flasks, lockpicks, or traps.", + "", + "As only the mechanist class starts with", + "the kit, but several others have some", + "tinkering skill, the workbench allows", + "broader use of tinkering tools." + ], + "details_line_highlights": [] + }, + "button": { + "blurb": [ + "Like the lever, BUTTONS can be pressed", + "manually, but may also be struck from", + "afar with projectiles to activate", + "dungeon mechanisms.", + "Some are designed to reset after a time." + ], + "details": [ + "Not much needs to be said about the", + "humble dungeon button.", + "", + "Architects may prefer buttons over", + "levers, as no clear floor space is", + "required, and buttons are easier", + "to hide on walls where outsiders", + "are not intended to access.", + "", + "Beware a button which resets, as", + "it usually heralds a puzzle, or", + "perhaps a trap for the careless." + ], + "details_line_highlights": [] + }, + "wall locks": { + "blurb": [ + "Usually used to secure impenetrable iron", + "doors, the WALL LOCK requires a key made", + "from matching materials.", + "Locks may operate other dungeon gadgets", + "and, in some cases, may be toggled to", + "activate and deactivate a gadget." + ], + "details": [ + "As iron doors are most often used to", + "guard precious items or gold, keys are", + "often traded for treasures.", + "Adventurers should make use of the", + "handy window installed in iron doors", + "to first peek at the prize within,", + "presuming they're tall enough.", + "", + "Iron, Bronze, and some Silver locks", + "may be picked by sufficiently skilled", + "Tinkerers, or with a Skeleton Key.", + "But as the value of the treasure", + "increases with the depth of the", + "dungeon, so too does security." + ], + "details_line_highlights": [] + }, + "arcane boulder": { + "blurb": [ + "Though boulders are already a deadly", + "hazard, concentrations of crystal ", + "imbues them with dangerous magic.", + "", + "Arcane boulders roll with enhanced speed", + "and momentum, defying physical laws." + ], + "details": [ + "The most deadly boulder trap, those", + "who live long enough to face these", + "imposing orbs should be adept at", + "avoiding them.", + "", + "Beware, they fall and roll much", + "faster, and can roll over liquid", + "and pits unimpeded, and their", + "crushing power is enhanced with", + "a magical detonation.", + "", + "Those with Push or Telekinesis may", + "be able to put these magical", + "qualities to use." + ], + "details_line_highlights": [] + }, + "assist shrine": { + "blurb": [ + "The ASSIST SHRINE did not always grace", + "the Minehead entry. The Church claims the", + "Eternals placed it there to:", + "''Bless the courageous to defeat Herx and", + "end his curse; Give hope to the hopeless.''", + "(for better or worse)" + ], + "details": [ + "When claiming items from the assist", + "shrine, a party should be aware that", + "they will be judged according to the", + "sum total of assistance taken.", + "For parties with mixed expertise, this", + "can allow stronger adventurers to", + "bear the full challenge, while novices", + "face a much less daunting challenge.", + "", + "Adventuring solo, even novice heroes", + "can accomplish great feats by taking", + "much more assistance.", + "", + "There is no shame in doing so!" + ], + "details_line_highlights": [] } } \ No newline at end of file diff --git a/lang/en.txt b/lang/en.txt index a156ba9c9..68d2a4e52 100644 --- a/lang/en.txt +++ b/lang/en.txt @@ -397,11 +397,11 @@ thou makest use of them.# 236 Tinkering# 237 Stealth# 238 Trading# -239 Appraise# -240 Swimming# +239 Lore# +240 Thaumaturgy# 241 Leader# -242 Casting# -243 Magic# +242 Mysticism# +243 Sorcery# 244 Ranged# 245 Sword# 246 Mace# @@ -664,8 +664,8 @@ thou makest use of them.# 446 You shot the %s!# 447 But it didn't even flinch!# 448 You shot %s!# -449 But she didn't even flinch!# -450 But he didn't even flinch!# +449 But they didn't even flinch!# +450 But they didn't even flinch!# 451 You are hit by an arrow!# 452 But it bounced off your armor!# 453 The arrow poisons you!# @@ -1065,8 +1065,8 @@ You should call a plumber!# 692 You killed the %s!# 693 You critically hit %s!# 694 You hit %s!# -695 She didn't even flinch!# -696 He didn't even flinch!# +695 They didn't even flinch!# +696 They didn't even flinch!# 697 You killed %s!# 698 The %s %s!# 699 %s %s!# @@ -1301,17 +1301,17 @@ Thank you!# 851 A field of light surrounds you!# 852 This scroll is blank.# 853 Your hand feels warm for a moment.# -854 Your weapon glows black.# +854 Your weapon glows ominously.# 855 Your weapon glows blue.# 856 Your weapon violently glows blue.# 857 Your skin feels warm for a moment.# -858 Your %s glows black.# +858 Your %s glows ominously.# 859 Your %s glows blue.# 860 Your %s violently glows blue.# 861 You feel like someone is helping you.# 862 You feel funny for a moment.# 863 The scroll smokes and crumbles to ash!# -864 The scroll erupts in a tower of flame!# +864 The scroll erupts in a circle of flame!# 865 A feast suddenly appears before you!# 866 Your stomach grumbles, but it passes.# 867 Your food pouch feels lighter!# @@ -1942,8 +1942,8 @@ Compatible save file not found.# 1506 goes for a swim in some lava.# 1507 walks into a spike trap and dies.# 1508 gets killed by a flying %s.# -1509 was betrayed by his fellow %s.# -1510 was betrayed by her fellow %s.# +1509 was betrayed by a fellow %s.# +1510 was betrayed by a fellow %s.# 1511 is violently dispatched by an angry human.# 1512 gets nibbled to death by a lowly rat.# 1513 is viciously flayed by a goblin.# @@ -1961,8 +1961,8 @@ Compatible save file not found.# 1525 is unceremoniously pulverized by the minotaur.# 1526 is damned to hell. Literally.# 1527 attempts a robbery and fails.# -1528 kills himself during an invocation.# -1529 kills herself during an invocation.# +1528 was killed attempting an invocation.# +1529 was killed attempting an invocation.# 1530 diets for a little too long.# 1531 is finished off by the poison.# 1532 bleeds to death.# @@ -3596,10 +3596,10 @@ last of your magic is drained!# 3238 DEPRECATED# 3239 Skill number invalid!# -3240 Your appraisal is not high enough to identify +3240 Your Lore skill is not high enough to identify "%s".# 3241 You feel the curse lift as you incant the spell...# -3242 The tomes' knowledge grants you control over the curse!# +3242 The tome's knowledge grants you control over the curse!# 3243 Begone from my shop, %s %s!# 3244 learns about the "no %s allowed" policy of %s's shop.# 3245 %d potions dropped from the fountain!# @@ -4029,7 +4029,7 @@ last of your magic is drained!# 3660 %d complex # 3661 %d intricate # 3662 %d artisan # -3663 Auto salvage...# +3663 Salvage...# 3664 You are unable to salvage %s.# 3665 Salvaged %d %s.# 3666 You have nothing else to learn from salvaging.# @@ -4088,13 +4088,13 @@ any gyrobot findings.# 3717 DEPRECATED# 3718 Inscribe# 3719 Repair# -3720 Select a blank scroll to inscribe# +3720 Select a blank scroll to inscribe:# 3721 DEPRECATED# 3722 DEPRECATED# 3723 Clear Slot# 3724 Inscribed %s!# 3725 You trace over some of the faded text...# -3726 Select a spellbook to repair# +3726 Select a spellbook to repair:# 3727 Your %s is depleted!# 3728 Your %s depletes before you can complete the inscription!# @@ -5072,8 +5072,8 @@ The next input you activate will be bound to this action.# 5217 Mouse & Keyboard# 5218 Mouse Sensitivity# 5219 Control the speed by which mouse movement affects camera movement.# -5220 Number Keys in Inventory# -5221 Allow the player to bind inventory items to the hotbar using the number keys on their keyboard.# +5220 Hotbar Keys in Inventory# +5221 Allow the player to bind inventory items to the hotbar using the hotbar slot keys on their keyboard.# 5222 Reverse Mouse# 5223 Reverse mouse up and down movement for controlling the orientation of the player.# 5224 Smooth Mouse# @@ -6609,7 +6609,87 @@ Host to enable the game setting. (%s %s)# 6356 You claim %d Assistance points from the shrine.# 6357 %s claims %d Assistance points from the Assist shrine.# - +6358 dryad# +6359 myconid# +6360 salamander# +6361 gremlin# +6362 dryads# +6363 myconids# +6364 salamanders# +6365 gremlins# +6366 strikes# +6367 strikes# +6368 strikes# +6369 strikes# +6370 *sigh...*# +6371 *croak*# +6372 Hail, champion.# +6373 Parrat soo skeba da.# +6374 You reset the %s lock.# +6375 You activate the %s lock.# +6376 The %s key is stuck tight!# +6377 The %s lock is currently in use.# +6378 You activate the lock using your %s.# +6379 You have no matching key for that %s lock.# +6380 That button is already pressed.# +6381 You press the button.# +6382 You hit the button!# +# lock types +6383 stone# +6384 spirit# +6385 bronze# +6386 iron# +6387 silver# +6388 gold# +6389 crystal# +6390 machine# +6391 key# +6392 lock# +6393 button# +6394 You see a %s lock.# +6395 You see a %s lock with a key.# +6396 You see a button.# +6397 Use %s key # +6398 # +6399 Reset %s lock# +6400 Activate %s lock# +6401 Press button# +6402 (%d held)# +6403 The iron door's lock is controlled elsewhere!# +6404 You open the iron door.# +6405 You close the iron door.# +6406 The iron door locks as you close it!# +6407 The iron door is locked!# +6408 You lock the iron door!# +6409 You see an iron door.# +6410 Your spell destroys the iron door!# +6411 You open the iron door!# +6412 You hit the iron door!# +6413 The iron door crashes open!# +6414 iron door# +6415 You unlock the iron door.# +6416 You lock the iron door.# +6417 You failed to unlock the iron door.# +6418 This iron door is already unlocked.# +6419 An arcane protection holds the iron door firmly in place!# +6420 Close iron door# +6421 Open iron door# +6422 You lack the skill to pick this %s lock!# +6423 There is already a %s key inside this lock.# +6424 This %s lock can't be picked!# +6425 The skeleton key doesn't fit!# +6426 Your %s activates the %s lock!# +6427 Your %s resets the %s lock.# +6428 You failed to lockpick the %s lock.# +6429 The %s lock seems unable to be reset by lockpicking.# +6430 This %s lock has already been lockpicked open.# +6431 Your song's effect cannot be extended any further!# +6432 Your strike pierced the %s's armor!# +6433 Your strike pierced %s's armor!# +6434 The %s parries your blow!# +6435 %s parries your blow!# +6436 You parry the %s's blow!# +6437 You parry %s's blow!# 6438 Input Device# 6439 The input device for voice chat.# 6440 Voice# @@ -6637,4 +6717,552 @@ Host to enable the game setting. 6462 Raw Input# 6463 Modified# -6599 END# +6464 Guard!# +6465 Your protection stops an attack!# +6466 The %s guards your spell!# +6467 %s guards your spell!# +6468 The %s guards your attack!# +6469 %s guards your attack!# +6470 Your protection stops a spell!# +6471 Your protection stops a flying %s!# +6472 Your protection stops a %s!# +6473 Your ward your body against physical harm!# +6474 You ward your spirit against harmful magics!# +6475 Your ward youself against mundane and magical attack!# +6476 Your joints are filled with nimble grace.# +6477 Your muscles swell with greater might.# +6478 Your mind stirs deeply with wise counsel.# +6479 Your body hardens with sturdy toughness.# +6480 Your magic calls for a target of holy wrath!# +6481 You detect an ally!# +6482 You divine the location of a nearby shrine.# +6483 Nearby perils are revealed to you!# +6484 Nearby perils are revealed to you!# +6485 The location of nearby treasures emerges in your mind...# +6486 You give blessings for the food that sustains you.# +6487 You seek out a wandering soul in search of penance.# +6488 You beg for providence from lowly places.# +6489 You call for willing souls to join your quest!# +6490 Your protection saves you from the boulder!# +6491 Your protection saves you from the falling bell!# +6492 Your protection saves you from the spikes!# +6493 You ward you body against devious traps.# +6494 Divine forces are leading you to treasure!# +6495 An enemy is revealed to you!# +6496 You recall your spell without a target.# +6497 You command holy exorcism!# +6498 Your spell had no target!# +6499 Cast spell...# +6500 Cast spell on # +6501 Cancel# +6502 Your blood is warded against impurities!# +6503 Your spell (%s) was interrupted!# +6504 Failed to sustain spell (%s).# +6505 The ward against impurities fades...# +6506 Your blood stirs with noble purpose!# +6507 Your blood returns to normal...# +6508 Your attacks are enchanted with holy light!# +6509 Holy light fades from your attacks...# +6510 Your attack smites the %s!# +6511 Your attack smites %s!# +6512 Alter Instrument# +6513 Select instrument to transmute:# +6514 This %s + is unable to be altered.# +6515 Your %s transforms into + %s!# +6516 The %s grows in strength!# +6517 The %s shrivels away!# +6518 The %s is severely weakened by your curse!# +6519 The %s is befuddled by magic!# +6520 Your spell focus improves!# +6521 Your weapons are imbued with venom!# +6522 Your curse humbles the %s!# +6523 The %s is crushed by your will.# +6524 You release a torrent of slippery grease!# +6525 You release a wave of raw magical energy!# +6526 %s is weakened!# +6527 The %s is weakened!# +6528 You feel weak!# +6529 The magical venom evaporates from your weapons.# +6530 Your spell focus returns to normal.# +6531 The %s poisons you!# +6532 %s poisons you!# +6533 The %s is poisoned!# +6534 %s is poisoned!# +6535 The amulet feels warm against your neck.# +6536 You feel greasy!# +6537 The %s is compelled to your command!# +6538 Gold Cost:# +6539 Not enough gold# +6540 Insufficient gold!# +6541 Select a weapon to transmute:# +6542 Select an item to transmute:# +6543 Select a jewel to transmute:# +6544 Transmutes Into:# +6545 Metallurgy# +6546 Geomancy# +6547 Forge Key# +6548 Forge Jewel# +6549 Enhance Weapon# +6550 Reshape Weapon# +6551 Alter Arrow# +6552 Not enough mana# +6553 Transmuted %s using + %s.# +6554 Transmuted %dG + using %s.# +6555 Mana Cost:# +6556 Change Focus# +6557 You awaken a void chest!# +6558 Your spell does nothing while the chest is open.# +6559 Wait! That's a void mimic!# +6560 Select an item to send to the void:# +6561 Send To Void# +6562 Sent %s + to the void.# +6563 This does not belong in the void# +6564 This %s + does not belong in the void.# +6565 The void is already being accessed by somebody else.# +6566 The void cannot accept any more items.# +6567 The mimic voids your %s!# +6568 revenant skull# +6569 minimimic# +6570 revenant skulls# +6571 minimimics# +6572 hits# +6573 bites# +6574 *rattle*# +6575 *creak*# +6576 You curse the flesh of the %s!# +6577 You summon a spirited %s!# +6578 There is no room here to summon!# +6579 spirit weapon# +6580 adorcised possession# +6581 flame elemental# +6582 hologram# +6583 moth# +6584 earth sprite# +6585 duck# +6586 monster_unused_6# +6587 monster_unused_7# +6588 monster_unused_8# +6589 adorcised possessions# +6590 flame elementals# +6591 holograms# +6592 moths# +6593 earth sprites# +6594 ducks# +6595 monster_unused_6s# +6596 monster_unused_7s# +6597 monster_unused_8s# +6598 hits# +6599 hits# +6600 hits# +6601 hits# +6602 hits# +6603 hits# +6604 hits# +6605 hits# +6606 hits# +6607 ...# +6608 ...# +6609 ...# +6610 ...# +6611 ...# +6612 *quack!*# +6613 ...# +6614 ...# +6615 ...# +6616 Adorcise# +6617 Select a weapon to summon:# +6618 This %s + cannot be adorcised.# +6619 returns to the floor.# +6620 vanishes!# +6621 Your %s + returns to the floor.# +6622 Insufficient gold!# +6623 The %s is numbed!# +6624 %s is numbed!# +6625 You feel numb!# +6626 dies from delayed pain.# +6627 You feel unstoppable!# +6628 Pain returns to your body!# +6629 The severity of your wounds sets in!# +6630 The %s could not reach any allies!# +6631 %s could not reach any allies!# +6632 The %s loses their courage!# +6633 The %s wanders in search of an ally!# +6634 %s wanders in search of an ally!# +6635 The %s could not reach any foes!# +6636 %s could not reach any foes!# +6637 The %s is encouraged!# +6638 The %s wanders in search of a foe!# +6639 %s wanders in search of a foe!# +6640 The %s becomes a scapegoat!# +6641 You become a scapegoat!# +6642 Your spore pods shrivel away ...# +6643 You bristle with swollen spore pods!# +6644 The %s drops their weapon!# +6645 %s drops their weapon!# +6646 The %s drops their equipment!# +6647 %s drops their equipment!# +6648 Your food returns to normal.# +6649 Your provisions return to normal.# +6650 You beg for your food to be multiplied...# +6651 You beg for your provisions to be multiplied...# +6652 Greater abundance preserved your %s!# +6653 Abundance preserved your %s!# +6654 Your magic protects the %s from damage!# +6655 You cover your equipment with a magic barrier!# +6656 The protective barrier fades from your equipment.# +6657 Your magic could not preserve your %s!# +6658 This trap has already been sabotaged.# +6659 You sabotage the trap!# +6660 You blast the %s!# +6661 You blast %s!# +6662 Some scrap pours out from the trap!# +6663 Your body turns to mist!# +6664 Your body materializes again.# +6665 Your mana could not sustain your mist form!# +6666 Your mist form protects you!# +6667 You create a hologram in your image!# +6668 fades into nothing.# +6669 gets spooked by a revenant skull.# +6670 gets looted by a minimimic.# +6671 gets reduced to ashes by a flame elemental.# +6672 is slain by the invisible hand of an adorcised weapon.# +6673 explodes!# +6674 You conjure a %s!# +6675 You were unable to conjure a %s!# +6676 You are already wielding a %s!# +6677 You splinter the %s's %s!# +6678 You splinter %s's %s!# +6679 You break the %s's %s!# +6680 You break %s's %s!# +6681 Your equipment feels lighter!# +6682 You feel the heft of your equipment again.# +6683 You reach out to items with unseen energy...# +6684 Your grasp on nearby items dissipates.# +6685 Your tether your throwing weapons with invisible magic!# +6686 Your throwing weapons return to normal.# +6687 Your %s returns to you!# +6688 You deface the %s!# +6689 You've disturbed a %s!# +6690 Your demesne door was closed.# +6691 Your demesne door fades.# +6692 There is no room to create a demesne door!# +6693 doorway# +6694 You enchant the doorway!# +6695 You create a portal!# +6696 You step through the portal.# +6697 You forged %d %s!# +6698 gets mothballed.# +6699 Your off-hand items are imbued with defensive force!# +6700 Your off-hand items are imbued with magic reflection!# +6701 ASSIST ITEM SELECTION# +6702 CLASS SELECTION# +6703 RACE SELECTION# +6704 The %s spins around!# +6705 %s spins around!# +6706 The %s gets dizzied!# +6707 %s gets dizzied!# +6708 Vandalize# +6709 Desecrate# +6710 Sanctify# +6711 Bless# +6712 Cleanse# +6713 Select an instrument to summon:# +6714 Select an item to vandalize:# +6715 Select an item to desecrate:# +6716 Select a bottle of water to bless:# +6717 Select an item to sanctify:# +6718 Select food to cleanse:# +6719 This %s + is unable to be vandalized.# +6720 This %s + is unable to be desecrated.# +6721 This %s + is unable to be blessed.# +6722 This %s + is unable to be sanctified.# +6723 This %s + is unable to be cleansed.# +6724 Vandalized %dG using + %s.# +6725 Your %s looks more edible.# +6726 Unable to bless further# +6727 You are already under the effects of %s.# +6728 You begin drawing energy from a well of magic...# +6729 You empower your next cast with ritual preparation...# +6730 You are enveloped by a flame cloak!# +6731 You are surrounded with protective flames!# +6732 Your flame cloak ignites the %s!# +6733 Your flame cloak ignites %s!# +6734 A flame cloak sets you on fire!# +6735 You prepare to absorb hostile magical energy!# +6736 There is no stable ceiling to complete the spell!# +6737 gets rocked by an earth sprite.# +6738 gets overgrown by a dryad.# +6739 gets vandalised by a gremlin.# +6740 gets capped by a myconid.# +6741 learns how cold-blooded a salamander can be.# +6742 %s is hit with a rooting seed!# +6743 The %s is hit with a rooting seed!# +6744 You are hit by a rooting seed!# +6745 %s is hit with a poison seed!# +6746 The %s is hit with a poison seed!# +6747 You are hit by a poison seed!# +6748 There is no ground to complete the spell!# +6749 mushroom# +6750 There is no room to complete the spell!# +6751 You stop shimmering.# +6752 You are covered with shimmering dust!# +6753 gets capped by a mushroom.# +6754 Mushroom# +6755 The air is filled with a thick fog.# +6756 You are pulled by telekinetic force!# +6757 You are caught in a vortex!# +6758 You are slammed back down!# +6759 Leaves# +6760 becomes dust in the wind.# +6761 %s is covered with shimmering dust!# +6762 The %s is covered with shimmering dust!# +6763 You are ensnared!# +6764 The %s is ensnared!# +6765 %s is ensnared!# +6766 The %s was unaffected by the bolas!# +6767 %s was unaffected by the bolas!# +6768 [4 rations needed]# +6769 You combine the %s with some %s...# +6770 You dissolve the %s and collect it with a %s...# +6771 Created %s.# +6772 [No tin opener]# +6773 Cook# +6774 [Insufficient fuel]# +6775 shrub# +6776 footlocker# +6777 weapon rack# +6778 Deserters & Disciples DLC is required +to replay this challenge! + + +# +6779 Gnome# +6780 Gremlin# +6781 Dryad# +6782 Myconid# +6783 Salamander# +6784 bard# +6785 sapper# +6786 scion# +6787 hermit# +6788 paladin# +# main menu capitalized +6789 Bard# +6790 Sapper# +6791 Scion# +6792 Hermit# +6793 Paladin# +6794 You lack the foliage to cast the spell!# +6795 You are covered with thorns!# +6796 You are covered with bladevines!# +6797 Your thorns withdraw.# +6798 Your bladevines crumble away.# +6799 You steady yourself with firm mycelium!# +6800 You plant yourself with sturdy roots!# +6801 You dismount the binding mycelium.# +6802 You dismount the binding roots.# +6803 returns to dust.# +6804 Higher %s skill required# +6805 Higher skill required# +6806 finds its final resting place.# +6807 revenant skeleton# +6808 You command a %s from the deceased!# +6809 You splinter the construction of the %s!# +6810 You splinter the construction of %s!# +6811 Your splintered armor wounds you!# +6812 Your shattered armor heavily wounds you!# +6813 Your mana could not return your %s!# +6814 You are unable to create a passage through this wall.# +6815 The %s comes to life!# +6816 dresser# +6817 drawer# +6818 candelabra# + +6826 You are blessed with peace!# +6827 You are blessed with justice!# +6828 You are blessed with providence!# +6829 You are blessed with purity!# +6830 You are blessed with sanctuary!# +6831 Unlearn# +6832 Sacrifice a spell to recharge:# +6833 Recharge# +6834 Recharge# +6835 Confirm Unlearn (%d)# +6836 You forget the %s spell.# +6837 This %s + cannot be used to recharge.# +6838 Your %s + is already fully charged!# +6839 Your %s is depleted!# +6840 The icon holds your enemies in stasis!# +6841 The icon roots your enemies in place!# +6842 Filtering Spells: +%s# +6843 Invoke# +6844 Play# +6845 You recall your previous light.# +6846 Player Movement Deadzone# +6847 Affect the analog deadzone of the control stick used for player movement.# +6848 Camera Turn Deadzone# +6849 Affect the analog deadzone of the control stick used for turning the player camera.# +6850 tablet of %s# +6851 tablets of %s# +6852 The %s deteriorates and crumbles to ash!# +6853 didn't survive the impact.# +6854 Velocity# +6855 Not enough resources# +6856 Your spell can not absorb any more magic!# +6857 Your spell absorbs the incoming hit!# +6858 Your absorbed magic increased your damage!# +6859 Your flame cloak falls to ashes.# +6860 Your flame cloak is roaring with flames!# +6861 The scroll erupts in a cloak of flame!# +6862 You are protected by a magical bubble!# +6863 Your magical bubble fades.# +6864 The guard over your body fades.# +6865 The guard over your spirit fades.# +6866 Your divine guard fades.# +6867 Your amulet glows a faint red.# +6868 You have nothing else to learn by trading with this shopkeep.# +6869 The duck slips out from your pack!# +6870 Instrument Background Tracks# +6871 Enable instrument music tracks to continue while their effects are active but not actively being played.# +6872 Instrument Foreground Tracks# +6873 Instrument Foreground Tracks description# +6874 You project your spirit away!# +6875 Your spirit rejoins your body.# +6876 Your spirit projection begins to fade...# +6877 Profile# +6878 Quack# +6879 Return# +6880 Hotbar Keys Update Selection# +6881 Allow the highlighted hotbar slot to change when using the individual slot keys to activate the hotbar.# +6882 The rot invigorates you!# +6883 You shed your burning foliage!# +6884 You sprout forth a shrub!# +6885 You spore forth a mushroom!# +6886 Your vandalism energizes you!# +6887 Select transmutation result:# +6888 Confirm# +6889 Deselect# +6890 %s grows in strength!# +6891 The %s cannot grow any stronger!# +6892 %s cannot grow any stronger!# +6893 %s shrivels away!# +6894 The %s cannot shrivel down any further!# +6895 %s cannot shrivel down any further!# +6896 You feel enlarged!# +6897 You feel diminished!# +6898 Your stature feels ordinary once more.# +6899 You were unable to scry any other allies.# +6900 %s loses their courage!# +6901 %s is encouraged!# +6902 %s is compelled to your command!# +6903 %s becomes a scapegoat!# +6904 You curse the flesh of %s!# +6905 The %s's vitality is defied!# +6906 %s's vitality is defied!# +6907 Your vitality is defied!# +6908 Your spell has no effect right now!# +6909 %s's spell power is diminished!# +6910 The %s's spell power is diminished!# +6911 Your spell power is diminished!# +6912 You suddenly feel rather tall!# +6913 You suddenly feel rather small!# +6914 You suddenly detect a change of appearance!# +6915 Cauldron# +6916 Your dousing flames increase your fervor!# +6917 Your impervious armor fades.# +6918 You are blazing with radiance!# +6919 Your scales constrict into impervious stone!# +6920 Your temperance returns to normal.# +6921 Your radiance reaches a more stable level.# +6922 You are unable to charge your symbol any further!# +6923 You are unable to charge your icon any further!# +6924 Your foliage flourishes with nutrition!# +6925 Your foliage grows!# +6926 Your prayer on %s fades.# +6927 Your prayer on the %s fades.# +6928 The %s receives a prayer for healing!# +6929 %s receives a prayer for healing!# +6930 You pray for healing!# +6931 Your prayer fades.# +6932 You receive a healing prayer!# +6933 Your prayer was interrupted!# +6934 The blood ward cures your poisoning!# +6935 The blood ward cures your bleeding!# +6936 You are unable to scry any allies.# +6937 You are unable to scry any traps nearby.# +6938 You are unable to scry any other traps nearby.# +6939 You are unable to scry any treasures nearby.# +6940 You are unable to scry any other treasures nearby.# +6941 You were not deemed worthy of an additional donation.# +6942 Your call for divine providence falls unanswered.# +6943 You discover a gift of divine providence!# +6944 Your ally %s discovers a gift of divine providence!# +6945 %s's ally %s discovers a gift of divine providence!# +6946 You are unable to detect any more monsters nearby.# +6947 You are unable to detect any monsters nearby.# +6948 You are unable to pinpoint any more monsters nearby.# +6949 You are unable to pinpoint any monsters nearby.# +6950 The %s gets smited with divine precision!# +6951 %s gets smited with divine precision!# +6952 You cover the area with a divine sigil!# +6953 You cover the area with divine sanctuary!# +6954 The %s is purchased into your service!# +6955 %s is purchased into your service!# +6956 (%02d:%02d%) Appraising...# +6957 (%02d:%02d%) %3.0f%% Appraising...# +6958 You sabotage the construction of the %s!# +6959 You sabotage the construction of %s!# +6960 Your skeleton key is damaged.# +6961 Your skeleton key is broken.# +6962 Your current power is unable to dominate any more creatures.# +6963 The scarab inflicts a curse of weakness!# +6964 The scarab inflicts a greater curse of weakness!# +6965 N/A# +6966 Your influence is not high enough to compel the %s!# +6967 Your influence is not high enough to compel %s!# +6968 The %s refuses to be compelled further!# +6969 %s refuses to be compelled further!# +6970 was unable to drop some of their equipment.# +6971 Your magical bubble enhances!# +6972 Your magical bubble has reached the limit of your powers.# +6973 Some of your gold has vanished!# +6974 cauldron# +6975 The cauldron is currently in use by someone else.# +6976 You see a cauldron.# +6977 Use cauldron# +6978 Workbench# +6979 Use workbench# +6980 You see a workbench.# +6981 workbench# +6982 The workbench is currently in use by someone else.# +6983 Mailbox# +6984 Use mailbox# +6985 You see a mailbox.# +6986 mailbox# +6987 The mailbox is currently in use by someone else.# +6988 Claim# +6989 Deserters and Disciples DLC +has been unlocked!# +6990 You are blinded by a flying slop ball! + *SPLAT*# +6991 Yum! Rotten food!# +6992 Your unidentified items are further appraised!# + +6999 END# diff --git a/lang/item_names.json b/lang/item_names.json index 119cf9060..19b4320e1 100644 --- a/lang/item_names.json +++ b/lang/item_names.json @@ -1230,7 +1230,7 @@ "name_unidentified": "bandana" }, "hat_circlet": { - "name_identified": "circlet", + "name_identified": "circlet of mysticism", "name_unidentified": "circlet" }, "hat_crown": { @@ -1328,7 +1328,775 @@ "mask_marigold": { "name_identified": "marigold", "name_unidentified": "marigold" - } + }, + "key_stone": { + "name_identified": "stone key", + "name_unidentified": "stone key" + }, + "key_bone": { + "name_identified": "spirit key", + "name_unidentified": "spirit key" + }, + "key_bronze": { + "name_identified": "bronze key", + "name_unidentified": "bronze key" + }, + "key_iron": { + "name_identified": "iron key", + "name_unidentified": "iron key" + }, + "key_silver": { + "name_identified": "silver key", + "name_unidentified": "silver key" + }, + "key_gold": { + "name_identified": "gold key", + "name_unidentified": "gold key" + }, + "key_crystal": { + "name_identified": "crystal key", + "name_unidentified": "crystal key" + }, + "key_machine": { + "name_identified": "machine key", + "name_unidentified": "machine key" + }, + "tool_foci_fire": { + "name_identified": "focus of flames", + "name_unidentified": "focus" + }, + "instrument_flute": { + "name_identified": "flute", + "name_unidentified": "flute" + }, + "instrument_lyre": { + "name_identified": "lyre", + "name_unidentified": "lyre" + }, + "instrument_drum": { + "name_identified": "drum", + "name_unidentified": "drum" + }, + "instrument_lute": { + "name_identified": "lute", + "name_unidentified": "lute" + }, + "instrument_horn": { + "name_identified": "horn", + "name_unidentified": "horn" + }, + "rapier": { + "name_identified": "rapier", + "name_unidentified": "rapier" + }, + "amulet_burningresist": { + "name_identified": "amulet of burning resistance", + "name_unidentified": "amulet" + }, + "grease_ball": { + "name_identified": "grease ball", + "name_unidentified": "grease ball" + }, + "branch_staff": { + "name_identified": "branch staff", + "name_unidentified": "branch staff" + }, + "branch_bow": { + "name_identified": "branch bow", + "name_unidentified": "branch bow" + }, + "branch_bow_infected": { + "name_identified": "infected branch bow", + "name_unidentified": "infected branch bow" + }, + "dust_ball": { + "name_identified": "dust ball", + "name_unidentified": "dust ball" + }, + "bolas": { + "name_identified": "bolas", + "name_unidentified": "bolas" + }, + "steel_flail": { + "name_identified": "steel flail", + "name_unidentified": "steel flail" + }, + "food_ration": { + "name_identified": "ration", + "name_unidentified": "ration" + }, + "food_ration_spicy": { + "name_identified": "spicy ration", + "name_unidentified": "ration" + }, + "food_ration_sour": { + "name_identified": "sour ration", + "name_unidentified": "ration" + }, + "food_ration_bitter": { + "name_identified": "bitter ration", + "name_unidentified": "ration" + }, + "food_ration_hearty": { + "name_identified": "hearty ration", + "name_unidentified": "ration" + }, + "food_ration_herbal": { + "name_identified": "herbal ration", + "name_unidentified": "ration" + }, + "food_ration_sweet": { + "name_identified": "sweet ration", + "name_unidentified": "ration" + }, + "slop_ball": { + "name_identified": "slop ball", + "name_unidentified": "slop ball" + }, + "tool_frying_pan": { + "name_identified": "frypan", + "name_unidentified": "frypan" + }, + "cleat_boots": { + "name_identified": "cleats", + "name_unidentified": "cleats" + }, + "bandit_breastpiece": { + "name_identified": "bandit leather", + "name_unidentified": "bandit leather" + }, + "tunic_blouse": { + "name_identified": "blouse", + "name_unidentified": "blouse" + }, + "bone_breastpiece": { + "name_identified": "bone breastpiece", + "name_unidentified": "bone breastpiece" + }, + "blackiron_breastpiece": { + "name_identified": "blackiron breastpiece", + "name_unidentified": "blackiron breastpiece" + }, + "silver_breastpiece": { + "name_identified": "silver breastpiece", + "name_unidentified": "silver breastpiece" + }, + "iron_pauldrons": { + "name_identified": "iron pauldrons", + "name_unidentified": "iron pauldrons" + }, + "quilted_gambeson": { + "name_identified": "quilted gambeson", + "name_unidentified": "quilted gambeson" + }, + "robe_cultist": { + "name_identified": "cultist robe", + "name_unidentified": "cultist robe" + }, + "robe_healer": { + "name_identified": "healer robe", + "name_unidentified": "healer robe" + }, + "robe_monk": { + "name_identified": "monk robe", + "name_unidentified": "monk robe" + }, + "robe_wizard": { + "name_identified": "wizard robe", + "name_unidentified": "wizard robe" + }, + "shawl": { + "name_identified": "shawl", + "name_unidentified": "shawl" + }, + "chain_hauberk": { + "name_identified": "chain hauberk", + "name_unidentified": "chain hauberk" + }, + "bone_bracers": { + "name_identified": "bone bracers", + "name_unidentified": "bone bracers" + }, + "blackiron_gauntlets": { + "name_identified": "blackiron gauntlets", + "name_unidentified": "blackiron gauntlets" + }, + "silver_gauntlets": { + "name_identified": "silver gauntlets", + "name_unidentified": "silver gauntlets" + }, + "quilted_gloves": { + "name_identified": "quilted gloves", + "name_unidentified": "quilted gloves" + }, + "chain_gloves": { + "name_identified": "chain gloves", + "name_unidentified": "chain gloves" + }, + "bone_boots": { + "name_identified": "bone boots", + "name_unidentified": "bone boots" + }, + "blackiron_boots": { + "name_identified": "blackiron boots", + "name_unidentified": "blackiron boots" + }, + "silver_boots": { + "name_identified": "silver boots", + "name_unidentified": "silver boots" + }, + "quilted_boots": { + "name_identified": "quilted boots", + "name_unidentified": "quilted boots" + }, + "loafers": { + "name_identified": "loafers", + "name_unidentified": "loafers" + }, + "chain_boots": { + "name_identified": "chain boots", + "name_unidentified": "chain boots" + }, + "scutum": { + "name_identified": "scutum", + "name_unidentified": "scutum" + }, + "bone_shield": { + "name_identified": "bone shield", + "name_unidentified": "bone shield" + }, + "blackiron_shield": { + "name_identified": "blackiron shield", + "name_unidentified": "blackiron shield" + }, + "silver_shield": { + "name_identified": "silver shield", + "name_unidentified": "silver shield" + }, + "cloak_dendrite": { + "name_identified": "dendrite cloak", + "name_unidentified": "dendrite cloak" + }, + "bone_helm": { + "name_identified": "bone helm", + "name_unidentified": "bone helm" + }, + "blackiron_helm": { + "name_identified": "blackiron helm", + "name_unidentified": "blackiron helm" + }, + "silver_helm": { + "name_identified": "silver helm", + "name_unidentified": "silver helm" + }, + "hat_felt": { + "name_identified": "felt hat", + "name_unidentified": "felt hat" + }, + "quilted_cap": { + "name_identified": "quilted cap", + "name_unidentified": "quilted cap" + }, + "hood_teal": { + "name_identified": "teal hood", + "name_unidentified": "teal hood" + }, + "chain_coif": { + "name_identified": "chain coif", + "name_unidentified": "chain coif" + }, + "food_shroom": { + "name_identified": "mushroom slice", + "name_unidentified": "mushroom slice" + }, + "food_nut": { + "name_identified": "nut", + "name_unidentified": "nut" + }, + "tool_foci_snow": { + "name_identified": "focus of snow", + "name_unidentified": "focus" + }, + "tool_foci_needles": { + "name_identified": "focus of needles", + "name_unidentified": "focus" + }, + "tool_foci_arcs": { + "name_identified": "focus of arcs", + "name_unidentified": "focus" + }, + "tool_foci_sand": { + "name_identified": "focus of sandblast", + "name_unidentified": "focus" + }, + "tool_foci_dark_life": { + "name_identified": "icon of claim life", + "name_unidentified": "icon" + }, + "tool_foci_dark_rift": { + "name_identified": "icon of void rift", + "name_unidentified": "icon" + }, + "tool_foci_dark_silence": { + "name_identified": "icon of silence", + "name_unidentified": "icon" + }, + "tool_foci_dark_vengeance": { + "name_identified": "icon of vengeance", + "name_unidentified": "icon" + }, + "tool_foci_dark_suppress": { + "name_identified": "icon of suppression", + "name_unidentified": "icon" + }, + "tool_foci_light_peace": { + "name_identified": "symbol of peace", + "name_unidentified": "symbol" + }, + "tool_foci_light_justice": { + "name_identified": "symbol of justice", + "name_unidentified": "symbol" + }, + "tool_foci_light_providence": { + "name_identified": "symbol of providence", + "name_unidentified": "symbol" + }, + "tool_foci_light_purity": { + "name_identified": "symbol of purity", + "name_unidentified": "symbol" + }, + "tool_foci_light_sanctuary": { + "name_identified": "symbol of sanctuary", + "name_unidentified": "symbol" + }, + "magicstaff_scepter": { + "name_identified": "archon scepter", + "name_unidentified": "scepter" + }, + "tome_sorcery": { + "name_identified": "tablet", + "name_unidentified": "tablet" + }, + "tome_mysticism": { + "name_identified": "tablet", + "name_unidentified": "tablet" + }, + "tome_thaumaturgy": { + "name_identified": "tablet", + "name_unidentified": "tablet" + }, + "hat_circlet_sorcery": { + "name_identified": "circlet of sorcery", + "name_unidentified": "circlet" + }, + "hat_circlet_thaumaturgy": { + "name_identified": "circlet of thaumaturgy", + "name_unidentified": "circlet" + }, + "tool_duck": { + "name_identified": "duck", + "name_unidentified": "duck" + }, + "shillelagh_mace": { + "name_identified": "shillelagh", + "name_unidentified": "shillelagh" + }, + "claymore_sword": { + "name_identified": "claymore", + "name_unidentified": "claymore" + }, + "anelace_sword": { + "name_identified": "anelace", + "name_unidentified": "anelace" + }, + "lance_spear": { + "name_identified": "lance", + "name_unidentified": "lance" + }, + "steel_falshion": { + "name_identified": "steel falshion", + "name_unidentified": "steel falshion" + }, + "steel_greataxe": { + "name_identified": "steel greataxe", + "name_unidentified": "steel greataxe" + }, + "blackiron_axe": { + "name_identified": "blackiron axe", + "name_unidentified": "blackiron axe" + }, + "blackiron_crossbow": { + "name_identified": "blackiron crossbow", + "name_unidentified": "blackiron crossbow" + }, + "blackiron_dart": { + "name_identified": "blackiron dart", + "name_unidentified": "blackiron dart" + }, + "blackiron_mace": { + "name_identified": "blackiron mace", + "name_unidentified": "blackiron mace" + }, + "blackiron_sword": { + "name_identified": "blackiron sword", + "name_unidentified": "blackiron sword" + }, + "blackiron_trident": { + "name_identified": "blackiron trident", + "name_unidentified": "blackiron trident" + }, + "bone_axe": { + "name_identified": "bone axe", + "name_unidentified": "bone axe" + }, + "bone_mace": { + "name_identified": "bone mace", + "name_unidentified": "bone mace" + }, + "bone_shortbow": { + "name_identified": "bone shortbow", + "name_unidentified": "bone shortbow" + }, + "bone_spear": { + "name_identified": "bone spear", + "name_unidentified": "bone spear" + }, + "bone_sword": { + "name_identified": "bone sword", + "name_unidentified": "bone sword" + }, + "bone_throwing": { + "name_identified": "bone", + "name_unidentified": "bone" + }, + "silver_axe": { + "name_identified": "silver axe", + "name_unidentified": "silver axe" + }, + "silver_glaive": { + "name_identified": "silver glaive", + "name_unidentified": "silver glaive" + }, + "silver_mace": { + "name_identified": "silver mace", + "name_unidentified": "silver mace" + }, + "silver_plumbata": { + "name_identified": "silver plumbata", + "name_unidentified": "silver plumbata" + }, + "silver_sword": { + "name_identified": "silver sword", + "name_unidentified": "silver sword" + }, + "quiver_bone": { + "name_identified": "quiver of bone ammo", + "name_unidentified": "quiver" + }, + "quiver_blackiron": { + "name_identified": "quiver of blackiron ammo", + "name_unidentified": "quiver" + }, + "gem_jewel": { + "name_identified": "jewel", + "name_unidentified": "jewel" + }, + "spellbook_meteor": { + "name_identified": "spellbook of meteor", + "name_unidentified": "spellbook" + }, + "spellbook_ice_wave": { + "name_identified": "spellbook of ice wave", + "name_unidentified": "spellbook" + }, + "spellbook_guard_body": { + "name_identified": "spellbook of guard body", + "name_unidentified": "spellbook" + }, + "spellbook_guard_spirit": { + "name_identified": "spellbook of guard spirit", + "name_unidentified": "spellbook" + }, + "spellbook_divine_guard": { + "name_identified": "spellbook of divine guard", + "name_unidentified": "spellbook" + }, + "spellbook_prof_nimbleness": { + "name_identified": "spellbook of nimbleness", + "name_unidentified": "spellbook" + }, + "spellbook_prof_greater_might": { + "name_identified": "spellbook of greater might", + "name_unidentified": "spellbook" + }, + "spellbook_prof_counsel": { + "name_identified": "spellbook of counsel", + "name_unidentified": "spellbook" + }, + "spellbook_prof_sturdiness": { + "name_identified": "spellbook of sturdiness", + "name_unidentified": "spellbook" + }, + "spellbook_bless_food": { + "name_identified": "spellbook of blessed meals", + "name_unidentified": "spellbook" + }, + "spellbook_pinpoint": { + "name_identified": "spellbook of pinpoint", + "name_unidentified": "spellbook" + }, + "spellbook_donation": { + "name_identified": "spellbook of donation", + "name_unidentified": "spellbook" + }, + "spellbook_scry_allies": { + "name_identified": "spellbook of scry allies", + "name_unidentified": "spellbook" + }, + "spellbook_scry_traps": { + "name_identified": "spellbook of scry traps", + "name_unidentified": "spellbook" + }, + "spellbook_scry_treasures": { + "name_identified": "spellbook of scry treasures", + "name_unidentified": "spellbook" + }, + "spellbook_detect_enemy": { + "name_identified": "spellbook of detect enemy", + "name_unidentified": "spellbook" + }, + "spellbook_turn_undead": { + "name_identified": "spellbook of exorcise", + "name_unidentified": "spellbook" + }, + "spellbook_heal_other": { + "name_identified": "spellbook of minor heal other", + "name_unidentified": "spellbook" + }, + "spellbook_blood_ward": { + "name_identified": "spellbook of blood ward", + "name_unidentified": "spellbook" + }, + "spellbook_divine_zeal": { + "name_identified": "spellbook of divine zeal", + "name_unidentified": "spellbook" + }, + "spellbook_maximise": { + "name_identified": "spellbook of maximise", + "name_unidentified": "spellbook" + }, + "spellbook_minimise": { + "name_identified": "spellbook of minimise", + "name_unidentified": "spellbook" + }, + "spellbook_incoherence": { + "name_identified": "spellbook of incoherence", + "name_unidentified": "spellbook" + }, + "spellbook_overcharge": { + "name_identified": "spellbook of overcharge", + "name_unidentified": "spellbook" + }, + "spellbook_envenom_weapon": { + "name_identified": "spellbook of envenom weapon", + "name_unidentified": "spellbook" + }, + "spellbook_psychic_spear": { + "name_identified": "spellbook of psychic spear", + "name_unidentified": "spellbook" + }, + "spellbook_defy_flesh": { + "name_identified": "spellbook of defy flesh", + "name_unidentified": "spellbook" + }, + "spellbook_grease_spray": { + "name_identified": "spellbook of grease spray", + "name_unidentified": "spellbook" + }, + "spellbook_blood_waves": { + "name_identified": "spellbook of blood waves", + "name_unidentified": "spellbook" + }, + "spellbook_command": { + "name_identified": "spellbook of compel", + "name_unidentified": "spellbook" + }, + "spellbook_metallurgy": { + "name_identified": "spellbook of metallurgy", + "name_unidentified": "spellbook" + }, + "spellbook_forge_key": { + "name_identified": "spellbook of forge key", + "name_unidentified": "spellbook" + }, + "spellbook_reshape_weapon": { + "name_identified": "spellbook of reshape weapon", + "name_unidentified": "spellbook" + }, + "spellbook_alter_arrow": { + "name_identified": "spellbook of alter arrow", + "name_unidentified": "spellbook" + }, + "spellbook_void_chest": { + "name_identified": "spellbook of void chest", + "name_unidentified": "spellbook" + }, + "spellbook_lead_bolt": { + "name_identified": "spellbook of lead bolt", + "name_unidentified": "spellbook" + }, + "spellbook_numbing_bolt": { + "name_identified": "spellbook of numbing bolt", + "name_unidentified": "spellbook" + }, + "spellbook_curse_flesh": { + "name_identified": "spellbook of curse flesh", + "name_unidentified": "spellbook" + }, + "spellbook_cowardice": { + "name_identified": "spellbook of cowardice", + "name_unidentified": "spellbook" + }, + "spellbook_seek_ally": { + "name_identified": "spellbook of seek ally", + "name_unidentified": "spellbook" + }, + "spellbook_deep_shade": { + "name_identified": "spellbook of deep shade", + "name_unidentified": "spellbook" + }, + "spellbook_spirit_weapon": { + "name_identified": "spellbook of spirit weapon", + "name_unidentified": "spellbook" + }, + "spellbook_spores": { + "name_identified": "spellbook of spores", + "name_unidentified": "spellbook" + }, + "spellbook_windgate": { + "name_identified": "spellbook of windgate", + "name_unidentified": "spellbook" + }, + "spellbook_telekinesis": { + "name_identified": "spellbook of telekinesis", + "name_unidentified": "spellbook" + }, + "spellbook_disarm": { + "name_identified": "spellbook of disarm", + "name_unidentified": "spellbook" + }, + "spellbook_abundance": { + "name_identified": "spellbook of abundance", + "name_unidentified": "spellbook" + }, + "spellbook_preserve": { + "name_identified": "spellbook of preserve", + "name_unidentified": "spellbook" + }, + "spellbook_sabotage": { + "name_identified": "spellbook of sabotage", + "name_unidentified": "spellbook" + }, + "spellbook_mist_form": { + "name_identified": "spellbook of mist form", + "name_unidentified": "spellbook" + }, + "spellbook_force_shield": { + "name_identified": "spellbook of kinetic defense", + "name_unidentified": "spellbook" + }, + "spellbook_splinter_gear": { + "name_identified": "spellbook of splinter armor", + "name_unidentified": "spellbook" + }, + "spellbook_attract_items": { + "name_identified": "spellbook of attract items", + "name_unidentified": "spellbook" + }, + "spellbook_absorb_magic": { + "name_identified": "spellbook of absorb magic", + "name_unidentified": "spellbook" + }, + "spellbook_tunnel": { + "name_identified": "spellbook of portal", + "name_unidentified": "spellbook" + }, + "spellbook_null_area": { + "name_identified": "spellbook of null area", + "name_unidentified": "spellbook" + }, + "spellbook_fire_sprite": { + "name_identified": "spellbook of summon fire sprite", + "name_unidentified": "spellbook" + }, + "spellbook_spin": { + "name_identified": "spellbook of spin", + "name_unidentified": "spellbook" + }, + "spellbook_cleanse_food": { + "name_identified": "spellbook of cleanse food", + "name_unidentified": "spellbook" + }, + "spellbook_flame_cloak": { + "name_identified": "spellbook of flame cloak", + "name_unidentified": "spellbook" + }, + "spellbook_lightning_bolt": { + "name_identified": "spellbook of lightning bolt", + "name_unidentified": "spellbook" + }, + "spellbook_disrupt_earth": { + "name_identified": "spellbook of disrupt earth", + "name_unidentified": "spellbook" + }, + "spellbook_fire_wall": { + "name_identified": "spellbook of fire wall", + "name_unidentified": "spellbook" + }, + "spellbook_slam": { + "name_identified": "spellbook of lift", + "name_unidentified": "spellbook" + }, + "spellbook_ignite": { + "name_identified": "spellbook of ignite", + "name_unidentified": "spellbook" + }, + "spellbook_shatter_objects": { + "name_identified": "spellbook of shatter objects", + "name_unidentified": "spellbook" + }, + "spellbook_kinetic_field": { + "name_identified": "spellbook of kinetic field", + "name_unidentified": "spellbook" + }, + "spellbook_thorns": { + "name_identified": "spellbook of thorns", + "name_unidentified": "spellbook" + }, + "spellbook_magicians_armor": { + "name_identified": "spellbook of magicians' bubble", + "name_unidentified": "spellbook" + }, + "spellbook_heal_minor": { + "name_identified": "spellbook of minor heal", + "name_unidentified": "spellbook" + }, + "spellbook_sigil": { + "name_identified": "spellbook of holy sigil", + "name_unidentified": "spellbook" + }, + "spellbook_sanctuary": { + "name_identified": "spellbook of divine fortress", + "name_unidentified": "spellbook" + }, + "spellbook_holy_beam": { + "name_identified": "spellbook of holy beam", + "name_unidentified": "spellbook" + }, + "spellbook_dominate": { + "name_identified": "spellbook of dominate", + "name_unidentified": "spellbook" + } }, "spell_names": { "spell_forcebolt": { @@ -1510,6 +2278,462 @@ }, "spell_slime_metal": { "name": "Slime Spray (Metal)" + }, + "spell_foci_fire": { + "name": "Focus of Flames" + }, + "spell_foci_snow": { + "name": "Focus of Snow" + }, + "spell_foci_needles": { + "name": "Focus of Needles" + }, + "spell_foci_arcs": { + "name": "Focus of Arcs" + }, + "spell_foci_sandblast": { + "name": "Focus of Sandblast" + }, + "spell_meteor": { + "name": "Meteor" + }, + "spell_flames": { + "name": "Flames" + }, + "spell_ice_wave": { + "name": "Ice Wave" + }, + "spell_earth_elemental": { + "name": "Summon Earth Sprite" + }, + "spell_bless_food": { + "name": "Blessed Meals" + }, + "spell_shrub": { + "name": "Germinate" + }, + "spell_conjure_food": { + "name": "Conjure Food" + }, + "spell_guard_body": { + "name": "Guard Body" + }, + "spell_guard_spirit": { + "name": "Guard Spirit" + }, + "spell_divine_guard": { + "name": "Divine Guard" + }, + "spell_prof_nimbleness": { + "name": "Nimbleness" + }, + "spell_prof_greater_might": { + "name": "Greater Might" + }, + "spell_prof_counsel": { + "name": "Counsel" + }, + "spell_prof_sturdiness": { + "name": "Sturdiness" + }, + "spell_pinpoint": { + "name": "Pinpoint" + }, + "spell_donation": { + "name": "Donation" + }, + "spell_scry_allies": { + "name": "Scry Allies" + }, + "spell_scry_shrines": { + "name": "Scry Shrines" + }, + "spell_scry_traps": { + "name": "Scry Traps" + }, + "spell_scry_treasures": { + "name": "Scry Treasures" + }, + "spell_penance": { + "name": "Penance" + }, + "spell_call_allies": { + "name": "Call Allies" + }, + "spell_sacred_path": { + "name": "Sacred Path" + }, + "spell_manifest_destiny": { + "name": "Manifest Destiny" + }, + "spell_detect_enemy": { + "name": "Detect Enemy" + }, + "spell_detect_enemies": { + "name": "Detect Enemies" + }, + "spell_turn_undead": { + "name": "Exorcise" + }, + "spell_heal_other": { + "name": "Minor Heal Other" + }, + "spell_blood_ward": { + "name": "Blood Ward" + }, + "spell_true_blood": { + "name": "True Blood" + }, + "spell_divine_zeal": { + "name": "Divine Zeal" + }, + "spell_alter_instrument": { + "name": "Alter Instrument" + }, + "spell_maximise": { + "name": "Maximise" + }, + "spell_minimise": { + "name": "Minimise" + }, + "spell_jump": { + "name": "Jump" + }, + "spell_incoherence": { + "name": "Incoherence" + }, + "spell_overcharge": { + "name": "Overcharge" + }, + "spell_envenom_weapon": { + "name": "Envenom Weapon" + }, + "spell_psychic_spear": { + "name": "Psychic Spear" + }, + "spell_defy_flesh": { + "name": "Defy Flesh" + }, + "spell_grease_spray": { + "name": "Grease Spray" + }, + "spell_blood_waves": { + "name": "Blood Waves" + }, + "spell_booby_trap": { + "name": "Booby Trap" + }, + "spell_command": { + "name": "Compel" + }, + "spell_metallurgy": { + "name": "Metallurgy" + }, + "spell_geomancy": { + "name": "Geomancy" + }, + "spell_forge_key": { + "name": "Forge Key" + }, + "spell_forge_jewel": { + "name": "Forge Jewel" + }, + "spell_enhance_weapon": { + "name": "Enhance Weapon" + }, + "spell_reshape_weapon": { + "name": "Reshape Weapon" + }, + "spell_alter_arrow": { + "name": "Alter Arrow" + }, + "spell_void_chest": { + "name": "Void Chest" + }, + "spell_puncture_void": { + "name": "Puncture Void" + }, + "spell_lead_bolt": { + "name": "Lead Bolt" + }, + "spell_mercury_bolt": { + "name": "Mercury Bolt" + }, + "spell_numbing_bolt": { + "name": "Numbing Bolt" + }, + "spell_delay_pain": { + "name": "Delay Pain" + }, + "spell_curse_flesh": { + "name": "Curse Flesh" + }, + "spell_revenant_curse": { + "name": "Revenant Curse" + }, + "spell_cowardice": { + "name": "Cowardice" + }, + "spell_courage": { + "name": "Courage" + }, + "spell_seek_ally": { + "name": "Seek Ally" + }, + "spell_seek_foe": { + "name": "Seek Foe" + }, + "spell_deep_shade": { + "name": "Deep Shade" + }, + "spell_shade_bolt": { + "name": "Shade Bolt" + }, + "spell_spirit_weapon": { + "name": "Spirit Weapon" + }, + "spell_adorcism": { + "name": "Adorcism" + }, + "spell_taboo": { + "name": "Scapegoat" + }, + "spell_wonderlight": { + "name": "Wonderlight" + }, + "spell_spores": { + "name": "Spores" + }, + "spell_spore_bomb": { + "name": "Spore Bomb" + }, + "spell_windgate": { + "name": "Windgate" + }, + "spell_vortex": { + "name": "Vortex" + }, + "spell_telekinesis": { + "name": "Telekinesis" + }, + "spell_kinetic_push": { + "name": "Kinetic Push" + }, + "spell_disarm": { + "name": "Disarm" + }, + "spell_strip": { + "name": "Strip" + }, + "spell_abundance": { + "name": "Abundance" + }, + "spell_greater_abundance": { + "name": "Greater Abundance" + }, + "spell_preserve": { + "name": "Preserve" + }, + "spell_restore": { + "name": "Restore" + }, + "spell_sabotage": { + "name": "Sabotage" + }, + "spell_harvest_trap": { + "name": "Harvest Trap" + }, + "spell_mist_form": { + "name": "Mist Form" + }, + "spell_hologram": { + "name": "Hologram" + }, + "spell_force_shield": { + "name": "Kinetic Defense" + }, + "spell_reflector": { + "name": "Reflector" + }, + "spell_splinter_gear": { + "name": "Splinter Armor" + }, + "spell_lighten_load": { + "name": "Lighten Load" + }, + "spell_attract_items": { + "name": "Attract Items" + }, + "spell_return_items": { + "name": "Return Items" + }, + "spell_absorb_magic": { + "name": "Absorb Magic" + }, + "spell_seize_magic": { + "name": "Seize Magic" + }, + "spell_deface": { + "name": "Deface" + }, + "spell_sunder_monument": { + "name": "Sunder Monument" + }, + "spell_demesne_door": { + "name": "Demesne Door" + }, + "spell_tunnel": { + "name": "Portal" + }, + "spell_null_area": { + "name": "Null Area" + }, + "spell_sphere_silence": { + "name": "Sphere of Silence" + }, + "spell_forge_metal_scrap": { + "name": "Forge Metal" + }, + "spell_forge_magic_scrap": { + "name": "Forge Magic" + }, + "spell_fire_sprite": { + "name": "Summon Fire Sprite" + }, + "spell_flame_elemental": { + "name": "Flame Elemental" + }, + "spell_spin": { + "name": "Spin" + }, + "spell_dizzy": { + "name": "Dizzy" + }, + "spell_vandalise": { + "name": "Vandalise" + }, + "spell_desecrate": { + "name": "Desecrate" + }, + "spell_sanctify": { + "name": "Sanctify" + }, + "spell_sanctify_water": { + "name": "Sanctify Water" + }, + "spell_cleanse_food": { + "name": "Cleanse Food" + }, + "spell_adorcise_instrument": { + "name": "Adorcise Instrument" + }, + "spell_flame_cloak": { + "name": "Flame Cloak" + }, + "spell_critical_spell": { + "name": "Critical Spell" + }, + "spell_magic_well": { + "name": "Magic Well" + }, + "spell_flame_shield": { + "name": "Flame Shield" + }, + "spell_lightning_bolt": { + "name": "Lightning Bolt" + }, + "spell_disrupt_earth": { + "name": "Disrupt Earth" + }, + "spell_earth_spines": { + "name": "Earth Spines" + }, + "spell_lightning_nexus": { + "name": "Lightning Nexus" + }, + "spell_fire_wall": { + "name": "Fire Wall" + }, + "spell_lift": { + "name": "Lift" + }, + "spell_slam": { + "name": "Lift" + }, + "spell_ignite": { + "name": "Ignite" + }, + "spell_shatter_objects": { + "name": "Shatter Objects" + }, + "spell_kinetic_field": { + "name": "Kinetic Field" + }, + "spell_ice_block": { + "name": "Ice Block" + }, + "spell_meteor_shower": { + "name": "Meteor Shower" + }, + "spell_chronomic_field": { + "name": "Chronomic Field" + }, + "spell_eternals_gaze": { + "name": "Eternals' Gaze" + }, + "spell_shatter_earth": { + "name": "Shatter Earth" + }, + "spell_roots": { + "name": "Roots" + }, + "spell_mushroom": { + "name": "Germinate" + }, + "spell_mycelium": { + "name": "Mycelium" + }, + "spell_mycelium_spores": { + "name": "Mycelium Spores" + }, + "spell_heal_pulse": { + "name": "Heal Pulse" + }, + "spell_thorns": { + "name": "Thorns" + }, + "spell_bladevines": { + "name": "Bladevines" + }, + "spell_bastion_mushroom": { + "name": "Bastion" + }, + "spell_bastion_roots": { + "name": "Bastion" + }, + "spell_magicians_armor": { + "name": "Magicians' Bubble" + }, + "spell_project_spirit": { + "name": "Project Spirit" + }, + "spell_breathe_fire": { + "name": "Breathe Fire" + }, + "spell_heal_minor": { + "name": "Minor Heal" + }, + "spell_holy_fire": { + "name": "Holy Fire" + }, + "spell_sigil": { + "name": "Holy Sigil" + }, + "spell_sanctuary": { + "name": "Divine Fortress" + }, + "spell_holy_beam": { + "name": "Holy Beam" } } } \ No newline at end of file diff --git a/playernames-female.txt b/playernames-female.txt index cbe9c407a..58fe11e1f 100644 --- a/playernames-female.txt +++ b/playernames-female.txt @@ -362,4 +362,5 @@ Suraya Coventia Asgorath Lara Cake Feebie Catsong -Elaine Signfield \ No newline at end of file +Elaine Signfield +Nomad Wagon \ No newline at end of file diff --git a/playernames-male.txt b/playernames-male.txt index 18a777cb8..c8bb80fb9 100644 --- a/playernames-male.txt +++ b/playernames-male.txt @@ -367,4 +367,5 @@ Telpemir Scumbo Bunger Trigger Lord Jeraldo -Toniel Deeno \ No newline at end of file +Toniel Deeno +Nomad Wagon \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c8e3e3c91..67ee22168 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -106,6 +106,13 @@ list(APPEND GAME_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/lobbies.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/shader.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/playfab.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/monster_d.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/monster_m.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/monster_s.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/monster_g.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/monster_summons.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/monster_moth.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/monster_duck.cpp" ) list(APPEND EDITOR_SOURCES diff --git a/src/actarrow.cpp b/src/actarrow.cpp index 3d2f691bb..294edfe3c 100644 --- a/src/actarrow.cpp +++ b/src/actarrow.cpp @@ -58,12 +58,15 @@ enum ArrowSpriteTypes : int PROJECTILE_FIRE_SPRITE, PROJECTILE_HEAVY_SPRITE, PROJECTILE_CRYSTAL_SPRITE, - PROJECTILE_HUNTING_SPRITE + PROJECTILE_HUNTING_SPRITE, + PROJECTILE_SEED_ROOT_SPRITE = 1881, + PROJECTILE_SEED_POISON_SPRITE = 1882, + PROJECTILE_BONE_SPRITE = 2304, + PROJECTILE_BLACKIRON_SPRITE = 2305 }; void actArrow(Entity* my) { - double dist = 0.0; node_t* node = nullptr; // lifespan @@ -83,7 +86,64 @@ void actArrow(Entity* my) return; } - if ( my->arrowQuiverType == QUIVER_FIRE || my->sprite == PROJECTILE_FIRE_SPRITE ) + if ( my->sprite == PROJECTILE_SEED_ROOT_SPRITE + || my->sprite == PROJECTILE_SEED_POISON_SPRITE ) + { + if ( ARROW_LIFE > 1 ) + { + if ( ARROW_STUCK == 0 ) + { + my->light = addLight(my->x / 16, my->y / 16, "magic_green"); + if ( flickerLights ) + { + //Torches will never flicker if this setting is disabled. + ARROW_FLICKER++; + } + if ( ARROW_FLICKER > 5 ) + { + ARROW_LIGHTING = (ARROW_LIGHTING == 1) + 1; + + if ( ARROW_LIGHTING == 1 ) + { + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, "magic_green"); + } + else + { + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, "magic_green_flicker"); + } + ARROW_FLICKER = 0; + } + } + + if ( ARROW_STUCK == 0 ) + { + my->removeLightField(); + Entity* particle = spawnMagicParticleCustom(my, 1816, 0.5, 4); + if ( particle ) + { + particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); + //particle->flags[SPRITE] = true; + particle->ditheringDisabled = true; + } + } + } + } + else if ( (my->arrowQuiverType == QUIVER_PIERCE || my->sprite == PROJECTILE_PIERCE_SPRITE) ) + { + if ( ARROW_STUCK == 0 ) + { + Entity* particle = spawnMagicParticleCustom(my, 158, 0.5, 4); + if ( particle ) + { + particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); + particle->flags[SPRITE] = true; + particle->ditheringDisabled = true; + } + } + } + else if ( my->arrowQuiverType == QUIVER_FIRE || my->sprite == PROJECTILE_FIRE_SPRITE ) { if ( ARROW_LIFE > 1 ) { @@ -178,11 +238,11 @@ void actArrow(Entity* my) } } } - else if ( my->arrowQuiverType == QUIVER_PIERCE || my->sprite == PROJECTILE_PIERCE_SPRITE ) + else if ( my->arrowQuiverType == QUIVER_LIGHTWEIGHT || my->sprite == PROJECTILE_SWIFT_SPRITE ) { if ( ARROW_STUCK == 0 ) { - Entity* particle = spawnMagicParticleCustom(my, 158, 0.5, 4); + Entity* particle = spawnMagicParticleCustom(my, 156, 0.5, 4); if ( particle ) { particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); @@ -191,11 +251,11 @@ void actArrow(Entity* my) } } } - else if ( my->arrowQuiverType == QUIVER_LIGHTWEIGHT || my->sprite == PROJECTILE_SWIFT_SPRITE ) + else if ( my->arrowQuiverType == QUIVER_HUNTING || my->sprite == PROJECTILE_HUNTING_SPRITE ) { if ( ARROW_STUCK == 0 ) { - Entity* particle = spawnMagicParticleCustom(my, 156, 0.5, 4); + Entity* particle = spawnMagicParticleCustom(my, 157, 0.5, 4); if ( particle ) { particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); @@ -204,20 +264,44 @@ void actArrow(Entity* my) } } } - else if ( my->arrowQuiverType == QUIVER_HUNTING || my->sprite == PROJECTILE_HUNTING_SPRITE ) + else if ( my->arrowQuiverType == QUIVER_BLACKIRON || my->sprite == PROJECTILE_BLACKIRON_SPRITE ) { if ( ARROW_STUCK == 0 ) { - Entity* particle = spawnMagicParticleCustom(my, 157, 0.5, 4); + Entity* particle = spawnMagicParticleCustom(my, 155, 0.5, 4); if ( particle ) { particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); particle->flags[SPRITE] = true; - particle->ditheringDisabled = true; + particle->ditheringDisabled = true; + } + } + } + else if ( my->arrowQuiverType == QUIVER_BONE || my->sprite == PROJECTILE_BONE_SPRITE ) + { + if ( ARROW_STUCK == 0 ) + { + Entity* particle = spawnMagicParticleCustom(my, 155, 0.5, 4); + if ( particle ) + { + particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); + particle->flags[SPRITE] = true; + particle->ditheringDisabled = true; } } } + if ( my->arrowArmorPierce > 0 && ARROW_STUCK == 0 && !(my->arrowQuiverType == QUIVER_PIERCE || my->sprite == PROJECTILE_PIERCE_SPRITE) ) + { + Entity* particle = spawnMagicParticleCustom(my, 158, 0.5, 4); + if ( particle ) + { + particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); + particle->flags[SPRITE] = true; + particle->ditheringDisabled = true; + } + } + if ( multiplayer != CLIENT ) { Sint32 val = (1 << 31); @@ -260,7 +344,8 @@ void actArrow(Entity* my) if ( my->arrowFallSpeed > 0 ) { real_t pitchChange = 0.02; - if ( my->arrowShotByWeapon == LONGBOW ) + if ( my->arrowShotByWeapon == LONGBOW || my->arrowShotByWeapon == BRANCH_BOW + || my->arrowShotByWeapon == BRANCH_BOW_INFECTED ) { pitchChange = 0.005; } @@ -302,6 +387,7 @@ void actArrow(Entity* my) } } + bool hitSomething = false; if ( multiplayer != CLIENT ) { // horizontal motion @@ -309,7 +395,55 @@ void actArrow(Entity* my) ARROW_VELY = sin(my->yaw) * my->arrowSpeed; ARROW_OLDX = my->x; ARROW_OLDY = my->y; - dist = clipMove(&my->x, &my->y, ARROW_VELX, ARROW_VELY, my); + + my->processEntityWind(); + bool halfSpeedCheck = false; + static ConsoleVariable cvar_arrow_clip("/arrow_clip_test", true); + if ( my->arrowSpeed > 4.0 ) // can clip through thin gates + { + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( auto it : entLists ) + { + if ( !*cvar_arrow_clip && (svFlags & SV_FLAG_CHEATS) ) + { + break; + } + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actGate || entity->behavior == &actDoor || entity->behavior == &actIronDoor ) + { + if ( entityDist(my, entity) <= my->arrowSpeed ) + { + halfSpeedCheck = true; + break; + } + } + } + if ( halfSpeedCheck ) + { + break; + } + } + } + + if ( !halfSpeedCheck ) + { + real_t dist = clipMove(&my->x, &my->y, ARROW_VELX, ARROW_VELY, my); + hitSomething = dist != sqrt(ARROW_VELX * ARROW_VELX + ARROW_VELY * ARROW_VELY); + } + else + { + real_t vel_x = ARROW_VELX / 2.0; + real_t vel_y = ARROW_VELY / 2.0; + real_t dist = clipMove(&my->x, &my->y, vel_x, vel_y, my); + hitSomething = dist != sqrt(vel_x * vel_x + vel_y * vel_y); + if ( !hitSomething ) + { + dist = clipMove(&my->x, &my->y, vel_x, vel_y, my); + hitSomething = dist != sqrt(vel_x * vel_x + vel_y * vel_y); + } + } } bool arrowInGround = false; @@ -370,17 +504,32 @@ void actArrow(Entity* my) } // damage monsters - if ( arrowSpawnedInsideEntity || dist != sqrt(ARROW_VELX * ARROW_VELX + ARROW_VELY * ARROW_VELY) || arrowInGround ) + if ( arrowSpawnedInsideEntity || hitSomething || arrowInGround ) { if ( arrowInGround ) { ARROW_STUCK = 2; + serverUpdateEntitySkill(my, 0); } else { ARROW_STUCK = 1; + if ( !arrowSpawnedInsideEntity && !arrowInGround && hitSomething && hit.entity ) + { + if ( my->arrowArmorPierce > 0 && hit.entity && hit.entity->getUID() > 0 ) + { + if ( hit.entity->getStats() || hit.entity->isDamageableCollider() ) + { + ARROW_STUCK = 0; + my->collisionIgnoreTargets.insert(hit.entity->getUID()); + } + } + } + if ( ARROW_STUCK > 0 ) + { + serverUpdateEntitySkill(my, 0); + } } - serverUpdateEntitySkill(my, 0); my->x = ARROW_OLDX; my->y = ARROW_OLDY; @@ -397,6 +546,10 @@ void actArrow(Entity* my) ARROW_VELY = 0; ARROW_VELZ = 0; my->entityCheckIfTriggeredBomb(true); + if ( !hit.entity ) + { + my->entityCheckIfTriggeredWallButton(); + } if ( hit.entity != NULL ) { Entity* parent = uidToEntity(my->parent); @@ -476,12 +629,29 @@ void actArrow(Entity* my) hit.entity->colliderKillerUid = parent ? parent->getUID() : 0; if ( parent && parent->behavior == &actPlayer ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(hit.entity->getColliderOnBreakLangEntry()), - Language::get(hit.entity->getColliderLangName())); + if ( oldHP > 0 ) + { + if ( parent->getStats() && parent->getStats()->getProficiency(PRO_RANGED) < SKILL_LEVEL_BASIC + && local_rng.rand() % 20 == 0 ) + { + parent->increaseSkill(PRO_RANGED); + } + } + + if ( hit.entity->getColliderOnBreakLangEntry() != 0 ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(hit.entity->getColliderOnBreakLangEntry()), + Language::get(hit.entity->getColliderLangName())); + } if ( hit.entity->isColliderWall() ) { Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_BARRIER_DESTROYED, "breakable barriers", 1); } + + if ( hit.entity->colliderOldHP > 0 ) + { + players[parent->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_COMMON, hit.entity); + } } } @@ -496,39 +666,72 @@ void actArrow(Entity* my) if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(hit.entity) ) + if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) { - my->removeLightField(); - list_RemoveNode(my->mynode); + if ( ARROW_STUCK > 0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } return; } } + //if ( hit.entity && hitstats ) + //{ + // if ( hitstats->getEffectActive(EFF_NULL_RANGED) ) + // { + // Uint8 effectStrength = hitstats->getEffectActive(EFF_NULL_RANGED); + // int duration = hitstats->EFFECTS_TIMERS[EFF_NULL_RANGED]; + // if ( effectStrength == 1 ) + // { + // if ( hitstats->EFFECTS_TIMERS[EFF_NULL_RANGED] > 0 ) + // { + // hitstats->EFFECTS_TIMERS[EFF_NULL_RANGED] = 1; + // } + // } + // else if ( effectStrength > 1 ) + // { + // --effectStrength; + // hitstats->setEffectValueUnsafe(EFF_NULL_RANGED, effectStrength); + // hit.entity->setEffect(EFF_NULL_RANGED, effectStrength, hitstats->EFFECTS_TIMERS[EFF_NULL_RANGED], false); + // } + // if ( (parent && parent->behavior == &actPlayer) || (parent && parent->behavior == &actMonster && parent->monsterAllyGetPlayerLeader()) + // || hit.entity->behavior == &actPlayer || hit.entity->monsterAllyGetPlayerLeader() ) + // { + // spawnDamageGib(hit.entity, 0, DamageGib::DMG_GUARD, DamageGibDisplayType::DMG_GIB_GUARD, true); + // } + + // if ( hit.entity->behavior == &actPlayer ) + // { + // messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6465)); + // } + // if ( parent && parent->behavior == &actPlayer ) + // { + // messagePlayerMonsterEvent(parent->skill[2], makeColorRGB(255, 255, 255), + // *hitstats, Language::get(6468), Language::get(6469), MSG_COMBAT); // %s guards the attack + // } + // playSoundEntity(hit.entity, 166, 128); + // my->removeLightField(); + // list_RemoveNode(my->mynode); + // return; + // } + //} + bool silverDamage = false; bool huntingDamage = false; if ( my->arrowQuiverType == QUIVER_SILVER ) { - switch ( hitstats->type ) + if ( hit.entity->isSmiteWeakMonster() ) { - case SKELETON: - case CREATURE_IMP: - case GHOUL: - case DEMON: - case SUCCUBUS: - case INCUBUS: - case VAMPIRE: - case LICH: - case LICH_ICE: - case LICH_FIRE: - case DEVIL: - // smite these creatures - silverDamage = true; - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 981); - playSoundEntity(hit.entity, 249, 64); - break; - default: - silverDamage = false; - break; + // smite these creatures + silverDamage = true; + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 981); + playSoundEntity(hit.entity, 249, 64); + } + else + { + silverDamage = false; } } else if ( my->arrowQuiverType == QUIVER_HUNTING ) @@ -595,7 +798,10 @@ void actArrow(Entity* my) { Stat* parentStats = parent->getStats(); if ( parentStats->helmet && parentStats->helmet->type == HAT_HOOD_WHISPERS - && !monsterIsImmobileTurret(hit.entity, hitstats) && !(hitstats->type == MIMIC) ) + && !monsterIsImmobileTurret(hit.entity, hitstats) && !hitstats->getEffectActive(EFF_STASIS) + && !(hitstats->type == MIMIC + || hitstats->type == MINIMIMIC + || hitstats->type == MONSTER_ADORCISED_WEAPON) ) { real_t hitAngle = hit.entity->yawDifferenceFromEntity(my); if ( (hitAngle >= 0 && hitAngle <= 2 * PI / 3) ) // 120 degree arc @@ -621,16 +827,23 @@ void actArrow(Entity* my) } } - if ( hit.entity->monsterState == MONSTER_STATE_WAIT + if ( (hit.entity->monsterState == MONSTER_STATE_WAIT || hit.entity->monsterState == MONSTER_STATE_PATH - || (hit.entity->monsterState == MONSTER_STATE_HUNT && uidToEntity(hit.entity->monsterTarget) == nullptr) ) + || (hit.entity->monsterState == MONSTER_STATE_HUNT && uidToEntity(hit.entity->monsterTarget) == nullptr)) + && !hitstats->getEffectActive(EFF_ROOTED) ) { // unaware monster, get backstab damage. int bonus = (parentStats->getModifiedProficiency(PRO_STEALTH) / 20 + 2) * (2 * stealthCapstoneBonus); damage += ((bonus * equipmentModifier) * bonusModifier); - if ( local_rng.rand() % 10 == 0 && hit.entity->behavior != &actPlayer ) + if ( local_rng.rand() % 4 == 0 + && hit.entity->behavior != &actPlayer + && !(parent->behavior == &actPlayer && hit.entity->monsterAllyGetPlayerLeader()) ) { - parent->increaseSkill(PRO_STEALTH); + if ( parent->behavior == &actPlayer && players[parent->skill[2]]->mechanics.allowedRaiseStealthAgainstEntity(*hit.entity) ) + { + parent->increaseSkill(PRO_STEALTH); + players[parent->skill[2]]->mechanics.enemyRaisedStealthAgainst[hit.entity->getUID()]++; + } } backstab = true; } @@ -640,9 +853,15 @@ void actArrow(Entity* my) // 1 in 2 chance to flank defenses. int bonus = (parentStats->getModifiedProficiency(PRO_STEALTH) / 20 + 1) * (stealthCapstoneBonus); damage += ((bonus * equipmentModifier) * bonusModifier); - if ( local_rng.rand() % 20 == 0 && hit.entity->behavior != &actPlayer ) + if ( local_rng.rand() % 20 == 0 + && hit.entity->behavior != &actPlayer + && !(parent->behavior == &actPlayer && hit.entity->monsterAllyGetPlayerLeader()) ) { - parent->increaseSkill(PRO_STEALTH); + if ( parent->behavior == &actPlayer && players[parent->skill[2]]->mechanics.allowedRaiseStealthAgainstEntity(*hit.entity) ) + { + parent->increaseSkill(PRO_STEALTH); + players[parent->skill[2]]->mechanics.enemyRaisedStealthAgainst[hit.entity->getUID()]++; + } } flanking = true; } @@ -689,6 +908,28 @@ void actArrow(Entity* my) damageMultiplier = std::max(0.75, damageMultiplier); } + Entity::modifyDamageMultipliersFromEffects(hit.entity, parent, damageMultiplier, DAMAGE_TABLE_RANGED, my); + + if ( my->arrowArmorPierce > 0 && parent && parent->behavior == &actPlayer ) + { + if ( parent->getStats() ) + { + /*real_t mult = 1.0; + if ( parent->getStats()->getModifiedProficiency(PRO_RANGED) >= SKILL_LEVEL_LEGENDARY ) + { + mult = 2.0; + }*/ + if ( parent->behavior == &actMonster ) + { + damageMultiplier += std::min(25, std::max(0, statGetPER(parent->getStats(), parent))) / 100.0; + } + else + { + damageMultiplier += std::max(0, statGetPER(parent->getStats(), parent)) / 100.0; + } + } + } + if ( hitWeaklyOnTarget ) { damage = damage * (std::max(0.1, damageMultiplier - 0.5)); @@ -712,10 +953,41 @@ void actArrow(Entity* my) } } + if ( hitstats && hitstats->getEffectActive(EFF_GUARD_BODY) ) + { + thaumSpellArmorProc(hit.entity, *hitstats, false, parent, EFF_GUARD_BODY); + } + if ( hitstats && hitstats->getEffectActive(EFF_DIVINE_GUARD) ) + { + thaumSpellArmorProc(hit.entity, *hitstats, false, parent, EFF_DIVINE_GUARD); + } + /*messagePlayer(0, "My damage: %d, AC: %d, Pierce: %d", my->arrowPower, AC(hitstats), my->arrowArmorPierce); messagePlayer(0, "Resolved to %d damage.", damage);*/ Sint32 oldHP = hitstats->HP; hit.entity->modHP(-damage); + + if ( hitstats ) + { + Sint32 damageTaken = oldHP - hitstats->HP; + if ( damageTaken > 0 ) + { + if ( hitstats->getEffectActive(EFF_DEFY_FLESH) ) + { + hit.entity->defyFleshProc(parent); + } + hit.entity->pinpointDamageProc(parent, damageTaken); + } + if ( hitstats->getEffectActive(EFF_SPORES) ) + { + if ( hit.entity->behavior == &actPlayer + && hitstats->type == MYCONID && hitstats->getEffectActive(EFF_GROWTH) >= 4 ) + { + floorMagicCreateSpores(hit.entity, hit.entity->x, hit.entity->y, hit.entity, 0, SPELL_SPORES); + } + } + } + // write obituary if ( parent ) { @@ -742,6 +1014,16 @@ void actArrow(Entity* my) } } } + + if ( hit.entity->onEntityTrapHitSacredPath(parent) ) + { + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + Language::get(6472), Language::get(6291)); + } + playSoundEntity(hit.entity, 166, 128); + } } else { @@ -855,10 +1137,29 @@ void actArrow(Entity* my) doSkillIncrease = false; // no skill for killing/hurting other turrets. } } + else if ( hit.entity->behavior == &actMonster + && (hit.entity->monsterAllyGetPlayerLeader() || (hitstats && achievementObserver.checkUidIsFromPlayer(hitstats->leader_uid) >= 0)) + && parent && parent->behavior == &actPlayer ) + { + doSkillIncrease = false; // no level up on allies + } if ( hit.entity->behavior == &actPlayer && parent && parent->behavior == &actPlayer ) { doSkillIncrease = false; // no skill for killing/hurting players } + if ( hitstats->getEffectActive(EFF_STASIS) ) + { + doSkillIncrease = false; + } + + if ( doSkillIncrease && parent && parent->behavior == &actPlayer ) + { + if ( parent->isInvisible() && parent->checkEnemy(hit.entity) ) + { + players[parent->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, 10.0, 1.0, hit.entity); + } + } + int chance = 10; if ( doSkillIncrease && (local_rng.rand() % chance == 0) && parent && parent->getStats() ) { @@ -895,6 +1196,47 @@ void actArrow(Entity* my) } } + bool envenomWeapon = false; + if ( parent ) + { + Stat* parentStats = parent->getStats(); + if ( parentStats && parentStats->getEffectActive(EFF_ENVENOM_WEAPON) && hitstats ) + { + if ( local_rng.rand() % 2 == 0 ) + { + int envenomDamage = std::min( + getSpellDamageSecondaryFromID(SPELL_ENVENOM_WEAPON, parent, parentStats, parent), + getSpellDamageFromID(SPELL_ENVENOM_WEAPON, parent, parentStats, parent)); + + hit.entity->modHP(-envenomDamage); // do the damage + for ( int tmp = 0; tmp < 3; ++tmp ) + { + Entity* gib = spawnGib(hit.entity, 211); + serverSpawnGibForClient(gib); + } + if ( !hitstats->getEffectActive(EFF_POISONED) ) + { + envenomWeapon = true; + hitstats->setEffectActive(EFF_POISONED, 1); + + int duration = 160 * envenomDamage; + hitstats->EFFECTS_TIMERS[EFF_POISONED] = std::max(200, duration - hit.entity->getCON() * 20); + hitstats->poisonKiller = parent->getUID(); + if ( hit.entity->isEntityPlayer() ) + { + messagePlayerMonsterEvent(hit.entity->isEntityPlayer(), makeColorRGB(255, 0, 0), *parentStats, Language::get(6531), Language::get(6532), MSG_COMBAT); + serverUpdateEffects(hit.entity->isEntityPlayer()); + } + + if ( parent->behavior == &actPlayer ) + { + players[parent->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_ENVENOM_WEAPON, 50.0, 1.0, hit.entity); + } + } + } + } + } + if ( hitstats->HP <= 0 && parent) { parent->awardXP( hit.entity, true, true ); @@ -909,15 +1251,7 @@ void actArrow(Entity* my) // alert the monster if ( hit.entity->behavior == &actMonster && parent != nullptr ) { - bool alertTarget = true; - if ( parent->behavior == &actMonster && parent->monsterAllyIndex != -1 ) - { - if ( hit.entity->behavior == &actMonster && hit.entity->monsterAllyIndex != -1 ) - { - // if a player ally + hit another ally, don't aggro back - alertTarget = false; - } - } + bool alertTarget = hit.entity->monsterAlertBeforeHit(parent); if ( alertTarget && hit.entity->monsterState != MONSTER_STATE_ATTACK && (hitstats->type < LICH || hitstats->type >= SHOPKEEPER) ) { @@ -967,7 +1301,7 @@ void actArrow(Entity* my) // you shot the %s! messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(446), Language::get(448), MSG_COMBAT_BASIC); } - if ( my->arrowArmorPierce > 0 && AC(hitstats) > 0 ) + if ( my->arrowArmorPierce > 0 /*&& AC(hitstats) > 0*/ ) { messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2513), Language::get(2514), MSG_COMBAT); } @@ -1019,12 +1353,21 @@ void actArrow(Entity* my) } } - if ( my->arrowArmorPierce > 0 && AC(hitstats) > 0 ) + if ( my->arrowArmorPierce > 0 /*&& AC(hitstats) > 0*/ ) { messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(2515)); } } + if ( my->sprite == PROJECTILE_SEED_POISON_SPRITE ) + { + floorMagicCreateSpores(nullptr, hit.entity->x, hit.entity->y, parent, 15, SPELL_SPORES); + } + else if ( my->sprite == PROJECTILE_SEED_ROOT_SPRITE ) + { + floorMagicCreateRoots(hit.entity->x, hit.entity->y, parent, 3, SPELL_ROOTS, 3 * TICKS_PER_SECOND, PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE); + } + bool statusEffectApplied = false; if ( hitstats->HP > 0 ) { @@ -1036,7 +1379,61 @@ void actArrow(Entity* my) procEffect = false; } } - if ( my->arrowQuiverType == QUIVER_FIRE && procEffect ) + + if ( hitstats->HP > 0 && hitstats->OLDHP > hitstats->HP && parent ) + { + // assist damage from summons + if ( parent->behavior == &actMonster ) + { + int summonSpellID = getSpellFromSummonedEntityForSpellEvent(parent); + if ( summonSpellID != SPELL_NONE ) + { + if ( Stat* parentStats = parent->getStats() ) + { + if ( Entity* leader = uidToEntity(parentStats->leader_uid) ) + { + if ( local_rng.rand() % 8 == 0 ) + { + magicOnSpellCastEvent(leader, nullptr, hit.entity, summonSpellID, spell_t::SPELL_LEVEL_EVENT_ASSIST | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + } + } + } + } + } + + if ( my->sprite == PROJECTILE_SEED_ROOT_SPRITE ) + { + if ( hit.entity->setEffect(EFF_ROOTED, true, TICKS_PER_SECOND, false) ) + { + statusEffectApplied = true; + } + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6743), Language::get(6742), MSG_COMBAT); + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6744)); + } + } + else if ( my->sprite == PROJECTILE_SEED_POISON_SPRITE ) + { + statusEffectApplied = true; + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6746), Language::get(6745), MSG_COMBAT); + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6747)); + } + } + else if ( my->arrowQuiverType == QUIVER_FIRE && procEffect ) { bool burning = hit.entity->flags[BURNING]; hit.entity->SetEntityOnFire(my); @@ -1149,11 +1546,11 @@ void actArrow(Entity* my) else if ( my->arrowQuiverType == QUIVER_HUNTING && !(hitstats->amulet && hitstats->amulet->type == AMULET_POISONRESISTANCE) && !(hitstats->type == INSECTOID) && procEffect ) { - if ( !hitstats->EFFECTS[EFF_POISONED] ) + if ( !hitstats->getEffectActive(EFF_POISONED) ) { hitstats->poisonKiller = my->parent; - hitstats->EFFECTS[EFF_POISONED] = true; - hitstats->EFFECTS[EFF_SLOW] = true; + hitstats->setEffectActive(EFF_POISONED, 1); + hitstats->setEffectActive(EFF_SLOW, 1); if ( my->arrowPoisonTime > 0 ) { hitstats->EFFECTS_TIMERS[EFF_POISONED] = my->arrowPoisonTime; @@ -1178,7 +1575,7 @@ void actArrow(Entity* my) } if ( hit.entity->behavior == &actPlayer ) { - if ( local_rng.rand() % 8 == 0 && hit.entity->char_gonnavomit == 0 && !hitstats->EFFECTS[EFF_VOMITING] ) + if ( local_rng.rand() % 8 == 0 && hit.entity->char_gonnavomit == 0 && !hitstats->getEffectActive(EFF_VOMITING) ) { // maybe vomit messagePlayer(hit.entity->skill[2], MESSAGE_STATUS, Language::get(634)); @@ -1197,6 +1594,20 @@ void actArrow(Entity* my) messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT_BASIC, color, Language::get(451)); // you are hit by an arrow! } } + else if ( envenomWeapon ) + { + statusEffectApplied = true; + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6533), Language::get(6534), MSG_COMBAT); + } + } + + if ( my->arrowQuiverType == QUIVER_HUNTING && procEffect ) + { + hit.entity->degradeAmuletProc(hitstats, AMULET_POISONRESISTANCE); + } } else { @@ -1302,7 +1713,9 @@ void actArrow(Entity* my) // if nothing chosen to degrade, check extra shield chances to degrade if ( hitstats->shield != NULL && hitstats->shield->status > BROKEN && armor == NULL && !itemTypeIsQuiver(hitstats->shield->type) && itemCategory(hitstats->shield) != SPELLBOOK - && hitstats->shield->type != TOOL_TINKERING_KIT ) + && !itemTypeIsFoci(hitstats->shield->type) + && !(hitstats->shield->type >= INSTRUMENT_FLUTE && hitstats->shield->type <= INSTRUMENT_HORN) + && hitstats->shield->type != TOOL_TINKERING_KIT && hitstats->shield->type != TOOL_FRYING_PAN ) { if ( hitstats->shield->type == TOOL_CRYSTALSHARD && hitstats->defending ) { @@ -1359,7 +1772,7 @@ void actArrow(Entity* my) { increaseSkill = false; } - else if ( hitstats->EFFECTS[EFF_SHAPESHIFT] ) + else if ( hitstats->getEffectActive(EFF_SHAPESHIFT) ) { increaseSkill = false; } @@ -1390,6 +1803,7 @@ void actArrow(Entity* my) { shieldDegradeChance += 10; } + if ( hit.entity->behavior == &actPlayer ) { if ( itemCategory(hitstats->shield) == ARMOR ) @@ -1421,13 +1835,16 @@ void actArrow(Entity* my) if ( armor != NULL && armor->status > BROKEN ) { - hit.entity->degradeArmor(*hitstats, *armor, armornum); - armorDegraded = true; + if ( hit.entity->degradeArmor(*hitstats, *armor, armornum) ) + { + armorDegraded = true; + } if ( armor->status == BROKEN ) { if ( parent && parent->behavior == &actPlayer && hit.entity->behavior == &actMonster ) { steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_UNSTOPPABLE_FORCE, STEAM_STAT_INT, 1); + players[parent->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_DEGRADE, hit.entity); if ( armornum == 4 && hitstats->type == BUGBEAR && (hitstats->defending || hit.entity->monsterAttack == MONSTER_POSE_BUGBEAR_SHIELD) ) { @@ -1497,16 +1914,46 @@ void actArrow(Entity* my) updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, false, dmgGib); } + } + + if ( ARROW_STUCK > 0 ) + { + if ( my->sprite == PROJECTILE_SEED_POISON_SPRITE ) + { + if ( !hitstats || hit.entity->isInertMimic() ) + { + floorMagicCreateSpores(nullptr, hit.entity->x, hit.entity->y, parent, 15, SPELL_SPORES); + } + } + else if ( my->sprite == PROJECTILE_SEED_ROOT_SPRITE ) + { + if ( !hitstats || hit.entity->isInertMimic() ) + { + floorMagicCreateRoots(hit.entity->x, hit.entity->y, parent, 3, SPELL_ROOTS, 3 * TICKS_PER_SECOND, PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE); + } + } + my->removeLightField(); + list_RemoveNode(my->mynode); } - my->removeLightField(); - list_RemoveNode(my->mynode); } else if ( my->sprite == PROJECTILE_ROCK_SPRITE ) { my->removeLightField(); list_RemoveNode(my->mynode); // rocks don't stick to walls... } + else if ( my->sprite == PROJECTILE_SEED_POISON_SPRITE ) + { + floorMagicCreateSpores(nullptr, my->x, my->y, uidToEntity(my->parent), 15, SPELL_SPORES); + my->removeLightField(); + list_RemoveNode(my->mynode); // rocks don't stick to walls... + } + else if ( my->sprite == PROJECTILE_SEED_ROOT_SPRITE ) + { + floorMagicCreateRoots(my->x, my->y, uidToEntity(my->parent), 3, SPELL_ROOTS, 3 * TICKS_PER_SECOND, PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE); + my->removeLightField(); + list_RemoveNode(my->mynode); // rocks don't stick to walls... + } else { playSoundEntity(my, 72 + local_rng.rand() % 3, 64); diff --git a/src/actarrowtrap.cpp b/src/actarrowtrap.cpp index 32bc49040..3768dec33 100644 --- a/src/actarrowtrap.cpp +++ b/src/actarrowtrap.cpp @@ -114,29 +114,45 @@ void actArrowTrap(Entity* my) return; } -#ifdef USE_FMOD - if ( ARROWTRAP_AMBIENCE == 0 ) + if ( my->actTrapSabotaged == 0 ) { +#ifdef USE_FMOD + if ( ARROWTRAP_AMBIENCE == 0 ) + { + ARROWTRAP_AMBIENCE--; + my->stopEntitySound(); + my->entity_sound = playSoundEntityLocal(my, 149, 64); + } + if ( my->entity_sound ) + { + bool playing = false; + my->entity_sound->isPlaying(&playing); + if ( !playing ) + { + my->entity_sound = nullptr; + } + } +#else ARROWTRAP_AMBIENCE--; - my->entity_sound = playSoundEntityLocal(my, 149, 64); - } - if ( my->entity_sound ) - { - bool playing = false; - my->entity_sound->isPlaying(&playing); - if ( !playing ) + if ( ARROWTRAP_AMBIENCE <= 0 ) { - my->entity_sound = nullptr; + ARROWTRAP_AMBIENCE = TICKS_PER_SECOND * 30; + playSoundEntityLocal( my, 149, 64 ); } +#endif } -#else - ARROWTRAP_AMBIENCE--; - if ( ARROWTRAP_AMBIENCE <= 0 ) + else { - ARROWTRAP_AMBIENCE = TICKS_PER_SECOND * 30; - playSoundEntityLocal( my, 149, 64 ); - } +#ifdef USE_FMOD + my->stopEntitySound(); #endif + } + + if ( multiplayer == CLIENT ) + { + my->flags[NOUPDATE] = true; + return; + } if ( !my->skill[28] ) { @@ -146,7 +162,7 @@ void actArrowTrap(Entity* my) Entity* targetToAutoHit = nullptr; // received on signal - if ( my->skill[28] == 2 || ARROWTRAP_DISABLED == -1 ) + if ( (my->skill[28] == 2 || ARROWTRAP_DISABLED == -1) && my->actTrapSabotaged == 0 ) { if ( ARROWTRAP_FIRED % 2 == 1 ) // not ready to fire. { @@ -275,6 +291,14 @@ void actArrowTrap(Entity* my) entity->arrowPoisonTime = 360; if ( stronger ) { ARROWTRAP_REFIRE = 25; } break; + case QUIVER_BONE: + entity->sprite = 2304; + if ( stronger ) { ARROWTRAP_REFIRE = 25; } + break; + case QUIVER_BLACKIRON: + entity->sprite = 2305; + if ( stronger ) { ARROWTRAP_REFIRE = 25; } + break; default: break; } diff --git a/src/actbeartrap.cpp b/src/actbeartrap.cpp index e58338dd6..f9c17e60e 100644 --- a/src/actbeartrap.cpp +++ b/src/actbeartrap.cpp @@ -133,7 +133,7 @@ void actBeartrap(Entity* my) { continue; } - if ( stat->type == GYROBOT || entity->isUntargetableBat() ) + if ( !entity->monsterIsTargetable() ) { continue; } @@ -155,7 +155,25 @@ void actBeartrap(Entity* my) if ( entityDist(my, entity) < 6.5 ) { entity->setEffect(EFF_PARALYZED, true, 200, false); - entity->setEffect(EFF_BLEEDING, true, 300, false); + if ( entity->setEffect(EFF_BLEEDING, true, 300, false) ) + { + if ( parent && parent->behavior == &actPlayer ) + { + if ( stats[parent->skill[2]]->helmet && stats[parent->skill[2]]->helmet->type == PUNISHER_HOOD ) + { + int mpAmount = parent->modMP(1 + local_rng.rand() % 2); + parent->playerInsectoidIncrementHungerToMP(mpAmount); + Uint32 color = makeColorRGB(0, 255, 0); + parent->setEffect(EFF_MP_REGEN, true, std::max(stats[parent->skill[2]]->EFFECTS_TIMERS[EFF_MP_REGEN], 10 * TICKS_PER_SECOND), false); + if ( parent->behavior == &actPlayer ) + { + messagePlayerColor(parent->skill[2], MESSAGE_HINT, color, Language::get(3753)); + steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_ITS_A_LIVING, STEAM_STAT_INT, 1); + } + playSoundEntity(parent, 168, 128); + } + } + } int damage = 10 + 3 * (BEARTRAP_STATUS + BEARTRAP_BEATITUDE); if ( parent ) { @@ -236,7 +254,8 @@ void actBeartrap(Entity* my) { messagePlayer(player, MESSAGE_HINT, Language::get(2522)); } - if ( local_rng.rand() % 10 == 0 ) + if ( local_rng.rand() % 10 == 0 && !stat->getEffectActive(EFF_STASIS) + && !monsterIsImmobileTurret(entity, stat) ) { parent->increaseSkill(PRO_LOCKPICKING); } @@ -383,7 +402,7 @@ void bombDoEffect(Entity* my, Entity* triggered, real_t entityDistance, bool spa bool wasAsleep = false; if ( stat ) { - wasAsleep = stat->EFFECTS[EFF_ASLEEP]; + wasAsleep = stat->getEffectActive(EFF_ASLEEP); } if ( damage > 0 ) { @@ -646,24 +665,43 @@ void bombDoEffect(Entity* my, Entity* triggered, real_t entityDistance, bool spa } if ( triggered->behavior == &actMonster ) { - if ( oldHP > 0 && stat->HP == 0 ) // got a kill + bool doSkillIncrease = true; + Stat* triggeredStats = triggered->getStats(); + if ( monsterIsImmobileTurret(triggered, triggeredStats) ) { - if ( local_rng.rand() % 5 == 0 ) - { - parent->increaseSkill(PRO_LOCKPICKING); - } + doSkillIncrease = false; + } + else if ( triggered->monsterAllyGetPlayerLeader() + || (triggeredStats && achievementObserver.checkUidIsFromPlayer(triggeredStats->leader_uid) >= 0) ) + { + doSkillIncrease = false; + } + else if ( triggeredStats && triggeredStats->getEffectActive(EFF_STASIS) ) + { + doSkillIncrease = false; } - else if ( oldHP > stat->HP ) + + if ( doSkillIncrease ) { - if ( local_rng.rand() % 20 == 0 ) // wounded + if ( oldHP > 0 && stat->HP == 0 ) // got a kill + { + if ( local_rng.rand() % 5 == 0 ) + { + parent->increaseSkill(PRO_LOCKPICKING); + } + } + else if ( oldHP > stat->HP ) + { + if ( local_rng.rand() % 20 == 0 ) // wounded + { + parent->increaseSkill(PRO_LOCKPICKING); + } + } + else if( local_rng.rand() % 20 == 0) // any other effect { parent->increaseSkill(PRO_LOCKPICKING); } } - else if( local_rng.rand() % 20 == 0) // any other effect - { - parent->increaseSkill(PRO_LOCKPICKING); - } if ( !achievementObserver.playerAchievements[player].bombTrack ) { @@ -884,7 +922,7 @@ void actBomb(Entity* my) } else if ( onEntity ) { - if ( onEntity->behavior == &actDoor ) + if ( onEntity->behavior == &actDoor || onEntity->behavior == &actIronDoor ) { if ( onEntity->doorHealth < BOMB_ENTITY_ATTACHED_START_HP || onEntity->flags[PASSABLE] || cursedExplode || BOMB_HIT_BY_PROJECTILE == 1 ) @@ -1005,7 +1043,7 @@ void actBomb(Entity* my) { continue; } - if ( stat->type == GYROBOT || entity->isUntargetableBat() ) + if ( !entity->monsterIsTargetable() ) { continue; } @@ -1170,6 +1208,63 @@ void actBomb(Entity* my) } } +bool Entity::entityCheckIfTriggeredWallButton() +{ + if ( multiplayer == CLIENT ) + { + return false; + } + if ( this->behavior != &actThrown && this->behavior != &actArrow ) + { + return false; + } + + bool foundButton = false; + + real_t height_limit_low = (behavior == &actThrown) ? 5.0 : 4.0; + real_t height_limit_high = -8.0; + + // check for wall buttons + if ( z < height_limit_low && z > height_limit_high ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 1); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + node_t* node; + for ( node = currentList->first; node != nullptr; node = node->next ) + { + if ( Entity* entity = ((Entity*)node->element) ) + { + if ( entity->behavior == &::actWallButton ) + { + Sint32 tmpsizex = sizex; + Sint32 tmpsizey = sizey; + sizex = std::max(sizex, 2); + sizey = std::max(sizey, 2); + if ( entityInsideEntity(this, entity) ) + { + entity->wallLockPlayerInteracting = MAXPLAYERS + 1; + foundButton = true; + if ( Entity* parent = uidToEntity(this->parent) ) + { + if ( parent->behavior == &actPlayer ) + { + entity->wallLockPlayerInteracting = 1 + parent->skill[2]; + } + } + } + sizex = tmpsizex; + sizey = tmpsizey; + } + } + } + } + } + + return foundButton; +} + bool Entity::entityCheckIfTriggeredBomb(bool triggerBomb) { if ( multiplayer == CLIENT ) @@ -1313,8 +1408,8 @@ void actDecoyBox(Entity* my) { // ignore pathing to this noisemaker as we're already distracted by it. if ( entityDist(entity, my) < TOUCHRANGE - && !myStats->EFFECTS[EFF_DISORIENTED] - && !myStats->EFFECTS[EFF_DISTRACTED_COOLDOWN] ) + && !myStats->getEffectActive(EFF_DISORIENTED) + && !myStats->getEffectActive(EFF_DISTRACTED_COOLDOWN) ) { // if we pathed within range detected = false; // skip the message. @@ -1347,7 +1442,7 @@ void actDecoyBox(Entity* my) { break; } - if ( !myStats->EFFECTS[EFF_DISTRACTED_COOLDOWN] + if ( !myStats->getEffectActive(EFF_DISTRACTED_COOLDOWN) && entity->monsterSetPathToLocation(my->x / 16, my->y / 16, 2, GeneratePathTypes::GENERATE_PATH_DEFAULT) && entity->children.first ) { @@ -1360,8 +1455,8 @@ void actDecoyBox(Entity* my) ++lured; if ( entityDist(entity, my) < TOUCHRANGE - && !myStats->EFFECTS[EFF_DISORIENTED] - && !myStats->EFFECTS[EFF_DISTRACTED_COOLDOWN] ) + && !myStats->getEffectActive(EFF_DISORIENTED) + && !myStats->getEffectActive(EFF_DISTRACTED_COOLDOWN) ) { detected = false; // skip the message. @@ -1396,9 +1491,11 @@ void actDecoyBox(Entity* my) Entity* gyrobot = uidToEntity(*c); if ( gyrobot && gyrobot->getRace() == GYROBOT ) { - if ( entity->entityShowOnMap < 250 ) + if ( entity->getEntityShowOnMapDuration() == 0 + || (entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_GYRO + && entity->getEntityShowOnMapDuration() < TICKS_PER_SECOND * 5) ) { - entity->entityShowOnMap = TICKS_PER_SECOND * 5; + entity->setEntityShowOnMap(Entity::SHOW_MAP_GYRO, TICKS_PER_SECOND * 5); if ( parent->skill[2] != 0 ) { serverUpdateEntitySkill(entity, 59); diff --git a/src/actboulder.cpp b/src/actboulder.cpp index 964beeb72..4a1f3153d 100644 --- a/src/actboulder.cpp +++ b/src/actboulder.cpp @@ -37,6 +37,8 @@ #define BOULDER_INIT my->skill[11] #define BOULDER_LAVA_EXPLODE my->skill[12] #define BOULDER_SOUND_ON_PUSH my->skill[13] +#define BOULDER_TELEKINESIS_PULL my->skill[14] +#define BOULDER_TELEKINESIS_PUSH my->skill[15] const int BOULDER_LAVA_SPRITE = 989; const int BOULDER_ARCANE_SPRITE = 990; @@ -117,6 +119,10 @@ bool doesEntityStopBoulder(Entity* entity) { return true; } + else if ( entity->behavior == &actIronDoor ) + { + return true; + } else if ( entity->behavior == &actBoulder ) { return true; @@ -189,6 +195,18 @@ bool doesEntityStopBoulder(Entity* entity) { return true; } + else if ( entity->behavior == &actCauldron ) + { + return true; + } + else if ( entity->behavior == &actWorkbench ) + { + return true; + } + else if ( entity->behavior == &actMailbox ) + { + return true; + } return false; } @@ -219,6 +237,15 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit Stat* stats = entity->getStats(); if ( stats ) { + if ( stats->type == DUCK_SMALL ) + { + if ( entity->monsterAttack == 0 ) + { + entity->attack(local_rng.rand() % 2 ? MONSTER_POSE_MELEE_WINDUP2 : MONSTER_POSE_MELEE_WINDUP3, 0, nullptr); + } + return 0; + } + if ( entity->behavior == &actPlayer ) { Uint32 color = makeColorRGB(255, 0, 0); @@ -244,14 +271,24 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit } playSoundEntity(my, 181, 128); playSoundEntity(entity, 28, 64); - Entity* gib = spawnGib(entity); + if ( Entity* gib = spawnGib(entity) ) + { + serverSpawnGibForClient(gib); + } int damage = 80; - if ( my->sprite == BOULDER_LAVA_SPRITE - || my->sprite == BOULDER_ARCANE_SPRITE ) + if ( my->sprite == BOULDER_LAVA_SPRITE ) { damage = 50; } + else if ( my->sprite == BOULDER_ARCANE_SPRITE ) + { + damage = 80; + } + if ( my->boulderShatterEarthSpell > 0 ) + { + damage = my->boulderShatterEarthDamage; + } int trapResist = entity->getEntityBonusTrapResist(); if ( trapResist != 0 ) @@ -260,7 +297,16 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit damage *= mult; } - if ( stats->helmet ) + if ( entity->onEntityTrapHitSacredPath(my) ) + { + if ( entity->behavior == &actPlayer ) + { + messagePlayerColor(entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + Language::get(6490)); + } + playSoundEntity(entity, 166, 128); + } + else if ( stats->helmet ) { bool shapeshifted = (entity->behavior == &actPlayer && entity->effectShapeshift != NOTHING); @@ -279,7 +325,10 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit } damage = 0; } - stats->helmet->status = BROKEN; + if ( !entity->spellEffectPreserveItem(stats->helmet) ) + { + stats->helmet->status = BROKEN; + } } else if ( stats->helmet->type == HELM_MINING ) { @@ -305,9 +354,12 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit } } damage *= mult; - if ( stats->helmet->status > BROKEN ) + if ( !entity->spellEffectPreserveItem(stats->helmet) ) { - stats->helmet->status = (Status)((int)stats->helmet->status - 1); + if ( stats->helmet->status > BROKEN ) + { + stats->helmet->status = (Status)((int)stats->helmet->status - 1); + } } } @@ -330,9 +382,10 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 0; net_packet->data[5] = stats->helmet->status; + SDLNet_Write16((int)stats->helmet->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -355,17 +408,48 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit entity->modHP(-damage); if ( entity->behavior == &actPlayer && stats->HP < oldHP ) { - Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_DAMAGE, "boulder trap", oldHP - stats->HP); + Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_DAMAGE, "arcane boulder", oldHP - stats->HP); } entity->setObituary(Language::get(3899)); stats->killer = KilledBy::BOULDER; } else { - entity->modHP(-damage); - if ( entity->behavior == &actPlayer && stats->HP < oldHP ) + if ( my->boulderShatterEarthSpell > 0 ) { - Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_DAMAGE, "boulder trap", oldHP - stats->HP); + Entity* caster = uidToEntity(my->boulderShatterEarthSpell); + applyGenericMagicDamage(caster, entity, *my, SPELL_SHATTER_EARTH, damage, true); + } + else if ( BOULDER_PLAYERPUSHED >= MAXPLAYERS ) + { + int spellID = SPELL_TELEKINESIS; + if ( (BOULDER_PLAYERPUSHED / MAXPLAYERS) == 2 ) + { + spellID = SPELL_KINETIC_PUSH; + } + applyGenericMagicDamage(players[BOULDER_PLAYERPUSHED % MAXPLAYERS]->entity, + entity, *my, spellID, damage, true); + if ( entity->behavior == &actPlayer && stats->HP < oldHP ) + { + Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_DAMAGE, "boulder trap", oldHP - stats->HP); + } + } + else if ( BOULDER_PLAYERPUSHED >= 0 ) + { + applyGenericMagicDamage(players[BOULDER_PLAYERPUSHED]->entity, + entity, *my, SPELL_NONE, damage, true); + if ( entity->behavior == &actPlayer && stats->HP < oldHP ) + { + Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_DAMAGE, "boulder trap", oldHP - stats->HP); + } + } + else + { + entity->modHP(-damage); + if ( entity->behavior == &actPlayer && stats->HP < oldHP ) + { + Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_DAMAGE, "boulder trap", oldHP - stats->HP); + } } entity->setObituary(Language::get(1505)); stats->killer = KilledBy::BOULDER; @@ -381,15 +465,19 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit entity->playerAutomatonDeathCounter = TICKS_PER_SECOND * 5; // set the death timer to immediately pop for players. } steamAchievementClient(entity->skill[2], "BARONY_ACH_THROW_ME_THE_WHIP"); - if ( BOULDER_PLAYERPUSHED >= 0 && entity->skill[2] != BOULDER_PLAYERPUSHED ) + if ( BOULDER_PLAYERPUSHED >= 0 && entity->skill[2] != (BOULDER_PLAYERPUSHED % MAXPLAYERS) ) { - steamAchievementClient(BOULDER_PLAYERPUSHED, "BARONY_ACH_MOVED_ITSELF"); + steamAchievementClient(BOULDER_PLAYERPUSHED % MAXPLAYERS, "BARONY_ACH_MOVED_ITSELF"); } if ( my->sprite == BOULDER_LAVA_SPRITE ) { Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_KILLED_BY, "brimstone boulder", 1); } + else if ( my->sprite == BOULDER_ARCANE_SPRITE ) + { + Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_KILLED_BY, "arcane boulder", 1); + } else { Compendium_t::Events_t::eventUpdateWorld(entity->skill[2], Compendium_t::CPDM_TRAP_KILLED_BY, "boulder trap", 1); @@ -399,7 +487,7 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit } if ( BOULDER_PLAYERPUSHED >= 0 && oldHP > 0 && stats->HP <= 0 ) { - Compendium_t::Events_t::eventUpdateWorld(BOULDER_PLAYERPUSHED, Compendium_t::CPDM_COMBAT_MASONRY_BOULDERS, "masons guild", 1); + Compendium_t::Events_t::eventUpdateWorld(BOULDER_PLAYERPUSHED % MAXPLAYERS, Compendium_t::CPDM_COMBAT_MASONRY_BOULDERS, "masons guild", 1); } if ( !lifeSaving ) @@ -428,7 +516,7 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit if ( mySummon && mySummon->monsterAllySummonRank != 0 ) { Stat* mySummonStats = mySummon->getStats(); - if ( mySummonStats ) + if ( mySummonStats && mySummonStats->type == SKELETON ) { int mp = (mySummonStats->MAXMP * (mySummonStats->HP / static_cast(mySummonStats->MAXHP))); if ( numSummonedAllies == 0 ) @@ -470,6 +558,15 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit { i = 0; } + if ( my->boulderShatterEarthSpell > 0 ) + { + i = 0; + createParticleRock(entity, 78); + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_ABILITY_ROCK, 78); + } + } int c; for ( c = 0; c < i; c++ ) { @@ -542,6 +639,10 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit { Compendium_t::Events_t::eventUpdateWorld(entity->monsterAllyIndex, Compendium_t::CPDM_TRAP_FOLLOWERS_KILLED, "brimstone boulder", 1); } + else if ( my->sprite == BOULDER_ARCANE_SPRITE ) + { + Compendium_t::Events_t::eventUpdateWorld(entity->monsterAllyIndex, Compendium_t::CPDM_TRAP_FOLLOWERS_KILLED, "arcane boulder", 1); + } else { Compendium_t::Events_t::eventUpdateWorld(entity->monsterAllyIndex, Compendium_t::CPDM_TRAP_FOLLOWERS_KILLED, "boulder trap", 1); @@ -588,6 +689,20 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit { if ( ignoreInsideEntity || entityInsideEntity( my, entity ) ) { + if ( my->boulderShatterEarthSpell > 0 ) + { + createParticleRock(entity, 78); + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_ABILITY_ROCK, 78); + } + + // destroy the boulder + playSoundEntity(my, 67, 128); + list_RemoveNode(my->mynode); + return 1; + } + // stop the boulder BOULDER_STOPPED = 1; my->vel_x = 0.0; // TODOR: Anywhere this is could possible be changed to be a static 'if( BOULDER_ROLLING == 0 ) { vel = 0 }' instead of duplicating code everywhere @@ -610,14 +725,14 @@ int boulderCheckAgainstEntity(Entity* my, Entity* entity, bool ignoreInsideEntit if ( ignoreInsideEntity || entityInsideEntity( my, entity ) ) { playSoundEntity(entity, 28, 64); - entity->skill[4] = 0; - if ( !entity->skill[0] ) + entity->doorHealth = 0; + if ( !entity->doorDir ) { - entity->skill[6] = (my->x > entity->x); + entity->doorSmacked = (my->x > entity->x); } else { - entity->skill[6] = (my->y < entity->y); + entity->doorSmacked = (my->y < entity->y); } playSoundEntity(my, 181, 128); } @@ -724,9 +839,12 @@ void actBoulder(Entity* my) } real_t boulderModifier = 1.0; - if ( gameModeManager.currentSession.challengeRun.isActive(GameModeManager_t::CurrentSession_t::ChallengeRun_t::CHEVENT_STRONG_TRAPS) ) + if ( my->boulderShatterEarthSpell == 0 ) { - boulderModifier = 2.0; + if ( gameModeManager.currentSession.challengeRun.isActive(GameModeManager_t::CurrentSession_t::ChallengeRun_t::CHEVENT_STRONG_TRAPS) ) + { + boulderModifier = 2.0; + } } // gravity @@ -738,6 +856,14 @@ void actBoulder(Entity* my) BOULDER_NOGROUND = true; } } + if ( my->boulderShatterEarthSpell > 0 ) + { + if ( my->z >= 0 && fabs(my->vel_z) > 1 && fabs(my->vel_z) < 2 ) + { + BOULDER_NOGROUND = true; // 2nd bounce + } + } + if ( my->z < 0 || BOULDER_NOGROUND ) { my->vel_z = std::min(my->vel_z + .1, 3.0); @@ -753,9 +879,24 @@ void actBoulder(Entity* my) } return; } - if ( !BOULDER_NOGROUND ) + + if ( !noground && my->boulderShatterEarthSpell > 0 && my->z >= 4.0 ) + { + createParticleRock(my, 78); + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(my, PARTICLE_EFFECT_ABILITY_ROCK, 78); + } + + // destroy the boulder + playSoundEntity(my, 67, 128); + list_RemoveNode(my->mynode); + return; + } + + //if ( !BOULDER_NOGROUND ) -- make it so falling boulders over pits does damage { - if ( my->z >= -8 && fabs(my->vel_z) > 2 ) + if ( my->z >= -8 && my->z < 0.0 && fabs(my->vel_z) > 2 ) { std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) @@ -1064,16 +1205,23 @@ void actBoulder(Entity* my) if ( !BOULDER_ROLLING ) { BOULDER_PLAYERPUSHED = -1; + int playerTelekinesis = BOULDER_TELEKINESIS_PULL - 1; + int playerKineticPush = BOULDER_TELEKINESIS_PUSH - 1; + for (i = 0; i < MAXPLAYERS; i++) { - if ( selectedEntity[i] == my || client_selected[i] == my ) + if ( selectedEntity[i] == my || client_selected[i] == my || playerTelekinesis == i || playerKineticPush == i ) { - if (inrange[i]) + if (inrange[i] || playerTelekinesis == i || playerKineticPush == i ) { bool hasRingOfStr = false; if ( players[i] && players[i]->entity ) { - if ( stats[i]->ring + if ( playerTelekinesis == i || playerKineticPush == i ) + { + hasRingOfStr = true; + } + else if ( stats[i]->ring && stats[i]->ring->type == ItemType::RING_STRENGTH) { hasRingOfStr = true; @@ -1083,7 +1231,8 @@ void actBoulder(Entity* my) { hasRingOfStr = true; } - else if ( stats[i]->EFFECTS[EFF_POTION_STR] ) + else if ( stats[i]->getEffectActive(EFF_POTION_STR) + || stats[i]->getEffectActive(EFF_GREATER_MIGHT) ) { hasRingOfStr = true; } @@ -1097,7 +1246,16 @@ void actBoulder(Entity* my) if (players[i] && players[i]->entity) { BOULDER_SOUND_ON_PUSH = i + 1; - BOULDER_ROLLING = 1; + if ( playerKineticPush == i ) + { + BOULDER_STOPPED = 0; + my->vel_x = 0.0; + my->vel_y = 0.0; + } + else + { + BOULDER_ROLLING = 1; + } /*my->x = floor(my->x / 16) * 16 + 8; my->y = floor(my->y / 16) * 16 + 8;*/ @@ -1105,6 +1263,10 @@ void actBoulder(Entity* my) BOULDER_DESTY = (int)(my->y / 16) * 16 + 8; real_t tangent = atan2(players[i]->entity->y - my->y, players[i]->entity->x - my->x); + if ( BOULDER_TELEKINESIS_PULL > 0 && (BOULDER_TELEKINESIS_PULL - 1 == i) ) + { + tangent += PI; + } while ( tangent >= 2 * PI ) { tangent -= 2 * PI; @@ -1114,6 +1276,8 @@ void actBoulder(Entity* my) tangent += 2 * PI; } real_t angle = tangent * 180.0 / PI; + + if ( (tangent >= PI - PI / 4) && tangent < (PI + PI / 4) ) { BOULDER_ROLLDIR = 0; // east @@ -1134,6 +1298,11 @@ void actBoulder(Entity* my) BOULDER_ROLLDIR = 3; // north //messagePlayer(0, MESSAGE_DEBUG, "GO NORTH %.2f", angle); } + + if ( playerKineticPush == i ) + { + my->yaw = BOULDER_ROLLDIR * PI / 2; + } //if ( (int)(players[i]->entity->x / 16) < (int)(my->x / 16) ) //{ // BOULDER_ROLLDIR = 0; // east @@ -1166,6 +1335,14 @@ void actBoulder(Entity* my) break; } BOULDER_PLAYERPUSHED = i; + if ( playerTelekinesis == i ) + { + BOULDER_PLAYERPUSHED = i + MAXPLAYERS; + } + else if ( playerKineticPush == i ) + { + BOULDER_PLAYERPUSHED = i + 2 * MAXPLAYERS; + } } } } @@ -1340,7 +1517,14 @@ void actBoulder(Entity* my) } } - Compendium_t::Events_t::eventUpdateWorld(BOULDER_SOUND_ON_PUSH - 1, Compendium_t::CPDM_BOULDERS_PUSHED, "boulder trap", 1); + if ( my->sprite == BOULDER_ARCANE_SPRITE ) + { + Compendium_t::Events_t::eventUpdateWorld(BOULDER_SOUND_ON_PUSH - 1, Compendium_t::CPDM_BOULDERS_PUSHED, "arcane boulder", 1); + } + else + { + Compendium_t::Events_t::eventUpdateWorld(BOULDER_SOUND_ON_PUSH - 1, Compendium_t::CPDM_BOULDERS_PUSHED, "boulder trap", 1); + } BOULDER_SOUND_ON_PUSH = 0; } } @@ -1446,6 +1630,8 @@ void actBoulder(Entity* my) { --BOULDER_BLOODTIME; } + BOULDER_TELEKINESIS_PULL = 0; + BOULDER_TELEKINESIS_PUSH = 0; } #define BOULDERTRAP_FIRED my->skill[0] @@ -1484,11 +1670,22 @@ void actBoulderTrapHole(Entity* my) } } +int getBoulderSpriteForMap() +{ + if ( currentlevel >= 26 ) + { + return BOULDER_ARCANE_SPRITE; + } + return 245; +} + void actBoulderTrap(Entity* my) { int x, y; int c; + if ( my->actTrapSabotaged == 0 ) + { #ifdef USE_FMOD if ( BOULDERTRAP_AMBIENCE == 0 ) { @@ -1513,6 +1710,14 @@ void actBoulderTrap(Entity* my) playSoundEntityLocal(my, 149, 64); } #endif + } + else + { +#ifdef USE_FMOD + my->stopEntitySound(); +#endif + return; + } if ( !my->skill[28] ) { @@ -1567,7 +1772,7 @@ void actBoulderTrap(Entity* my) } if ( foundTrapdoor == c ) { - Entity* entity = newEntity(245, 1, map.entities, nullptr); // boulder + Entity* entity = newEntity(getBoulderSpriteForMap(), 1, map.entities, nullptr); // boulder entity->parent = my->getUID(); entity->x = (x << 4) + 8; entity->y = (y << 4) + 8; @@ -1621,6 +1826,8 @@ void actBoulderTrapEast(Entity* my) int x, y; int c; + if ( my->actTrapSabotaged == 0 ) + { #ifdef USE_FMOD if ( my->boulderTrapAmbience == 0 ) { @@ -1645,6 +1852,14 @@ void actBoulderTrapEast(Entity* my) playSoundEntityLocal(my, 149, 64); } #endif + } + else + { +#ifdef USE_FMOD + my->stopEntitySound(); +#endif + return; + } if ( my->boulderTrapRefireCounter > 0 ) { @@ -1687,7 +1902,7 @@ void actBoulderTrapEast(Entity* my) y = ((int)(my->y)) >> 4; if ( !map.tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map.height] ) { - Entity* entity = newEntity(245, 1, map.entities, nullptr); // boulder + Entity* entity = newEntity(getBoulderSpriteForMap(), 1, map.entities, nullptr); // boulder entity->parent = my->getUID(); entity->x = (x << 4) + 8; entity->y = (y << 4) + 8; @@ -1731,6 +1946,8 @@ void actBoulderTrapSouth(Entity* my) int x, y; int c; + if ( my->actTrapSabotaged == 0 ) + { #ifdef USE_FMOD if ( my->boulderTrapAmbience == 0 ) { @@ -1755,6 +1972,14 @@ void actBoulderTrapSouth(Entity* my) playSoundEntityLocal(my, 149, 64); } #endif + } + else + { +#ifdef USE_FMOD + my->stopEntitySound(); +#endif + return; + } if ( my->boulderTrapRefireCounter > 0 ) { @@ -1797,7 +2022,7 @@ void actBoulderTrapSouth(Entity* my) y = ((int)(my->y)) >> 4; if ( !map.tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map.height] ) { - Entity* entity = newEntity(245, 1, map.entities, nullptr); // boulder + Entity* entity = newEntity(getBoulderSpriteForMap(), 1, map.entities, nullptr); // boulder entity->parent = my->getUID(); entity->x = (x << 4) + 8; entity->y = (y << 4) + 8; @@ -1841,6 +2066,8 @@ void actBoulderTrapWest(Entity* my) int x, y; int c; + if ( my->actTrapSabotaged == 0 ) + { #ifdef USE_FMOD if ( my->boulderTrapAmbience == 0 ) { @@ -1865,6 +2092,14 @@ void actBoulderTrapWest(Entity* my) playSoundEntityLocal(my, 149, 64); } #endif + } + else + { +#ifdef USE_FMOD + my->stopEntitySound(); +#endif + return; + } if ( my->boulderTrapRefireCounter > 0 ) { @@ -1907,7 +2142,7 @@ void actBoulderTrapWest(Entity* my) y = ((int)(my->y)) >> 4; if ( !map.tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map.height] ) { - Entity* entity = newEntity(245, 1, map.entities, nullptr); // boulder + Entity* entity = newEntity(getBoulderSpriteForMap(), 1, map.entities, nullptr); // boulder entity->parent = my->getUID(); entity->x = (x << 4) + 8; entity->y = (y << 4) + 8; @@ -1951,6 +2186,8 @@ void actBoulderTrapNorth(Entity* my) int x, y; int c; + if ( my->actTrapSabotaged == 0 ) + { #ifdef USE_FMOD if ( my->boulderTrapAmbience == 0 ) { @@ -1975,6 +2212,14 @@ void actBoulderTrapNorth(Entity* my) playSoundEntityLocal(my, 149, 64); } #endif + } + else + { +#ifdef USE_FMOD + my->stopEntitySound(); +#endif + return; + } if ( my->boulderTrapRefireCounter > 0 ) { @@ -2017,7 +2262,7 @@ void actBoulderTrapNorth(Entity* my) y = ((int)(my->y)) >> 4; if ( !map.tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map.height] ) { - Entity* entity = newEntity(245, 1, map.entities, nullptr); // boulder + Entity* entity = newEntity(getBoulderSpriteForMap(), 1, map.entities, nullptr); // boulder entity->parent = my->getUID(); entity->x = (x << 4) + 8; entity->y = (y << 4) + 8; @@ -2169,7 +2414,7 @@ void boulderSokobanOnDestroy(bool pushedOffLedge) bool Entity::isBoulderSprite() { - if ( sprite == 245 || sprite == 989 || sprite == 990 ) + if ( !flags[SPRITE] && (sprite == 245 || sprite == 989 || sprite == 990) ) { return true; } @@ -2220,7 +2465,7 @@ void boulderLavaOrArcaneOnDestroy(Entity* my, int sprite, Entity* boulderHitEnti } else { - boulderHitEntity->SetEntityOnFire(); + boulderHitEntity->SetEntityOnFire(my); Stat* stats = boulderHitEntity->getStats(); if ( stats ) { diff --git a/src/actcampfire.cpp b/src/actcampfire.cpp index cd335619c..5054e4acb 100644 --- a/src/actcampfire.cpp +++ b/src/actcampfire.cpp @@ -17,6 +17,8 @@ #include "net.hpp" #include "player.hpp" #include "prng.hpp" +#include "scores.hpp" +#include "collision.hpp" /*------------------------------------------------------------------------------- @@ -170,3 +172,496 @@ void actCampfire(Entity* my) } } } + +void actCauldron(Entity* my) +{ + if ( !CAMPFIRE_INIT ) + { + CAMPFIRE_INIT = 1; + CAMPFIRE_HEALTH = MAXPLAYERS; + my->createWorldUITooltip(); + } + + // crackling sounds + if ( CAMPFIRE_HEALTH > 0 ) + { +#ifdef USE_FMOD + if ( CAMPFIRE_SOUNDTIME == 0 ) + { + CAMPFIRE_SOUNDTIME--; + my->stopEntitySound(); + my->entity_sound = playSoundEntityLocal(my, 133, 32); + } + if ( my->entity_sound ) + { + bool playing = false; + my->entity_sound->isPlaying(&playing); + if ( !playing ) + { + my->entity_sound = nullptr; + } + } +#else + CAMPFIRE_SOUNDTIME--; + if ( CAMPFIRE_SOUNDTIME <= 0 ) + { + CAMPFIRE_SOUNDTIME = 480; + playSoundEntityLocal(my, 133, 128); + } +#endif + + // spew flame particles + if ( flickerLights ) + { + for ( int i = 0; i < 3; i++ ) + { + if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) + { + entity->x += ((local_rng.rand() % 30) - 10) / 10.f; + entity->y += ((local_rng.rand() % 30) - 10) / 10.f; + entity->z -= 1; + } + } + if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) + { + entity->z -= 2; + } + } + else + { + if ( ticks % TICKS_PER_SECOND == 0 ) + { + if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) + { + entity->z -= 2; + } + } + } + + // light environment + if ( !CAMPFIRE_LIGHTING ) + { + my->light = addLight(my->x / 16, my->y / 16, "campfire"); + CAMPFIRE_LIGHTING = 1; + } + if ( flickerLights ) + { + //Campfires will never flicker if this setting is disabled. + CAMPFIRE_FLICKER--; + } + if ( CAMPFIRE_FLICKER <= 0 ) + { + CAMPFIRE_LIGHTING = (CAMPFIRE_LIGHTING == 1) + 1; + + if ( CAMPFIRE_LIGHTING == 1 ) + { + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, "campfire"); + } + else + { + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, "campfire_flicker"); + } + CAMPFIRE_FLICKER = 2 + local_rng.rand() % 7; + } + } + else + { + my->removeLightField(); + my->light = NULL; + + my->stopEntitySound(); + } + + if ( multiplayer == CLIENT ) + { + return; + } + + auto& cauldronInteracting = my->skill[6]; + Entity* interacting = uidToEntity(cauldronInteracting); + + if ( cauldronInteracting > 0 ) + { + if ( !interacting || (entityDist(interacting, my) > TOUCHRANGE) ) + { + int playernum = -1; + if ( !interacting ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( achievementObserver.playerUids[i] == cauldronInteracting ) + { + playernum = i; + break; + } + } + } + else if ( interacting->behavior == &actPlayer ) + { + playernum = interacting->skill[2]; + } + cauldronInteracting = 0; + serverUpdateEntitySkill(my, 6); + if ( multiplayer == SERVER && playernum > 0 ) + { + strcpy((char*)net_packet->data, "CAUC"); + net_packet->data[4] = playernum; + SDLNet_Write32(my->getUID(), &net_packet->data[5]); + net_packet->address.host = net_clients[playernum - 1].host; + net_packet->address.port = net_clients[playernum - 1].port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, playernum - 1); + } + else if ( multiplayer == SINGLE || playernum == 0 ) + { + if ( playernum >= 0 && playernum < MAXPLAYERS ) + { + GenericGUI[playernum].alchemyGUI.closeAlchemyMenu(); + } + } + } + } + + // using + for ( int i = 0; i < MAXPLAYERS; i++ ) + { + if ( selectedEntity[i] == my || client_selected[i] == my ) + { + if ( inrange[i] && players[i]->entity ) + { + if ( cauldronInteracting != 0 ) + { + if ( Entity* interacting = uidToEntity(cauldronInteracting) ) + { + if ( interacting != players[i]->entity ) + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6975)); + } + } + } + else + { + cauldronInteracting = players[i]->entity->getUID(); + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(my, 6); + } + if ( players[i]->isLocalPlayer() ) + { + GenericGUI[i].openGUI(GUI_TYPE_ALCHEMY, my); + } + else if ( multiplayer == SERVER && i > 0 ) + { + strcpy((char*)net_packet->data, "CAUO"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + } + break; + } + } + } +} + +void actWorkbench(Entity* my) +{ + if ( !CAMPFIRE_INIT ) + { + CAMPFIRE_INIT = 1; + CAMPFIRE_HEALTH = MAXPLAYERS; + my->createWorldUITooltip(); + } + +// // crackling sounds +// if ( CAMPFIRE_HEALTH > 0 ) +// { +//#ifdef USE_FMOD +// if ( CAMPFIRE_SOUNDTIME == 0 ) +// { +// CAMPFIRE_SOUNDTIME--; +// my->stopEntitySound(); +// my->entity_sound = playSoundEntityLocal(my, 133, 32); +// } +// if ( my->entity_sound ) +// { +// bool playing = false; +// my->entity_sound->isPlaying(&playing); +// if ( !playing ) +// { +// my->entity_sound = nullptr; +// } +// } +//#else +// CAMPFIRE_SOUNDTIME--; +// if ( CAMPFIRE_SOUNDTIME <= 0 ) +// { +// CAMPFIRE_SOUNDTIME = 480; +// playSoundEntityLocal(my, 133, 128); +// } +//#endif +// +// // spew flame particles +// if ( flickerLights ) +// { +// for ( int i = 0; i < 3; i++ ) +// { +// if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) +// { +// entity->x += ((local_rng.rand() % 30) - 10) / 10.f; +// entity->y += ((local_rng.rand() % 30) - 10) / 10.f; +// entity->z -= 1; +// } +// } +// if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) +// { +// entity->z -= 2; +// } +// } +// else +// { +// if ( ticks % TICKS_PER_SECOND == 0 ) +// { +// if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) +// { +// entity->z -= 2; +// } +// } +// } +// +// // light environment +// if ( !CAMPFIRE_LIGHTING ) +// { +// my->light = addLight(my->x / 16, my->y / 16, "campfire"); +// CAMPFIRE_LIGHTING = 1; +// } +// if ( flickerLights ) +// { +// //Campfires will never flicker if this setting is disabled. +// CAMPFIRE_FLICKER--; +// } +// if ( CAMPFIRE_FLICKER <= 0 ) +// { +// CAMPFIRE_LIGHTING = (CAMPFIRE_LIGHTING == 1) + 1; +// +// if ( CAMPFIRE_LIGHTING == 1 ) +// { +// my->removeLightField(); +// my->light = addLight(my->x / 16, my->y / 16, "campfire"); +// } +// else +// { +// my->removeLightField(); +// my->light = addLight(my->x / 16, my->y / 16, "campfire_flicker"); +// } +// CAMPFIRE_FLICKER = 2 + local_rng.rand() % 7; +// } +// } +// else +// { +// my->removeLightField(); +// my->light = NULL; +// +// my->stopEntitySound(); +// } + + if ( multiplayer == CLIENT ) + { + return; + } + + auto& workbenchInteracting = my->skill[6]; + Entity* interacting = uidToEntity(workbenchInteracting); + + if ( workbenchInteracting > 0 ) + { + if ( !interacting || (entityDist(interacting, my) > TOUCHRANGE) ) + { + int playernum = -1; + if ( !interacting ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( achievementObserver.playerUids[i] == workbenchInteracting ) + { + playernum = i; + break; + } + } + } + else if ( interacting->behavior == &actPlayer ) + { + playernum = interacting->skill[2]; + } + workbenchInteracting = 0; + serverUpdateEntitySkill(my, 6); + if ( multiplayer == SERVER && playernum > 0 ) + { + strcpy((char*)net_packet->data, "WRKC"); + net_packet->data[4] = playernum; + SDLNet_Write32(my->getUID(), &net_packet->data[5]); + net_packet->address.host = net_clients[playernum - 1].host; + net_packet->address.port = net_clients[playernum - 1].port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, playernum - 1); + } + else if ( multiplayer == SINGLE || playernum == 0 ) + { + if ( playernum >= 0 && playernum < MAXPLAYERS ) + { + GenericGUI[playernum].tinkerGUI.closeTinkerMenu(); + } + } + } + } + + // using + for ( int i = 0; i < MAXPLAYERS; i++ ) + { + if ( selectedEntity[i] == my || client_selected[i] == my ) + { + if ( inrange[i] && players[i]->entity ) + { + if ( workbenchInteracting != 0 ) + { + if ( Entity* interacting = uidToEntity(workbenchInteracting) ) + { + if ( interacting != players[i]->entity ) + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6982)); + } + } + } + else + { + workbenchInteracting = players[i]->entity->getUID(); + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(my, 6); + } + if ( players[i]->isLocalPlayer() ) + { + GenericGUI[i].openGUI(GUI_TYPE_TINKERING, my); + } + else if ( multiplayer == SERVER && i > 0 ) + { + strcpy((char*)net_packet->data, "WRKO"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + } + break; + } + } + } +} + +void actMailbox(Entity* my) +{ + if ( !CAMPFIRE_INIT ) + { + CAMPFIRE_INIT = 1; + CAMPFIRE_HEALTH = MAXPLAYERS; + my->createWorldUITooltip(); + } + + if ( multiplayer == CLIENT ) + { + return; + } + + auto& mailboxInteracting = my->skill[6]; + Entity* interacting = uidToEntity(mailboxInteracting); + + if ( mailboxInteracting > 0 ) + { + if ( !interacting || (entityDist(interacting, my) > TOUCHRANGE) ) + { + int playernum = -1; + if ( !interacting ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( achievementObserver.playerUids[i] == mailboxInteracting ) + { + playernum = i; + break; + } + } + } + else if ( interacting->behavior == &actPlayer ) + { + playernum = interacting->skill[2]; + } + mailboxInteracting = 0; + serverUpdateEntitySkill(my, 6); + if ( multiplayer == SERVER && playernum > 0 ) + { + strcpy((char*)net_packet->data, "MBXC"); + net_packet->data[4] = playernum; + SDLNet_Write32(my->getUID(), &net_packet->data[5]); + net_packet->address.host = net_clients[playernum - 1].host; + net_packet->address.port = net_clients[playernum - 1].port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, playernum - 1); + } + else if ( multiplayer == SINGLE || playernum == 0 ) + { + if ( playernum >= 0 && playernum < MAXPLAYERS ) + { + GenericGUI[playernum].mailboxGUI.closeMailMenu(); + } + } + } + } + + // using + for ( int i = 0; i < MAXPLAYERS; i++ ) + { + if ( selectedEntity[i] == my || client_selected[i] == my ) + { + if ( inrange[i] && players[i]->entity ) + { + if ( mailboxInteracting != 0 ) + { + if ( Entity* interacting = uidToEntity(mailboxInteracting) ) + { + if ( interacting != players[i]->entity ) + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6987)); + } + } + } + else + { + mailboxInteracting = players[i]->entity->getUID(); + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(my, 6); + } + if ( players[i]->isLocalPlayer() ) + { + GenericGUI[i].openGUI(GUI_TYPE_MAILBOX, my); + } + else if ( multiplayer == SERVER && i > 0 ) + { + strcpy((char*)net_packet->data, "MBXO"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + } + break; + } + } + } +} \ No newline at end of file diff --git a/src/actchest.cpp b/src/actchest.cpp index 8bc1d071b..92b548739 100644 --- a/src/actchest.cpp +++ b/src/actchest.cpp @@ -153,15 +153,11 @@ void createChestInventory(Entity* my, int chestType) // itemnum = rng.rand() % NUMITEMS; //Keep trying until you don't get a spell or invalid item. //} //newItem(static_cast(itemnum), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - int cat = rng.rand() % (NUMCATEGORIES - 1); // exclude spell_cat + int cat = rng.rand() % (Category::CATEGORY_MAX - 2); // exclude spell_cat Item* currentItem = newItem(itemLevelCurve(static_cast(cat), 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); if ( currentItem ) { - if ( currentItem->type >= BRONZE_TOMAHAWK && currentItem->type <= CRYSTAL_SHURIKEN ) - { - // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) - currentItem->status = std::min(static_cast(DECREPIT + (currentItem->type - BRONZE_TOMAHAWK)), EXCELLENT); - } + itemLevelCurvePostProcess(my, currentItem, rng); } } break; @@ -170,6 +166,11 @@ void createChestInventory(Entity* my, int chestType) if ( rng.rand() % 2 ) { //Empty. + Item* item = newItem(itemLevelCurve(SCROLL, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } } else { @@ -180,7 +181,18 @@ void createChestInventory(Entity* my, int chestType) { if ( rng.rand() % 20 == 0 ) { - newItem(MASK_MOUTH_ROSE, static_cast(itemStatus), -1 + rng.rand() % 3, 1, rng.rand(), false, inventory); + if ( rng.rand() % 2 == 0 ) + { + Item* item = newItem(itemLevelCurve(SCROLL, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } + } + else + { + newItem(MASK_MOUTH_ROSE, static_cast(itemStatus), -1 + rng.rand() % 3, 1, rng.rand(), false, inventory); + } } else { @@ -196,7 +208,11 @@ void createChestInventory(Entity* my, int chestType) for ( i = 0; i < itemcount; ++i ) { //newItem(static_cast(FOOD_BREAD + (rng.rand() % 7)), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(FOOD, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(FOOD, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } } if ( rng.rand() % 10 == 0 ) { @@ -209,7 +225,7 @@ void createChestInventory(Entity* my, int chestType) break; case 3: //Treasures, jewelry, gems 'n stuff. - itemcount = (rng.rand() % 5) + 1; + itemcount = /*(rng.rand() % 5) +*/ 1; for ( i = 0; i < itemcount; ++i ) { if ( rng.rand() % 4 ) @@ -228,13 +244,37 @@ void createChestInventory(Entity* my, int chestType) { //Spawn a ring. //newItem(static_cast(RING_ADORNMENT + rng.rand() % 12), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(RING, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(RING, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } } else { //Spawn an amulet. //newItem(static_cast(AMULET_SEXCHANGE + rng.rand() % 6), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(AMULET, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(AMULET, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } + } + } + if ( rng.rand() % 4 > 0 ) // 75% + { + Item* item = newItem(SCROLL_IDENTIFY, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } + } + if ( rng.rand() % 4 == 0 ) // 25% + { + Item* item = newItem(SCROLL_REMOVECURSE, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); } } break; @@ -259,7 +299,11 @@ void createChestInventory(Entity* my, int chestType) //{ // newItem(CROSSBOW, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); //} - newItem(itemLevelCurve(WEAPON, minimumQuality, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(WEAPON, minimumQuality, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } } break; case 1: @@ -300,7 +344,11 @@ void createChestInventory(Entity* my, int chestType) //{ // newItem(static_cast(28 + rng.rand() % 10), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); //} - newItem(itemLevelCurve(ARMOR, minimumQuality, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(ARMOR, minimumQuality, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } } break; case 2: @@ -355,8 +403,16 @@ void createChestInventory(Entity* my, int chestType) // newItem(static_cast(28 + rng.rand() % 10), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); //} - newItem(itemLevelCurve(WEAPON, minimumQuality, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(ARMOR, minimumQuality, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(WEAPON, minimumQuality, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } + item = newItem(itemLevelCurve(ARMOR, minimumQuality, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } // try for thrown items. itemcount = 0 + rng.rand() % 2; @@ -365,11 +421,7 @@ void createChestInventory(Entity* my, int chestType) Item* thrown = newItem(itemLevelCurve(THROWN, minimumQuality, currentlevel, rng), WORN, 0, 3 + rng.rand() % 3, rng.rand(), false, inventory); if ( thrown ) { - if ( thrown->type >= BRONZE_TOMAHAWK && thrown->type <= CRYSTAL_SHURIKEN ) - { - // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) - thrown->status = std::min(static_cast(DECREPIT + (thrown->type - BRONZE_TOMAHAWK)), EXCELLENT); - } + itemLevelCurvePostProcess(my, thrown, rng); } } } @@ -420,11 +472,7 @@ void createChestInventory(Entity* my, int chestType) Item* thrown = newItem(itemLevelCurve(THROWN, minimumQuality, currentlevel, rng), WORN, 0, 3 + rng.rand() % 3, rng.rand(), false, inventory); if ( thrown ) { - if ( thrown->type >= BRONZE_TOMAHAWK && thrown->type <= CRYSTAL_SHURIKEN ) - { - // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) - thrown->status = std::min(static_cast(DECREPIT + (thrown->type - BRONZE_TOMAHAWK)), EXCELLENT); - } + itemLevelCurvePostProcess(my, thrown, rng); } } break; @@ -445,7 +493,7 @@ void createChestInventory(Entity* my, int chestType) * * Wizard's chest, which will contain 1-2 scrolls, a magic book, a staff, and either a wizard/magician/whatever implement of some sort or a piece of armor. */ int magic_type = rng.rand() % 4; - + bool doneFoci = false; switch ( magic_type ) { case 0: @@ -454,7 +502,11 @@ void createChestInventory(Entity* my, int chestType) for ( i = 0; i < itemcount; ++i ) { //newItem(static_cast(SCROLL_IDENTIFY + rng.rand() % 12), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(SCROLL, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(SCROLL, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } } if ( rng.rand() % 10 == 0 ) { @@ -478,14 +530,32 @@ void createChestInventory(Entity* my, int chestType) for ( i = 0; i < itemcount; ++i ) { //newItem(static_cast(SPELLBOOK_FORCEBOLT + rng.rand() % 22), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(SPELLBOOK, 0, currentlevel + 6, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(SPELLBOOK, 0, currentlevel + 6, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + int spell_level = currentlevel + 6; + //if ( spell_level >= 15 ) + //{ + // if ( rng.rand() % 8 == 0 ) // some lower level spells + // { + // spell_level = 0 + 5 * rng.rand() % 3; + // } + //} + itemLevelCurvePostProcess(my, item, rng, spell_level); + } } break; case 2: + { //A staff. //newItem(static_cast(MAGICSTAFF_LIGHT + rng.rand() % 10), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(MAGICSTAFF, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(MAGICSTAFF, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } break; + } case 3: //So spawn several items at once. A wizard's chest! @@ -494,14 +564,73 @@ void createChestInventory(Entity* my, int chestType) for ( i = 0; i < itemcount; ++i ) { //newItem(static_cast(SCROLL_IDENTIFY + rng.rand() % 12), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(SCROLL, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(SCROLL, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } } //newItem(static_cast(SPELLBOOK_FORCEBOLT + rng.rand() % 22), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(SPELLBOOK, 0, currentlevel + 6, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(SPELLBOOK, 0, currentlevel + 6, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + int spell_level = currentlevel + 6; + //if ( spell_level >= 15 ) + //{ + // if ( rng.rand() % 8 == 0 ) // some lower level spells + // { + // spell_level = 0 + 5 * rng.rand() % 3; + // } + //} + itemLevelCurvePostProcess(my, item, rng, spell_level); + } //newItem(static_cast(MAGICSTAFF_LIGHT + rng.rand() % 10), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(MAGICSTAFF, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - switch ( rng.rand() % 9 ) + item = newItem(itemLevelCurve(MAGICSTAFF, 0, currentlevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } + + if ( rng.rand() % 50 == 0 ) + { + int limit = 5; + if ( rng.rand() % 10 == 0 ) + { + // no limit + } + else if ( currentlevel <= 8 ) + { + limit = 2; + } + else if ( currentlevel <= 12 ) + { + limit = 2; + } + switch ( rng.rand() % limit ) + { + case 0: + newItem(TOOL_FOCI_FIRE, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + case 1: + newItem(TOOL_FOCI_SNOW, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + case 2: + newItem(TOOL_FOCI_SAND, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + case 3: + newItem(TOOL_FOCI_ARCS, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + case 4: + newItem(TOOL_FOCI_NEEDLES, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + default: + break; + } + doneFoci = true; + } + + switch ( rng.rand() % 10 ) { case 0: //A cloak. Item 24. @@ -547,6 +676,32 @@ void createChestInventory(Entity* my, int chestType) case 8: newItem(HAT_HEADDRESS, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); break; + case 9: + if ( !doneFoci ) + { + switch ( rng.rand() % 5 ) + { + case 0: + newItem(TOOL_FOCI_FIRE, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + case 1: + newItem(TOOL_FOCI_SNOW, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + case 2: + newItem(TOOL_FOCI_SAND, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + case 3: + newItem(TOOL_FOCI_ARCS, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + case 4: + newItem(TOOL_FOCI_NEEDLES, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + break; + default: + break; + } + doneFoci = true; + } + break; default: break; } @@ -561,7 +716,11 @@ void createChestInventory(Entity* my, int chestType) for ( i = 0; i < itemcount; ++i ) { //newItem(static_cast(POTION_WATER + (rng.rand() % 15)), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); - newItem(itemLevelCurve(POTION, 0, currentlevel + 7, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + Item* item = newItem(itemLevelCurve(POTION, 0, currentlevel + 7, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), false, inventory); + if ( item ) + { + itemLevelCurvePostProcess(my, item, rng); + } } if ( rng.rand() % 8 == 0 ) { @@ -652,6 +811,35 @@ void Entity::actChest() this->createWorldUITooltip(); } + if ( sprite == 1791 ) + { + if ( chestVoidState == 0 ) + { + sprite = 188; + if ( parent != 0 ) + { + if ( Entity* lid = uidToEntity(parent) ) + { + lid->sprite = 216; + } + } + } + } + else if ( sprite == 188 ) + { + if ( chestVoidState != 0 ) + { + sprite = 1791; + if ( parent != 0 ) + { + if ( Entity* lid = uidToEntity(parent) ) + { + lid->sprite = 1790; + } + } + } + } + if ( multiplayer == CLIENT ) { if ( chestHasVampireBook ) @@ -704,7 +892,7 @@ void Entity::actChest() } } - list_t* inventory = static_cast(children.first->element); + list_t* inventory = getChestInventoryList(); node_t* node = NULL; Item* item = NULL; @@ -712,17 +900,31 @@ void Entity::actChest() if ( chestHealth <= 0 ) { + if ( chestVoidState > 0 ) + { + chestVoidState = 0; + createParticleErupt(this, 625); + serverSpawnMiscParticles(this, PARTICLE_EFFECT_ERUPT, 625); + } auto& rng = entity_rng ? *entity_rng : local_rng; // the chest busts open, drops some items randomly, then destroys itself. node_t* nextnode; - for ( node = inventory->first; node != NULL; node = nextnode ) + if ( chestVoidState == 0 ) { - nextnode = node->next; - item = (Item*)node->element; - if ( rng.rand() % 2 == 0 ) + for ( node = inventory->first; node != NULL; node = nextnode ) { - dropItemMonster(item, this, NULL); + nextnode = node->next; + item = (Item*)node->element; + if ( rng.rand() % 2 == 0 || (item && item->type >= WOODEN_SHIELD && item->type < NUMITEMS + && (items[item->type].hasAttribute("UNVOIDABLE") + || item->type == KEY_IRON + || item->type == KEY_BRONZE + || item->type == KEY_SILVER + || item->type == KEY_GOLD)) ) + { + dropItemMonster(item, this, NULL); + } } } @@ -807,6 +1009,21 @@ void Entity::actChest() } } + if ( chestVoidState > 0 ) + { + --chestVoidState; + if ( chestStatus == 1 ) + { + chestVoidState = std::max(1, chestVoidState); + } + if ( chestVoidState == 0 ) + { + serverUpdateEntitySkill(this, 17); + createParticleErupt(this, 625); + serverSpawnMiscParticles(this, PARTICLE_EFFECT_ERUPT, 625); + } + } + //Using the chest (TODO: Monsters using it?). int chestclicked = -1; for (int i = 0; i < MAXPLAYERS; ++i) @@ -830,47 +1047,67 @@ void Entity::actChest() { if ( !chestStatus ) { - messagePlayer(chestclicked, MESSAGE_INTERACTION, Language::get(459)); - openedChest[chestclicked] = this; - - Compendium_t::Events_t::eventUpdateWorld(chestclicked, Compendium_t::CPDM_CHESTS_OPENED, "chest", 1); + bool voidChestInUse = false; + if ( chestVoidState != 0 ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( openedChest[i] && openedChest[i]->chestVoidState != 0 ) + { + voidChestInUse = true; + } + } + } - chestOpener = chestclicked; - if ( !players[chestclicked]->isLocalPlayer() && multiplayer == SERVER) + if ( voidChestInUse ) { - //Send all of the items to the client. - strcpy((char*)net_packet->data, "CHST"); //Chest. - SDLNet_Write32((Uint32)getUID(), &net_packet->data[4]); //Give the client the UID. - net_packet->address.host = net_clients[chestclicked - 1].host; - net_packet->address.port = net_clients[chestclicked - 1].port; - net_packet->len = 8; - sendPacketSafe(net_sock, -1, net_packet, chestclicked - 1); - for (node = inventory->first; node != NULL; node = node->next) + messagePlayer(chestclicked, MESSAGE_INTERACTION, Language::get(6565)); + } + else + { + messagePlayer(chestclicked, MESSAGE_INTERACTION, Language::get(459)); + openedChest[chestclicked] = this; + + Compendium_t::Events_t::eventUpdateWorld(chestclicked, Compendium_t::CPDM_CHESTS_OPENED, "chest", 1); + + chestOpener = chestclicked; + if ( !players[chestclicked]->isLocalPlayer() && multiplayer == SERVER) { - item = (Item*) node->element; - strcpy((char*)net_packet->data, "CITM"); //Chest item. - SDLNet_Write32((Uint32)item->type, &net_packet->data[4]); - SDLNet_Write32((Uint32)item->status, &net_packet->data[8]); - SDLNet_Write32((Uint32)item->beatitude, &net_packet->data[12]); - SDLNet_Write32((Uint32)item->count, &net_packet->data[16]); - SDLNet_Write32((Uint32)item->appearance, &net_packet->data[20]); - net_packet->data[24] = item->identified; - net_packet->data[25] = 1; //forceNewStack ? 1 : 0; - net_packet->data[26] = (Sint8)item->x; - net_packet->data[27] = (Sint8)item->y; + //Send all of the items to the client. + strcpy((char*)net_packet->data, "CHST"); //Chest. + SDLNet_Write32((Uint32)getUID(), &net_packet->data[4]); //Give the client the UID. + net_packet->data[8] = chestVoidState != 0 ? 1 : 0; net_packet->address.host = net_clients[chestclicked - 1].host; net_packet->address.port = net_clients[chestclicked - 1].port; - net_packet->len = 28; + net_packet->len = 9; sendPacketSafe(net_sock, -1, net_packet, chestclicked - 1); + for (node = inventory->first; node != NULL; node = node->next) + { + item = (Item*) node->element; + strcpy((char*)net_packet->data, "CITM"); //Chest item. + SDLNet_Write32((Uint32)item->type, &net_packet->data[4]); + SDLNet_Write32((Uint32)item->status, &net_packet->data[8]); + SDLNet_Write32((Uint32)item->beatitude, &net_packet->data[12]); + SDLNet_Write32((Uint32)item->count, &net_packet->data[16]); + SDLNet_Write32((Uint32)item->appearance, &net_packet->data[20]); + net_packet->data[24] = item->identified; + net_packet->data[25] = 1; //forceNewStack ? 1 : 0; + net_packet->data[26] = (Sint8)item->x; + net_packet->data[27] = (Sint8)item->y; + net_packet->address.host = net_clients[chestclicked - 1].host; + net_packet->address.port = net_clients[chestclicked - 1].port; + net_packet->len = 28; + sendPacketSafe(net_sock, -1, net_packet, chestclicked - 1); + } } + else + { + players[chestclicked]->openStatusScreen(GUI_MODE_INVENTORY, INVENTORY_MODE_ITEM); // Reset the GUI to the inventory. + players[chestclicked]->GUI.activateModule(Player::GUI_t::MODULE_CHEST); + players[chestclicked]->inventoryUI.chestGUI.openChest(chestVoidState != 0); + } + chestStatus = 1; //Toggle chest open/closed. } - else - { - players[chestclicked]->openStatusScreen(GUI_MODE_INVENTORY, INVENTORY_MODE_ITEM); // Reset the GUI to the inventory. - players[chestclicked]->GUI.activateModule(Player::GUI_t::MODULE_CHEST); - players[chestclicked]->inventoryUI.chestGUI.openChest(); - } - chestStatus = 1; //Toggle chest open/closed. } else { @@ -1070,6 +1307,44 @@ void Entity::closeChestServer() } } +Item* Entity::addItemToVoidChest(int player, Item* item, bool forceNewStack, Item* specificDestinationStack) +{ + if ( !item ) + { + return nullptr; + } + if ( player < 0 || player >= MAXPLAYERS ) + { + return nullptr; + } + if ( multiplayer == CLIENT ) + { + if ( players[player]->isLocalPlayer() ) + { + //Tell the server. + strcpy((char*)net_packet->data, "CITM"); + net_packet->data[4] = player; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + SDLNet_Write32((Uint32)item->type, &net_packet->data[5]); + SDLNet_Write32((Uint32)item->status, &net_packet->data[9]); + SDLNet_Write32((Uint32)item->beatitude, &net_packet->data[13]); + SDLNet_Write32((Uint32)item->count, &net_packet->data[17]); + SDLNet_Write32((Uint32)item->appearance, &net_packet->data[21]); + net_packet->data[25] = item->identified; + net_packet->data[26] = forceNewStack ? 1 : 0; + net_packet->data[27] = 1; + net_packet->len = 28; + sendPacketSafe(net_sock, -1, net_packet, 0); + + return item; + } + return nullptr; + } + + return addItemToVoidChestServer(player, item, forceNewStack, specificDestinationStack); +} + Item* Entity::addItemToChest(Item* item, bool forceNewStack, Item* specificDestinationStack) { if (!item) @@ -1095,7 +1370,8 @@ Item* Entity::addItemToChest(Item* item, bool forceNewStack, Item* specificDesti SDLNet_Write32((Uint32)item->appearance, &net_packet->data[21]); net_packet->data[25] = item->identified; net_packet->data[26] = forceNewStack ? 1 : 0; - net_packet->len = 27; + net_packet->data[27] = players[player]->inventoryUI.chestGUI.voidChest ? 1 : 0; + net_packet->len = 28; sendPacketSafe(net_sock, -1, net_packet, 0); return addItemToChestClientside(player, item, forceNewStack, specificDestinationStack); @@ -1104,7 +1380,7 @@ Item* Entity::addItemToChest(Item* item, bool forceNewStack, Item* specificDesti Item* item2 = NULL; //Add the item to the chest's inventory. - list_t* inventory = static_cast(children.first->element); + list_t* inventory = getChestInventoryList(); node_t* t_node = NULL; if ( !forceNewStack ) @@ -1200,6 +1476,7 @@ Item* Entity::addItemToChestFromInventory(int player, Item* item, int amount, bo } } item->identified = true; + Item::onItemIdentified(player, item); return nullptr; } } @@ -1357,7 +1634,9 @@ Item* Entity::getItemFromChest(Item* item, int amount, bool getInfoOnly) SDLNet_Write32((Uint32)count, &net_packet->data[17]); SDLNet_Write32((Uint32)item->appearance, &net_packet->data[21]); net_packet->data[25] = item->identified; - net_packet->len = 26; + net_packet->data[26] = 0; + net_packet->data[27] = players[player]->inventoryUI.chestGUI.voidChest ? 1 : 0; + net_packet->len = 28; sendPacketSafe(net_sock, -1, net_packet, 0); } } @@ -1371,7 +1650,7 @@ Item* Entity::getItemFromChest(Item* item, int amount, bool getInfoOnly) { return NULL; } - if ( item->node->list != children.first->element ) + if ( item->node->list != getChestInventoryList() ) { return NULL; } @@ -1420,6 +1699,29 @@ void closeChestClientside(const int player) } } +list_t* Entity::getChestInventoryList() +{ + if ( multiplayer == CLIENT ) + { + return nullptr; + } + if ( behavior == &::actChest ) + { + if ( chestVoidState != 0 ) + { + return &stats[0]->void_chest_inventory; + } + else + { + if ( children.first && children.first->element ) + { + return (list_t*)children.first->element; + } + } + } + return nullptr; +} + Item* addItemToChestClientside(const int player, Item* item, bool forceNewStack, Item* specificDestinationStack) { if (openedChest[player]) @@ -1465,6 +1767,188 @@ Item* addItemToChestClientside(const int player, Item* item, bool forceNewStack, return nullptr; } +Item* Entity::addItemToVoidChestServer(int player, Item* item, bool forceNewStack, Item* specificDestinationStack) +{ + if ( !item ) + { + return nullptr; + } + + Item* item2 = NULL; + node_t* t_node = NULL; + + //Add the item to the chest's inventory. + list_t* inventory = &stats[0]->void_chest_inventory; + + if ( !inventory ) + { + return nullptr; + } + + bool voidChestInUse = false; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( openedChest[i] && openedChest[i]->chestVoidState != 0 && i != player ) + { + voidChestInUse = true; + break; + } + } + if ( voidChestInUse ) // someone already has a void chest open + { + if ( player >= 1 && player < MAXPLAYERS ) + { + bool dropped = false; + if ( players[player]->entity ) + { + auto item2 = newItem(item->type, + item->status, + item->beatitude, + item->count, + item->appearance, + item->identified, + &stats[player]->inventory); + dropped = dropItem(item2, player, true, true); + } + + if ( !dropped ) + { + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. + entity->flags[INVISIBLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->x = players[player]->player_last_x; + entity->y = players[player]->player_last_y; + entity->sizex = 4; + entity->sizey = 4; + entity->yaw = local_rng.rand() % 360 * (PI / 180.0); + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->vel_z = (-10 - local_rng.rand() % 20) * .01; + entity->flags[PASSABLE] = true; + entity->behavior = &actItem; + entity->skill[10] = item->type; + entity->skill[11] = item->status; + entity->skill[12] = item->beatitude; + entity->skill[13] = item->count; + entity->skill[14] = item->appearance; + entity->skill[15] = item->identified; + entity->parent = 0; + entity->itemOriginalOwner = 0; + + playSoundPos(players[player]->player_last_x, players[player]->player_last_y, 47 + local_rng.rand() % 3, 64); + } + } + messagePlayer(player, MESSAGE_INVENTORY, Language::get(6565)); + return nullptr; + } + + int originalQty = item->count; + if ( !forceNewStack ) + { + while ( item->count > 0 ) + { + bool anyItemsInserted = false; + + //If item's already in the chest, add it to a pre-existing stack. + for ( t_node = inventory->first; t_node != NULL; t_node = t_node->next ) + { + item2 = (Item*)t_node->element; + if ( !specificDestinationStack ) + { + if ( !itemCompare(item, item2, false) ) + { + if ( item2->shouldItemStack(player) ) + { + int stackAmount = std::max(0, item2->getMaxStackLimit(player) - item2->count); + int qty = std::max(0, std::min((int)item->count, stackAmount)); + anyItemsInserted = qty > 0; + item2->count += qty; + item->count -= qty; + } + if ( item->count <= 0 ) + { + return item2; + } + } + } + else + { + if ( specificDestinationStack == item2 && item2->shouldItemStack(player) ) + { + int stackAmount = std::max(0, item2->getMaxStackLimit(player) - item2->count); + int qty = std::max(0, std::min((int)item->count, stackAmount)); + anyItemsInserted = qty > 0; + item2->count += qty; + item->count -= qty; + if ( item->count <= 0 ) + { + return item2; + } + } + } + } + + if ( !anyItemsInserted ) + { + break; + } + } + } + + bool voidChestFull = list_Size(inventory) >= Player::Inventory_t::MAX_CHEST_X * Player::Inventory_t::MAX_CHEST_Y; + if ( voidChestFull ) // void chest is full + { + bool dropped = false; + if ( player >= 1 && player < MAXPLAYERS ) + { + auto item2 = newItem(item->type, + item->status, + item->beatitude, + item->count, + item->appearance, + item->identified, + &stats[player]->inventory); + dropped = dropItem(item2, player, true, true); + + if ( !dropped ) + { + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. + entity->flags[INVISIBLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->x = players[player]->player_last_x; + entity->y = players[player]->player_last_y; + entity->sizex = 4; + entity->sizey = 4; + entity->yaw = local_rng.rand() % 360 * (PI / 180.0); + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->vel_z = (-10 - local_rng.rand() % 20) * .01; + entity->flags[PASSABLE] = true; + entity->behavior = &actItem; + entity->skill[10] = item->type; + entity->skill[11] = item->status; + entity->skill[12] = item->beatitude; + entity->skill[13] = item->count; + entity->skill[14] = item->appearance; + entity->skill[15] = item->identified; + entity->parent = 0; + entity->itemOriginalOwner = 0; + + playSoundPos(players[player]->player_last_x, players[player]->player_last_y, 47 + local_rng.rand() % 3, 64); + } + } + + messagePlayer(player, MESSAGE_INVENTORY, Language::get(6566)); + return nullptr; + } + + item->node = list_AddNodeLast(inventory); + item->node->element = item; + item->node->deconstructor = &defaultDeconstructor; + + return item; +} + Item* Entity::addItemToChestServer(Item* item, bool forceNewStack, Item* specificDestinationStack) { if (!item) @@ -1476,7 +1960,7 @@ Item* Entity::addItemToChestServer(Item* item, bool forceNewStack, Item* specifi node_t* t_node = NULL; //Add the item to the chest's inventory. - list_t* inventory = static_cast(children.first->element); + list_t* inventory = getChestInventoryList(); if (!inventory) { @@ -1514,6 +1998,63 @@ Item* Entity::addItemToChestServer(Item* item, bool forceNewStack, Item* specifi return item; } +bool Entity::removeItemFromVoidChestServer(int player, Item* item, int count) +{ + if ( !item ) + { + return false; + } + + Item* item2 = NULL; + node_t* t_node = NULL; + + list_t* inventory = &stats[0]->void_chest_inventory; + if ( !inventory ) + { + return false; + } + + node_t* nextnode = nullptr; + bool removedItems = false; + for ( t_node = inventory->first; t_node != NULL; t_node = nextnode ) + { + nextnode = t_node->next; + item2 = (Item*)t_node->element; + if ( !item2 || !item2->node || item2->node->list != inventory ) + { + return false; + } + if ( !itemCompare(item, item2, false, false) ) + { + if ( count < item2->count ) + { + //Grab only one item from the chest. + int oldcount = item2->count; + item2->count = oldcount - count; + if ( item2->count <= 0 ) + { + list_RemoveNode(item2->node); + } + } + else if ( count == item2->count ) + { + //Grab all items from the chest. + list_RemoveNode(item2->node); + } + else if ( count > item2->count ) + { + count -= item2->count; + //Grab items that we can, and then look for more. + list_RemoveNode(item2->node); + removedItems = true; + continue; + } + return true; + } + } + return removedItems; +} + bool Entity::removeItemFromChestServer(Item* item, int count) { if (!item) @@ -1524,7 +2065,10 @@ bool Entity::removeItemFromChestServer(Item* item, int count) Item* item2 = NULL; node_t* t_node = NULL; - list_t* inventory = static_cast(children.first->element); + Sint32 oldVoidChestState = chestVoidState; + chestVoidState = 0; + list_t* inventory = getChestInventoryList(); + chestVoidState = oldVoidChestState; if (!inventory) { return false; @@ -1536,7 +2080,7 @@ bool Entity::removeItemFromChestServer(Item* item, int count) { nextnode = t_node->next; item2 = (Item*) t_node->element; - if (!item2 || !item2->node || item2->node->list != children.first->element) + if (!item2 || !item2->node || item2->node->list != inventory ) { return false; } @@ -1582,8 +2126,10 @@ void Entity::lockChest() chestLocked = 1; } -void Entity::chestHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster) +void Entity::chestHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster, bool doSound) { + updateEntityOldHPBeforeMagicHit(*this, magicProjectile); + if ( behavior == &actMonster ) { Stat* stats = getStats(); @@ -1610,7 +2156,7 @@ void Entity::chestHandleDamageMagic(int damage, Entity &magicProjectile, Entity } if ( caster && oldHP > 0 ) { - awardXP(caster, true, true); + caster->awardXP(this, true, true); } } else @@ -1657,6 +2203,11 @@ void Entity::chestHandleDamageMagic(int damage, Entity &magicProjectile, Entity messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(2520)); } Compendium_t::Events_t::eventUpdateWorld(caster->skill[2], Compendium_t::CPDM_CHESTS_DESTROYED, "chest", 1); + + if ( chestOldHealth > 0 ) + { + players[caster->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_COMMON, this); + } } else { @@ -1674,5 +2225,8 @@ void Entity::chestHandleDamageMagic(int damage, Entity &magicProjectile, Entity false, DamageGib::DMG_DEFAULT); } } - playSoundEntity(this, 28, 128); + if ( doSound ) + { + playSoundEntity(this, 28, 128); + } } diff --git a/src/actdoor.cpp b/src/actdoor.cpp index 73a0f551c..811457a01 100644 --- a/src/actdoor.cpp +++ b/src/actdoor.cpp @@ -253,7 +253,7 @@ void actDoor(Entity* my) } bool insideEntity = false; - if ( entity->behavior == &actDoor ) + if ( entity->behavior == &actDoor || entity->behavior == &actIronDoor ) { real_t oldx = entity->x; real_t oldy = entity->y; @@ -329,8 +329,13 @@ void actDoorFrame(Entity* my) } } -void Entity::doorHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster) +void Entity::doorHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster, bool messages, bool doSound) { + if ( behavior == &::actIronDoor ) + { + damage = 0; + } + updateEntityOldHPBeforeMagicHit(*this, magicProjectile); doorHealth -= damage; //Decrease door health. if ( caster ) { @@ -338,30 +343,44 @@ void Entity::doorHandleDamageMagic(int damage, Entity &magicProjectile, Entity * { if ( doorHealth <= 0 ) { - if ( magicProjectile.behavior == &actBomb ) + if ( messages ) { - messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(3617), items[magicProjectile.skill[21]].getIdentifiedName(), Language::get(674)); + if ( magicProjectile.behavior == &actBomb ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(3617), items[magicProjectile.skill[21]].getIdentifiedName(), + behavior == &::actIronDoor ? Language::get(6414) : Language::get(674)); + } + else + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(387)); + } } - else + Compendium_t::Events_t::eventUpdateWorld(caster->skill[2], Compendium_t::CPDM_DOOR_BROKEN, "door", 1); + + if ( doorOldHealth > 0 ) { - messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(387)); + players[caster->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_COMMON, this); } - Compendium_t::Events_t::eventUpdateWorld(caster->skill[2], Compendium_t::CPDM_DOOR_BROKEN, "door", 1); } - else + else if ( damage > 0 ) { - if ( magicProjectile.behavior == &actBomb ) - { - messagePlayer(caster->skill[2], MESSAGE_COMBAT_BASIC, Language::get(3618), items[magicProjectile.skill[21]].getIdentifiedName(), Language::get(674)); - } - else + if ( messages ) { - messagePlayer(caster->skill[2], MESSAGE_COMBAT_BASIC, Language::get(378), Language::get(674)); + if ( magicProjectile.behavior == &actBomb ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT_BASIC, Language::get(3618), items[magicProjectile.skill[21]].getIdentifiedName(), + behavior == &::actIronDoor ? Language::get(6414) : Language::get(674)); + } + else + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT_BASIC, Language::get(378), + behavior == &::actIronDoor ? Language::get(6414) : Language::get(674)); + } } } - updateEnemyBar(caster, this, Language::get(674), doorHealth, doorMaxHealth, - false, DamageGib::DMG_DEFAULT); } + updateEnemyBar(caster, this, behavior == &::actIronDoor ? Language::get(6414) : Language::get(674), doorHealth, doorMaxHealth, + false, DamageGib::DMG_DEFAULT); } if ( !doorDir ) { @@ -371,6 +390,321 @@ void Entity::doorHandleDamageMagic(int damage, Entity &magicProjectile, Entity * { doorSmacked = (magicProjectile.y < this->y); } + if ( doSound ) + { + playSoundEntity(this, 28, 128); + } +} + +void actIronDoor(Entity* my) +{ + if ( my ) + { + my->actIronDoor(); + } +} + +void Entity::actIronDoor() +{ + Entity* entity; + int i, c; + + auto& rng = entity_rng ? *entity_rng : local_rng; + + if ( !doorInit ) + { + createWorldUITooltip(); + + doorInit = 1; + doorStartAng = yaw; + doorHealth = 100; + doorMaxHealth = doorHealth; + doorOldHealth = doorHealth; + doorPreventLockpickExploit = 1; + doorLockpickHealth = 50; + if ( doorForceLockedUnlocked == 2 ) + { + doorLocked = 0; // force unlocked. + } + else if ( doorForceLockedUnlocked <= 1 ) + { + doorLocked = 1; + doorPreventLockpickExploit = 0; + } + doorOldStatus = doorStatus; + scalex = 1.01; + scaley = 1.01; + scalez = 1.01; + flags[BURNABLE] = false; + } + else + { + if ( multiplayer != CLIENT ) + { + doorOldHealth = doorHealth; + + // door mortality :p + if ( doorHealth <= 0 ) + { + for ( c = 0; c < 5 && false; c++ ) + { + entity = spawnGib(this); + entity->flags[INVISIBLE] = false; + entity->sprite = 187; // Splinter.vox + entity->x = floor(x / 16) * 16 + 8; + entity->y = floor(y / 16) * 16 + 8; + entity->z = 0; + entity->z += -7 + local_rng.rand() % 14; + if ( !doorDir ) + { + // horizontal door + entity->y += -4 + local_rng.rand() % 8; + if ( doorSmacked ) + { + entity->yaw = PI; + } + else + { + entity->yaw = 0; + } + } + else + { + // vertical door + entity->x += -4 + local_rng.rand() % 8; + if ( doorSmacked ) + { + entity->yaw = PI / 2; + } + else + { + entity->yaw = 3 * PI / 2; + } + } + entity->pitch = (local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + entity->vel_x = cos(entity->yaw) * (1.2 + (local_rng.rand() % 10) / 50.0); + entity->vel_y = sin(entity->yaw) * (1.2 + (local_rng.rand() % 10) / 50.0); + entity->vel_z = -.25; + entity->fskill[3] = 0.04; + serverSpawnGibForClient(entity); + } + playSoundEntity(this, 76, 64); + list_RemoveNode(mynode); + return; + } - playSoundEntity(this, 28, 128); + if ( doorUnlockWhenPowered == 1 ) + { + if ( circuit_status == CIRCUIT_ON ) + { + if ( doorLocked == 1 ) + { + doorLocked = 0; + playSoundEntity(this, 91, 64); + } + } + else if ( circuit_status == CIRCUIT_OFF ) + { + if ( doorStatus == 0 ) // closed + { + if ( doorLocked == 0 ) + { + doorLocked = 1; + playSoundEntity(this, 57, 64); + } + } + } + } + + // using door + for ( i = 0; i < MAXPLAYERS; i++ ) + { + if ( selectedEntity[i] == this || client_selected[i] == this ) + { + if ( Player::getPlayerInteractEntity(i) && inrange[i] ) + { + Entity* playerEntity = Player::getPlayerInteractEntity(i); + if ( !doorLocked ) // door unlocked + { + if ( !doorDir && !doorStatus ) + { + // open door + doorStatus = 1 + (playerEntity->x > x); + playSoundEntity(this, 21, 96); + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6404)); + Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_DOOR_OPENED, "iron door", 1); + } + else if ( doorDir && !doorStatus ) + { + // open door + doorStatus = 1 + (playerEntity->y < y); + playSoundEntity(this, 21, 96); + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6404)); + Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_DOOR_OPENED, "iron door", 1); + } + else + { + // close door + doorStatus = 0; + playSoundEntity(this, 22, 96); + if ( doorUnlockWhenPowered == 1 && circuit_status == CIRCUIT_OFF ) + { + doorLocked = 1; + playSoundEntity(this, 57, 64); + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6406)); + } + else + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6405)); + } + Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_DOOR_CLOSED, "iron door", 1); + } + } + else + { + // door locked + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6407)); + playSoundEntity(this, 152, 64); + } + } + } + } + } + + // door swinging + static const real_t doorSpeed = 0.15; + if ( !doorStatus ) + { + // closing door + if ( yaw > doorStartAng ) + { + yaw = std::max(doorStartAng, yaw - 0.15); + } + else if ( yaw < doorStartAng ) + { + yaw = std::min(doorStartAng, yaw + 0.15); + } + } + else + { + // opening door + if ( doorStatus == 1 ) + { + if ( yaw > doorStartAng + PI / 2 ) + { + yaw = std::max(doorStartAng + PI / 2, yaw - doorSpeed); + } + else if ( yaw < doorStartAng + PI / 2 ) + { + yaw = std::min(doorStartAng + PI / 2, yaw + doorSpeed); + } + } + else if ( doorStatus == 2 ) + { + if ( yaw > doorStartAng - PI / 2 ) + { + yaw = std::max(doorStartAng - PI / 2, yaw - doorSpeed); + } + else if ( yaw < doorStartAng - PI / 2 ) + { + yaw = std::min(doorStartAng - PI / 2, yaw + doorSpeed); + } + } + } + + // setting collision + if ( yaw == doorStartAng && flags[PASSABLE] ) + { + // don't set impassable if someone's inside, otherwise do + node_t* node; + bool somebodyinside = false; + std::vector entLists; + if ( multiplayer == CLIENT ) + { + entLists.push_back(map.entities); // clients use old map.entities method + } + else + { + entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 2); + } + real_t oldmyx = x; + real_t oldmyy = y; + x = (static_cast(x) >> 4) * 16.0 + 8.0; // door positioning isn't centred on tile so adjust + y = (static_cast(y) >> 4) * 16.0 + 8.0; + for ( std::vector::iterator it = entLists.begin(); it != entLists.end() && !somebodyinside; ++it ) + { + list_t* currentList = *it; + for ( node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity == this || (entity->flags[PASSABLE] && entity->behavior != &actDeathGhost) + || entity->behavior == &actDoorFrame ) + { + continue; + } + + bool insideEntity = false; + if ( entity->behavior == &actDoor || entity->behavior == &::actIronDoor ) + { + real_t oldx = entity->x; + real_t oldy = entity->y; + entity->x = (static_cast(entity->x) >> 4) * 16.0 + 8.0; // door positioning isn't centred on tile so adjust + entity->y = (static_cast(entity->y) >> 4) * 16.0 + 8.0; + insideEntity = entityInsideEntity(this, entity); + entity->x = oldx; + entity->y = oldy; + } + else + { + insideEntity = entityInsideEntity(this, entity); + } + + if ( insideEntity ) + { + somebodyinside = true; + break; + } + } + } + x = oldmyx; + y = oldmyy; + if ( !somebodyinside ) + { + focaly = 0; + if ( doorStartAng == 0 ) + { + y -= 5; + } + else + { + x -= 5; + } + flags[PASSABLE] = false; + } + } + else if ( yaw != doorStartAng && !flags[PASSABLE] ) + { + focaly = -5; + if ( doorStartAng == 0 ) + { + y += 5; + } + else + { + x += 5; + } + flags[PASSABLE] = true; + } + + // update for clients + if ( multiplayer == SERVER ) + { + if ( doorOldStatus != doorStatus ) + { + doorOldStatus = doorStatus; + serverUpdateEntitySkill(this, 3); + } + } + } } diff --git a/src/actflame.cpp b/src/actflame.cpp index 1d6da76f2..f8e6ce784 100644 --- a/src/actflame.cpp +++ b/src/actflame.cpp @@ -83,6 +83,65 @@ void actFlame(Entity* my) static ConsoleVariable cvar_flame_use_vismap("/flame_use_vismap", true); +Entity* spawnFlameSprites(Entity* parentent, Sint32 sprite) +{ + if ( !parentent ) + { + return nullptr; + } + /*if ( *cvar_flame_use_vismap && !intro ) + { + if ( parentent->behavior != actPlayer + && parentent->behavior != actPlayerLimb + && !parentent->flags[OVERDRAW] + && !parentent->flags[GENIUS] ) + { + int x = parentent->x / 16.0; + int y = parentent->y / 16.0; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + bool anyVismap = false; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] && players[i]->isLocalPlayer() ) + { + if ( cameras[i].vismap && cameras[i].vismap[y + x * map.height] ) + { + anyVismap = true; + break; + } + } + } + if ( !anyVismap ) + { + return nullptr; + } + } + } + }*/ + + if ( Entity* fx = createParticleAestheticOrbit(parentent, sprite, 10, PARTICLE_EFFECT_FLAMES_BURNING) ) + { + fx->flags[SPRITE] = true; + fx->flags[INVISIBLE] = true; + fx->x = parentent->x; + fx->y = parentent->y; + fx->z = parentent->z; + fx->scalex = 1.0; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->vel_z = -0.25; + fx->actmagicOrbitDist = 0; + fx->fskill[2] = parentent->yaw + (local_rng.rand() % 8) * PI / 4.0; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + return fx; + } + return nullptr; +} + Entity* spawnFlame(Entity* parentent, Sint32 sprite ) { if ( !parentent ) diff --git a/src/actgate.cpp b/src/actgate.cpp index 41bf29d95..bf2156cc6 100644 --- a/src/actgate.cpp +++ b/src/actgate.cpp @@ -188,7 +188,7 @@ void Entity::actGate() { Entity* entity = (Entity*)node->element; if ( entity == this || (entity->flags[PASSABLE] && entity->behavior != &actDeathGhost) - || entity->behavior == &actDoorFrame ) + || entity->behavior == &actDoorFrame || entity->behavior == &::actGate ) { continue; } diff --git a/src/actgeneral.cpp b/src/actgeneral.cpp index 9298b5823..b5a4f803f 100644 --- a/src/actgeneral.cpp +++ b/src/actgeneral.cpp @@ -127,6 +127,91 @@ void actFurniture(Entity* my) my->actFurniture(); } +void Entity::furnitureHandleDamageMagic(int damage, Entity& magicProjectile, Entity* caster, bool messages, bool doSound) +{ + updateEntityOldHPBeforeMagicHit(*this, magicProjectile); + int oldHP = this->furnitureHealth; + this->furnitureHealth -= damage; + if ( caster ) + { + if ( caster->behavior == &actPlayer ) + { + bool destroyed = oldHP > 0 && this->furnitureHealth <= 0; + if ( destroyed ) + { + gameModeManager.currentSession.challengeRun.updateKillEvent(this); + players[caster->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_COMMON, this); + } + switch ( this->furnitureType ) + { + case FURNITURE_CHAIR: + if ( destroyed && messages ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(388)); + } + break; + case FURNITURE_TABLE: + if ( destroyed && messages ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(389)); + } + break; + case FURNITURE_BED: + if ( destroyed && messages ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2505)); + } + break; + case FURNITURE_BUNKBED: + if ( destroyed && messages ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2506)); + } + break; + case FURNITURE_PODIUM: + if ( destroyed && messages ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2507)); + } + break; + default: + break; + } + } + + switch ( this->furnitureType ) + { + case FURNITURE_CHAIR: + updateEnemyBar(caster, this, Language::get(677), this->furnitureHealth, this->furnitureMaxHealth, + false, DamageGib::DMG_DEFAULT); + break; + case FURNITURE_TABLE: + updateEnemyBar(caster, this, Language::get(676), this->furnitureHealth, this->furnitureMaxHealth, + false, DamageGib::DMG_DEFAULT); + break; + case FURNITURE_BED: + updateEnemyBar(caster, this, Language::get(2505), this->furnitureHealth, this->furnitureMaxHealth, + false, DamageGib::DMG_DEFAULT); + break; + case FURNITURE_BUNKBED: + updateEnemyBar(caster, this, Language::get(2506), this->furnitureHealth, this->furnitureMaxHealth, + false, DamageGib::DMG_DEFAULT); + break; + case FURNITURE_PODIUM: + updateEnemyBar(caster, this, Language::get(2507), this->furnitureHealth, this->furnitureMaxHealth, + false, DamageGib::DMG_DEFAULT); + break; + default: + break; + } + } + + if ( doSound ) + { + playSoundEntity(this, 28, 128); + } +} + void Entity::actFurniture() { @@ -589,6 +674,473 @@ void Entity::actPistonCam() } } +int colliderGetSpellRange(Entity* my) +{ + if ( my->colliderSpellEvent % 1000 == 0 ) { return 0; } + int range = 32; + int effectType = my->colliderSpellEvent % 1000; + switch ( effectType ) + { + case 3: + case 4: + case 7: + range = 128; + break; + default: + break; + } + return range; +} + +void actColliderMushroomCap(Entity* my) +{ + Entity* parent = uidToEntity(my->parent); + if ( !parent ) + { + Entity* gib = spawnGib(my); + gib->sprite = my->sprite; + gib->z = my->z; + gib->skill[5] = 1; // poof + list_RemoveNode(my->mynode); + return; + } + + my->z = parent->z - 7.0 + 1.5; + my->pitch = (my->fskill[2]) * (PI / 24) * sin(my->fskill[1]); + my->roll = (my->fskill[2]) * (PI / 24) * sin(my->fskill[1] + PI / 2); + + real_t bobScale = 0.08 * my->skill[0]; + my->scalex = 1.0 + bobScale * sin(my->fskill[0]); + my->scaley = 1.0 + bobScale * sin(my->fskill[0]); + my->scalez = 1.0 - bobScale * sin(my->fskill[0]); + + parent->scalex = 1.0 - bobScale * 0.25 * sin(my->fskill[0]); + parent->scaley = 1.0 - bobScale * 0.25 * sin(my->fskill[0]); + parent->scalez = 1.0 + bobScale * 0.25 * sin(my->fskill[0]); + + int trigger = 0; + if ( parent->colliderSpellEventTrigger != 0 ) + { + trigger = parent->colliderSpellEventTrigger; + parent->colliderSpellEventTrigger = 0; + } + + auto& friendlyFire = my->skill[1]; + + if ( trigger > 0 ) + { + real_t percent = (trigger % 100) / 100.0; + my->skill[0] = 4; + + if ( percent < 0.5 ) + { + playSoundEntityLocal(parent, 711 + local_rng.rand() % 3, 128); + } + else + { + playSoundEntityLocal(parent, 714 + local_rng.rand() % 3, 128); + } + my->fskill[0] = 0.0; + my->fskill[1] = 2 * PI * percent; + my->fskill[2] = 0.0; + + if ( trigger >= 100 ) + { + friendlyFire = 1; + /*if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + friendlyFire = 0; + }*/ + } + else + { + friendlyFire = 0; + } + } + + if ( my->skill[0] > 0 ) + { + int range = colliderGetSpellRange(parent); + int effectType = parent->colliderSpellEvent % 1000; + if ( effectType == 3 || effectType == 4 || effectType == 7 ) + { + range = 32; + } + + auto cacheType = AOEIndicators_t::CACHE_MUSHROOM_1; + Uint32 color = makeColorRGB(0, 145, 16); + int gibSprite = 1885; + switch ( effectType ) + { + case 4: + color = makeColorRGB(206, 162, 146); + cacheType = AOEIndicators_t::CACHE_MUSHROOM_2; + gibSprite = 1887; + break; + case 5: + color = makeColorRGB(116, 128, 180); + cacheType = AOEIndicators_t::CACHE_MUSHROOM_3; + gibSprite = 1888; + break; + case 2: + color = makeColorRGB(180, 116, 160); + cacheType = AOEIndicators_t::CACHE_MUSHROOM_4; + gibSprite = 1889; + break; + default: + break; + } + + my->fskill[2] = std::min(1.0, std::max(0.05, my->fskill[2] * 1.15)); + if ( my->skill[0] == 4 ) // circle first + { + if ( my->fskill[1] >= 2 * PI ) + { + my->skill[0]--; + + if ( (effectType == 3 || effectType == 4 || effectType == 7) && multiplayer != CLIENT ) + { + if ( Entity* target = uidToEntity(parent->colliderSpellTarget) ) + { + real_t tangent = atan2(target->y - parent->y, target->x - parent->x); + + Entity* caster = parent; + if ( parent->colliderCreatedParent != 0 ) + { + if ( Entity* ent = uidToEntity(parent->colliderCreatedParent) ) + { + caster = ent; + } + } + if ( Entity* missile = castSpell( + caster ? caster->getUID() : 0, + getSpellFromID( + (effectType == 3 || effectType == 7) ? SPELL_SPORE_BOMB : SPELL_MYCELIUM_BOMB + ), false, true) ) + { + missile->collisionIgnoreTargets.insert(parent->getUID()); + missile->x = parent->x; + missile->y = parent->y; + missile->z = 6.0; + real_t vel = sqrt(pow(missile->vel_x, 2) + pow(missile->vel_y, 2)); + missile->vel_z = -1.2; + missile->vel_x = vel * cos(tangent); + missile->vel_y = vel * sin(tangent); + } + } + } + + for ( int i = 0; i < 8; ++i ) + { + Entity* fx = createParticleAestheticOrbit(parent, gibSprite, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_MUSHROOM_SPELL); + fx->x = parent->x; + fx->y = parent->y; + fx->actmagicOrbitDist = range; + fx->yaw = parent->yaw + (i * PI / 4.0); + fx->pitch = -PI; + fx->fskill[4] = fx->yaw; + + Entity* gib = spawnGib(parent); + gib->sprite = gibSprite; + gib->vel_x = 1.5 * cos(fx->yaw); + gib->vel_y = 1.5 * sin(fx->yaw); + gib->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; + } + + if ( Entity* fx = createParticleAOEIndicator(parent, parent->x, parent->y, 0.0, TICKS_PER_SECOND * 2, range) ) + { + //fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + //fx->scalex = 0.8; + //fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + indicator->cacheType = cacheType; + } + } + playSoundEntityLocal(parent, 169, 128); + playSoundEntityLocal(parent, 717 + local_rng.rand() % 3, 128); + } + } + else + { + my->fskill[0] += 0.25; + if ( my->fskill[0] >= 2 * PI ) + { + my->fskill[0] -= 2 * PI; + my->skill[0]--; + if ( my->skill[0] == 1 ) + { + auto& colliderData = EditorEntityData_t::colliderData[parent->colliderDamageTypes]; + if ( (effectType == 3 || effectType == 4) && + colliderData.name.find("_fragile") != std::string::npos ) + { + if ( multiplayer != CLIENT ) + { + parent->colliderCurrentHP = 0; + parent->colliderKillerUid = 0; + } + } + else + { + playSoundEntityLocal(parent, 717 + local_rng.rand() % 3, 128); + for ( int i = 0; i < 8; ++i ) + { + Entity* fx = createParticleAestheticOrbit(parent, gibSprite, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_MUSHROOM_SPELL); + fx->x = parent->x; + fx->y = parent->y; + fx->actmagicOrbitDist = range; + fx->yaw = parent->yaw + (i * PI / 4.0) + PI / 8; + fx->pitch = -PI; + fx->fskill[4] = fx->yaw; + + Entity* gib = spawnGib(parent); + gib->sprite = gibSprite; + gib->vel_x = 1.0 * cos(fx->yaw); + gib->vel_y = 1.0 * sin(fx->yaw); + gib->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; + } + } + } + else if ( friendlyFire == 0 ) + { + auto& colliderData = EditorEntityData_t::colliderData[parent->colliderDamageTypes]; + if ( !(effectType == 3 || effectType == 4) && + colliderData.name.find("_fragile") != std::string::npos ) + { + if ( multiplayer != CLIENT ) + { + parent->colliderCurrentHP = 0; + parent->colliderKillerUid = 0; + } + } + } + } + + if ( multiplayer != CLIENT ) + { + Entity* caster = uidToEntity(parent->colliderCreatedParent); + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1 + (range / 16)); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( !(entity->behavior == &actPlayer || entity->behavior == &actMonster) ) + { + continue; + } + if ( !entity->monsterIsTargetable() ) + { + continue; + } + if ( entityDist(my, entity) > (real_t)(range + 4.0) ) + { + continue; + } + Stat* stats = entity->getStats(); + if ( !stats ) { continue; } + + if ( caster && caster->behavior == &actMonster ) + { + if ( caster == entity ) + { + continue; + } + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } + if ( caster && caster->behavior == &actPlayer ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + if ( entity->behavior == &actMonster + && stats->type == MYCONID ) + { + continue; + } + + auto props = getParticleEmitterHitProps(parent->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 && (ticks - props->tick) < 50 ) + { + continue; + } + + real_t tangent = atan2(entity->y - parent->y, entity->x - parent->x); + bool oldPassable = entity->flags[PASSABLE]; + entity->flags[PASSABLE] = false; + real_t d = lineTraceTarget(parent, parent->x, parent->y, tangent, (real_t)(range + 4.0), 0, false, entity); + entity->flags[PASSABLE] = oldPassable; + if ( hit.entity != entity ) + { + continue; + } + + int damage = 5; + bool alertTarget = true; + bool friendlyFireTarget = false; + if ( effectType == 6 || effectType == 7 ) // player casted + { + damage = getSpellDamageFromID(SPELL_MUSHROOM, caster, nullptr, my); + + int bonusEffect = 0; + if ( parent && parent->colliderDropVariable > 0 ) + { + bonusEffect = parent->colliderDropVariable; + } + if ( caster && caster->behavior == &actPlayer ) + { + if ( Stat* casterStats = caster->getStats() ) + { + if ( casterStats->type == MYCONID && casterStats->getEffectActive(EFF_GROWTH) >= 2 ) + { + bonusEffect = std::max(bonusEffect, casterStats->getEffectActive(EFF_GROWTH) - 1); + } + } + } + damage += damage * (bonusEffect * 1.0); + + if ( caster && caster->behavior == &actPlayer ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + damage = 0; + alertTarget = false; + friendlyFireTarget = true; + } + } + else if ( achievementObserver.checkUidIsFromPlayer(parent->colliderCreatedParent) >= 0 ) + { + if ( (entity->behavior == &actMonster && entity->monsterAllyGetPlayerLeader()) + || entity->behavior == &actPlayer ) + { + damage = 0; + alertTarget = false; + friendlyFireTarget = true; + } + } + } + + if ( applyGenericMagicDamage(caster ? caster : parent, entity, caster ? *caster : *parent, SPELL_MUSHROOM, damage, alertTarget, true) ) + { + stats->killer = KilledBy::MUSHROOM; + entity->setObituary(Language::get(6753)); + + props->hits++; + props->tick = ticks; + if ( effectType == 1 ) + { + bool wasEffected = stats->getEffectActive(EFF_POISONED); + if ( entity->setEffect(EFF_POISONED, true, 3 * TICKS_PER_SECOND + 10, false) ) + { + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 944); + if ( caster ) + { + stats->poisonKiller = caster->getUID(); + } + } + } + else if ( effectType == 5 ) + { + bool wasEffected = stats->getEffectActive(EFF_DUSTED); + if ( entity->setEffect(EFF_DUSTED, true, 10 * TICKS_PER_SECOND + 10, true) ) + { + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 944); + if ( !wasEffected ) + { + messagePlayerColor(entity->isEntityPlayer(), MESSAGE_STATUS, makeColorRGB(255, 0, 0), Language::get(6752)); + } + } + } + else if ( effectType == 2 || effectType == 3 || effectType == 4 || effectType == 6 || effectType == 7 ) + { + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 944); + + if ( effectType == 3 || effectType == 6 || effectType == 7 ) + { + if ( !friendlyFireTarget ) + { + bool wasEffected = stats->getEffectActive(EFF_POISONED); + if ( entity->setEffect(EFF_POISONED, true, 3 * TICKS_PER_SECOND + 10, false) ) + { + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 944); + if ( caster ) + { + stats->poisonKiller = caster->getUID(); + } + } + } + } + + if ( entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + real_t pushbackMultiplier = 0.9; + real_t tangent = atan2(entity->y - parent->y, entity->x - parent->x); + if ( entity->behavior == &actPlayer ) + { + if ( !players[entity->skill[2]]->isLocalPlayer() ) + { + entity->monsterKnockbackVelocity = pushbackMultiplier; + entity->monsterKnockbackTangentDir = tangent; + serverUpdateEntityFSkill(entity, 11); + serverUpdateEntityFSkill(entity, 9); + } + else + { + entity->monsterKnockbackVelocity = pushbackMultiplier; + entity->monsterKnockbackTangentDir = tangent; + } + } + else if ( entity->behavior == &actMonster ) + { + entity->vel_x = cos(tangent) * pushbackMultiplier; + entity->vel_y = sin(tangent) * pushbackMultiplier; + entity->monsterKnockbackVelocity = 0.01; + entity->monsterKnockbackUID = parent->colliderCreatedParent; + entity->monsterKnockbackTangentDir = tangent; + } + } + } + } + } + } + } + } + } + else + { + my->fskill[2] *= 0.95; + } + + my->fskill[1] += 0.125; + my->focalz = -1.5; +} + bool Entity::isColliderShownAsWallOnMinimap() const { if ( !isDamageableCollider() ) { return false; } @@ -645,6 +1197,13 @@ bool Entity::isColliderAttachableToBombs() const return colliderDmgType.bombsAttach; } +bool Entity::isColliderPathableMonster(Monster type) const +{ + if ( !isDamageableCollider() ) { return false; } + auto& colliderData = EditorEntityData_t::colliderData[colliderDamageTypes]; + return colliderData.pathableMonsters.find(type) != colliderData.pathableMonsters.end(); +} + bool Entity::isDamageableCollider() const { return behavior == &actColliderDecoration && colliderMaxHP > 0; @@ -674,6 +1233,7 @@ bool Entity::isColliderBreakableContainer() const void Entity::colliderOnDestroy() { + removeLightField(); if ( multiplayer == CLIENT ) { return; } flags[PASSABLE] = true; @@ -690,28 +1250,69 @@ void Entity::colliderOnDestroy() } } - if ( colliderHideMonster != 0 ) + auto find = EditorEntityData_t::colliderData.find(colliderDamageTypes); + if ( find != EditorEntityData_t::colliderData.end() ) { - int type = colliderHideMonster % 1000; - int numSpawns = type == BAT_SMALL ? 2 : 1; - int successes = 0; - for ( int i = 0; i < numSpawns; ++i ) + if ( find->second.name == "mushroom_spell_casted" ) { - auto monster = summonMonster((Monster)type, ((int)(x / 16)) * 16 + 8, ((int)(y / 16)) * 16 + 8); - if ( monster ) + for ( int i = 0; i < this->colliderDropVariable; ++i ) { - monster->yaw = yaw; - monster->lookAtEntity(*monster); - monster->monsterLookDir = yaw; - if ( Stat* stats = monster->getStats() ) + if ( local_rng.rand() % 10 == 0 ) { - stats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS] = 1; + if ( Entity* ent = dropItemMonster(newItem(DUST_BALL, SERVICABLE, 0, 1, 0, true, nullptr), this, nullptr) ) + { + ent->itemOriginalOwner = colliderCreatedParent; + ent->itemGerminateResult = 1; + } + } + else + { + if ( Entity* ent = dropItemMonster(newItem(FOOD_SHROOM, SERVICABLE, 0, 1, 0, true, nullptr), this, nullptr) ) + { + ent->itemOriginalOwner = colliderCreatedParent; + ent->itemGerminateResult = 1; + } + } + } + Entity* parent = uidToEntity(this->colliderCreatedParent); + floorMagicCreateSpores(this, this->x, this->y, parent ? parent : this, 0, SPELL_SPORES); + } + else if ( find->second.name == "germinate_spell_casted" ) + { + for ( int i = 0; i < this->colliderDropVariable; ++i ) + { + if ( Entity* ent = dropItemMonster(newItem(FOOD_NUT, SERVICABLE, 0, 1, 0, true, nullptr), this, nullptr) ) + { + ent->itemOriginalOwner = colliderCreatedParent; + ent->itemGerminateResult = 1; + } + } + } + } + + if ( colliderHideMonster != 0 ) + { + int type = colliderHideMonster % 1000; + int numSpawns = type == BAT_SMALL ? 2 : 1; + int successes = 0; + for ( int i = 0; i < numSpawns; ++i ) + { + auto monster = summonMonster((Monster)type, ((int)(x / 16)) * 16 + 8, ((int)(y / 16)) * 16 + 8); + if ( monster ) + { + monster->yaw = yaw; + monster->lookAtEntity(*monster); + monster->monsterLookDir = yaw; + if ( Stat* stats = monster->getStats() ) + { + stats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS] = 1; if ( stats->type == GHOUL && currentlevel >= 15 ) { strcpy(stats->name, "enslaved ghoul"); stats->setAttribute("special_npc", "enslaved ghoul"); } - if ( stats->type == AUTOMATON ) + stats->setAttribute("spawn_no_sleep", "1"); + if ( stats->type == AUTOMATON && strcmp(map.filename, "automat.lmp") ) { monster->monsterStoreType = 1; // damaged } @@ -731,8 +1332,16 @@ void Entity::colliderOnDestroy() } if ( successes == 1 ) { - messagePlayer(killer->skill[2], MESSAGE_INTERACTION, Language::get(6234), - getMonsterLocalizedName((Monster)type).c_str(), Language::get(getColliderLangName())); + if ( getColliderOnJumpLangEntry() == 6815 ) + { + messagePlayer(killer->skill[2], MESSAGE_INTERACTION, Language::get(getColliderOnJumpLangEntry()), + Language::get(getColliderLangName())); + } + else + { + messagePlayer(killer->skill[2], MESSAGE_INTERACTION, Language::get(getColliderOnJumpLangEntry()), + getMonsterLocalizedName((Monster)type).c_str(), Language::get(getColliderLangName())); + } } else if ( successes > 1 ) { @@ -758,6 +1367,7 @@ void Entity::colliderOnDestroy() entity->goldBouncing = 0; entity->z = 0.0 - (local_rng.rand() % 3); entity->flags[INVISIBLE] = false; + entity->goldAmountBonus = entity->goldAmount; if ( multiplayer == SERVER ) { @@ -796,6 +1406,7 @@ void Entity::colliderOnDestroy() ent->goldInContainer = 0; ent->z = 0.0 - (local_rng.rand() % 3); ent->flags[INVISIBLE] = false; + ent->goldAmountBonus = ent->goldAmount; if ( multiplayer == SERVER ) { @@ -889,6 +1500,13 @@ int Entity::getColliderOnBreakLangEntry() const return colliderData.breakMessageLangEntry; } +int Entity::getColliderOnJumpLangEntry() const +{ + if ( !isDamageableCollider() ) { return 1; } + auto& colliderData = EditorEntityData_t::colliderData[colliderDamageTypes]; + return colliderData.colliderJumpLangEntry; +} + int Entity::getColliderSfxOnHit() const { if ( !isDamageableCollider() ) { return 0; } @@ -904,6 +1522,331 @@ int Entity::getColliderSfxOnBreak() const return colliderData.sfxBreak[local_rng.rand() % colliderData.sfxBreak.size()]; } +Entity* Entity::createBreakableCollider(int colliderDamageType, real_t _x, real_t _y, Entity* parent) +{ + if ( colliderDamageType == 0 ) + { + return nullptr; + } + int x = static_cast(_x) >> 4; + int y = static_cast(_y) >> 4; + if ( !(x > 0 && x < map.width - 1 && y > 0 && y < map.height - 1) ) + { + return nullptr; + } + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( !map.tiles[mapIndex] || swimmingtiles[map.tiles[mapIndex]] || lavatiles[map.tiles[mapIndex]] ) + { + return nullptr; + } + + auto find = EditorEntityData_t::colliderData.find(colliderDamageType); + if ( find == EditorEntityData_t::colliderData.end() ) + { + return nullptr; + } + + + Entity* breakable = newEntity(-1, 1, map.entities, nullptr); + breakable->x = x * 16.0 + 8.0; + breakable->y = y * 16.0 + 8.0; + std::vector> coords = + { + {0, 1}, + {0, -1}, + {1, 0}, + {1, 1}, + {1, -1}, + {-1, 0}, + {-1, 1}, + {-1, -1} + }; + + bool goodspot = false; + if ( checkObstacle(breakable->x, breakable->y, breakable, nullptr, true, true, true, false) ) + { + while ( coords.size() > 0 ) + { + int pick = local_rng.rand() % coords.size(); + if ( checkObstacle(breakable->x + coords[pick].first * 16, breakable->y + coords[pick].second * 16, breakable, nullptr, true, true, true, false) ) + { + coords.erase(coords.begin() + pick); + } + else + { + goodspot = true; + breakable->x += coords[pick].first * 16; + breakable->y += coords[pick].second * 16; + break; + } + } + + if ( !goodspot ) + { + list_RemoveNode(breakable->mynode); + return nullptr; + } + } + + breakable->colliderDamageTypes = colliderDamageType; + breakable->colliderCreatedParent = parent ? parent->getUID() : 0; + breakable->colliderDecorationRotation = 2 * (local_rng.rand() % 3); + Entity::colliderAssignProperties(breakable, false, &map); + + breakable->colliderSetServerSkillOnSpawned(); + breakable->flags[UPDATENEEDED] = true; + breakable->flags[NOUPDATE] = false; + return breakable; +} + +void Entity::colliderSetServerSkillOnSpawned() +{ + Sint32 val = (1 << 31); + val |= (Uint8)(25); + val |= (Uint8)(colliderDamageTypes) << 8; + val |= (Uint8)(colliderSpellEvent % 1000) << 16; + skill[2] = val; +} + +void Entity::colliderAssignProperties(Entity* entity, bool mapGeneration, map_t* whichMap) +{ + auto& rng = mapGeneration ? map_rng : local_rng; + + entity->seedEntityRNG(rng.getU32()); + + if ( mapGeneration || multiplayer != CLIENT ) + { + int dir = entity->colliderDecorationRotation; + if ( dir == -1 ) + { + dir = rng.rand() % 8; + entity->colliderDecorationRotation = dir; + } + entity->yaw = dir * (PI / 4); + } + /*static ConsoleVariable debugColliderType("/collider_type", 14); + entity->colliderDamageTypes = *debugColliderType;*/ + auto find = EditorEntityData_t::colliderData.find(entity->colliderDamageTypes); + if ( find != EditorEntityData_t::colliderData.end() ) + { + auto& data = find->second; + if ( data.hasOverride("dir_offset") ) + { + if ( mapGeneration || multiplayer != CLIENT ) + { + entity->yaw = ((entity->colliderDecorationRotation + data.getOverride("dir_offset")) * (PI / 4)); + } + } + if ( data.hasOverride("model") ) + { + entity->colliderDecorationModel = data.getOverride("model"); + } + if ( data.hasOverride("height") ) + { + entity->colliderDecorationHeightOffset = data.getOverride("height"); + } + if ( entity->colliderDecorationRotation == 0 ) + { + if ( data.hasOverride("east_x") ) + { + entity->colliderDecorationXOffset = data.getOverride("east_x"); + } + if ( data.hasOverride("east_y") ) + { + entity->colliderDecorationYOffset = data.getOverride("east_y"); + } + } + else if ( entity->colliderDecorationRotation == 2 ) + { + if ( data.hasOverride("south_x") ) + { + entity->colliderDecorationXOffset = data.getOverride("south_x"); + } + if ( data.hasOverride("south_y") ) + { + entity->colliderDecorationYOffset = data.getOverride("south_y"); + } + } + else if ( entity->colliderDecorationRotation == 4 ) + { + if ( data.hasOverride("west_x") ) + { + entity->colliderDecorationXOffset = data.getOverride("west_x"); + } + if ( data.hasOverride("west_y") ) + { + entity->colliderDecorationYOffset = data.getOverride("west_y"); + } + } + else if ( entity->colliderDecorationRotation == 6 ) + { + if ( data.hasOverride("north_x") ) + { + entity->colliderDecorationXOffset = data.getOverride("north_x"); + } + if ( data.hasOverride("north_y") ) + { + entity->colliderDecorationYOffset = data.getOverride("north_y"); + } + } + if ( data.hasOverride("collision") ) + { + entity->colliderHasCollision = data.getOverride("collision"); + } + if ( data.hasOverride("collision_x") ) + { + entity->colliderSizeX = data.getOverride("collision_x"); + } + if ( data.hasOverride("collision_y") ) + { + entity->colliderSizeY = data.getOverride("collision_y"); + } + if ( data.hasOverride("hp") ) + { + entity->colliderMaxHP = data.getOverride("hp"); + } + if ( data.hasOverride("diggable") ) + { + entity->colliderDiggable = data.getOverride("diggable"); + } + + if ( entity->colliderIsMapGenerated == 0 && (mapGeneration || multiplayer != CLIENT) + && entity->entity_rng ) + { + static int lastSpellEvent = 0; + if ( data.spellTriggers.size() > 0 ) + { + std::vector chances; + bool avoidLastSpell = false; + for ( auto s : data.spellTriggers ) + { + chances.push_back(1); + if ( lastSpellEvent != 0 && s != lastSpellEvent ) + { + avoidLastSpell = true; + } + } + + if ( avoidLastSpell ) + { + int unusedChances = 0; + for ( size_t i = 0; i < chances.size(); ++i ) + { + if ( chances[i] != lastSpellEvent ) + { + ++unusedChances; + } + } + if ( unusedChances > 0 ) + { + for ( size_t i = 0; i < chances.size(); ++i ) + { + if ( data.spellTriggers[i] == lastSpellEvent ) + { + chances[i] = 0; + } + } + } + } + + int pickIndex = entity->entity_rng->discrete(chances.data(), chances.size()); + int picked = data.spellTriggers[pickIndex]; + if ( picked > 0 ) + { + if ( entity->entity_rng->rand() % 5 > 0 ) + { + picked += 1000; + } + entity->colliderSpellEvent = picked; + lastSpellEvent = picked % 1000; + } + } + } + } + + entity->sprite = entity->colliderDecorationModel; + entity->sizex = entity->colliderSizeX; + entity->sizey = entity->colliderSizeY; + + if ( mapGeneration || multiplayer != CLIENT ) + { + entity->x += entity->colliderDecorationXOffset * 0.25; + entity->y += entity->colliderDecorationYOffset * 0.25; + entity->z = 7.5 - entity->colliderDecorationHeightOffset * 0.25; + bool modifiedFocal = false; + if ( entity->x < 0.0 ) + { + while ( entity->x < 0.0 ) + { + entity->x += 16.0; + entity->focalx -= 16.0; + } + modifiedFocal = true; + } + if ( entity->y < 0 ) + { + while ( entity->y < 0.0 ) + { + entity->y += 16.0; + entity->focaly -= 16.0; + } + modifiedFocal = true; + } + if ( static_cast(entity->x) >= whichMap->width * 16 ) + { + while ( static_cast(entity->x) >= whichMap->width * 16 ) + { + entity->x -= 16.0; + entity->focalx += 16.0; + } + modifiedFocal = true; + } + if ( static_cast(entity->y) >= whichMap->height * 16 ) + { + while ( static_cast(entity->y) >= whichMap->height * 16 ) + { + entity->y -= 16.0; + entity->focaly += 16.0; + } + modifiedFocal = true; + } + if ( modifiedFocal ) + { + real_t fx = entity->focalx; + real_t fy = entity->focaly; + entity->focalx = fx * cos(entity->yaw) - fy * cos(entity->yaw + PI / 2); + entity->focaly = -fx * sin(entity->yaw) + fy * sin(entity->yaw + PI / 2); + } + } + + if ( mapGeneration ) + { + entity->flags[PASSABLE] = entity->colliderHasCollision == 0; + } + else + { + entity->flags[PASSABLE] = true; // set later in act() + } + entity->flags[BLOCKSIGHT] = false; + entity->behavior = &actColliderDecoration; + entity->colliderCurrentHP = entity->colliderMaxHP; + entity->colliderOldHP = entity->colliderMaxHP; + if ( entity->isDamageableCollider() ) + { + entity->flags[UNCLICKABLE] = false; + } + else + { + entity->flags[UNCLICKABLE] = true; + } + /*if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3);*/ +} + +std::set collidersToRaiseToHeight; void actColliderDecoration(Entity* my) { if ( !my ) @@ -934,10 +1877,111 @@ void actColliderDecoration(Entity* my) { my->colliderHasCollision |= EditorEntityData_t::COLLIDER_COLLISION_FLAG_NPC; } + + if ( colliderData.name.find("mushroom_spell") != std::string::npos ) + { + collidersToRaiseToHeight.insert(my->getUID()); + my->z = 16.0; + Entity* entity = newEntity(my->sprite + 1, 1, map.entities, nullptr); //Sprite entity. + entity->x = my->x; + entity->y = my->y; + entity->z = my->z - 7.0; + entity->behavior = &actColliderMushroomCap; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[UNCLICKABLE] = true; + entity->yaw = my->yaw; + entity->parent = my->getUID(); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } + else if ( colliderData.name.find("germinate_spell") != std::string::npos ) + { + collidersToRaiseToHeight.insert(my->getUID()); + my->z = 16.0; + } + } + } + + if ( collidersToRaiseToHeight.find(my->getUID()) != collidersToRaiseToHeight.end() ) + { + // mushroom_spell + my->z = std::max(8.0, my->z - 0.25); + if ( my->z <= 8.0 ) + { + collidersToRaiseToHeight.erase(my->getUID()); + + if ( multiplayer != CLIENT ) + { + auto& colliderData = EditorEntityData_t::colliderData[my->colliderDamageTypes]; + if ( colliderData.name.find("germinate_spell") != std::string::npos ) + { + Entity* caster = my->colliderCreatedParent != 0 ? uidToEntity(my->colliderCreatedParent) : my; + createRadiusMagic(SPELL_HEAL_PULSE, caster, + my->x, my->y, 24, 10 * TICKS_PER_SECOND, nullptr); + } + } + } + if ( my->z > 8.5 ) + { + if ( my->ticks % 2 == 0 ) { + const int x = my->x + local_rng.uniform(-3, 3); + const int y = my->y + local_rng.uniform(-3, 3); + auto poof = spawnPoof(x, y, 6, 0.33); + } + } + } + + my->removeLightField(); + if ( my->flags[BURNABLE] && my->flags[BURNING] ) + { + my->light = addLight(my->x / 16, my->y / 16, "object_burning"); + } + + if ( (my->colliderHasCollision == 0) ) + { + my->flags[PASSABLE] = true; + } + else if ( my->flags[PASSABLE] ) + { + // check inside + std::vector entLists; + if ( multiplayer == CLIENT ) + { + entLists.push_back(map.entities); // clients use old map.entities method + } + else + { + entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + } + bool somebodyinside = false; + for ( std::vector::iterator it = entLists.begin(); it != entLists.end() && !somebodyinside; ++it ) + { + list_t* currentList = *it; + for ( node_t* node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( !(entity->behavior == &actPlayer || entity->behavior == &actMonster) ) + { + continue; + } + if ( entityInsideEntity(my, entity) ) + { + somebodyinside = true; + break; + } + } + } + if ( !somebodyinside ) + { + my->flags[PASSABLE] = false; } } - my->flags[PASSABLE] = (my->colliderHasCollision == 0); if ( multiplayer != CLIENT ) { bool checkWallDeletion = false; @@ -965,6 +2009,7 @@ void actColliderDecoration(Entity* my) if ( !map.tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map.height] ) { //messagePlayer(0, MESSAGE_DEBUG, "[Collider]: Destroyed self at x: %d, y: %d", x, y); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -980,6 +2025,35 @@ void actColliderDecoration(Entity* my) my->createWorldUITooltip(); } + my->colliderTelepathy = 0; + if ( my->colliderContainedEntity != 0 ) + { + Sint32 telepathy = 0; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i]->isLocalPlayer() ) + { + if ( players[i]->entity && players[i]->entity->isBlind() ) + { + if ( stats[i]->type == GNOME ) + { + telepathy |= (1 << i); + } + } + } + } + if ( telepathy ) + { + if ( Entity* containedEntity = uidToEntity(my->colliderContainedEntity) ) + { + if ( containedEntity->behavior == &actGoldBag ) + { + my->colliderTelepathy = telepathy; + } + } + } + } + if ( multiplayer != CLIENT ) { auto& colliderData = EditorEntityData_t::colliderData[my->colliderDamageTypes]; @@ -995,10 +2069,165 @@ void actColliderDecoration(Entity* my) } } + auto prevOldHP = my->colliderOldHP; my->colliderOldHP = my->colliderCurrentHP; if ( my->colliderCurrentHP > 0 ) { + if ( my->colliderSpellEvent > 0 ) + { + if ( my->colliderCreatedParent != 0 && my->ticks >= 10 * TICKS_PER_SECOND ) + { + // destroy self + my->colliderCurrentHP = 0; + my->colliderKillerUid = 0; + } + else if ( my->colliderSpellEventCooldown > 0 ) + { + my->colliderSpellEventCooldown--; + } + else if ( (my->colliderSpellEvent % 1000) != 8 && (my->colliderSpellEvent % 1000) != 9 ) + { + Entity* found = nullptr; + bool rescan = false; + int effectType = my->colliderSpellEvent % 1000; + if ( prevOldHP != my->colliderCurrentHP ) + { + my->colliderSpellEventCooldown = 4 * TICKS_PER_SECOND; + my->colliderSpellEventTrigger = 100 + 75 + local_rng.rand() % 21; + serverUpdateEntitySkill(my, 21); + if ( effectType == 3 || effectType == 4 || effectType == 7 ) + { + rescan = true; // look for a random target + } + } + + if ( (my->colliderSpellEventCooldown == 0 && (my->ticks % TICKS_PER_SECOND == 0 && my->colliderSpellEvent >= 1000) + || rescan) ) + { + Entity* caster = (my->colliderCreatedParent != 0 ? uidToEntity(my->colliderCreatedParent) : nullptr); + bool targetNonPlayer = false; + if ( !caster && achievementObserver.checkUidIsFromPlayer(my->colliderCreatedParent) >= 0 ) + { + targetNonPlayer = true; + } + int range = colliderGetSpellRange(my); + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1 + (range / 16)); + std::vector entitiesInRange; + for ( std::vector::iterator it = entLists.begin(); it != entLists.end() && !found; ++it ) + { + list_t* currentList = *it; + node_t* node; + for ( node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( !entity || !(entity->behavior == &actPlayer || entity->behavior == &actMonster) ) { continue; } + if ( !entity->monsterIsTargetable() ) { continue; } + if ( caster ) + { + if ( caster == entity + || (caster->checkFriend(entity) + && !(caster->behavior == &actMonster && + (caster->monsterTarget == entity->getUID() // caster targeting this + || (entity->behavior == &actMonster && entity->monsterTarget == caster->getUID())))) ) // entity targeting caster + { + continue; + } + } + else + { + if ( targetNonPlayer ) + { + if ( entity->behavior == &actMonster && !entity->monsterAllyGetPlayerLeader() ) + { + // allowed to target + } + else + { + continue; + } + } + else if ( !(entity->behavior == &actPlayer || (entity->behavior == &actMonster && entity->monsterAllyGetPlayerLeader())) ) + { + continue; + } + } + real_t targetDist = entityDist(entity, my); + if ( targetDist >= (range + 4.0) ) + { + continue; + } + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + real_t traceDist = range; + if ( Stat* entitystats = entity->getStats() ) + { + int light = entity->entityLightAfterReductions(*entitystats, my); + if ( targetDist > light ) + { + continue; + } + } + lineTraceTarget(my, my->x, my->y, tangent, traceDist, 0, false, entity); + if ( hit.entity == entity ) + { + if ( effectType == 3 || effectType == 4 || effectType == 7 ) + { + entitiesInRange.push_back(entity); + } + else + { + found = entity; + break; + } + } + } + } + + if ( effectType == 3 || effectType == 4 || effectType == 7 ) + { + if ( entitiesInRange.size() > 0 ) + { + if ( caster && caster->behavior == &actMonster && caster->monsterTarget != 0 ) + { + for ( auto ent : entitiesInRange ) + { + if ( ent->getUID() == caster->monsterTarget ) + { + found = ent; + break; + } + } + } + if ( !found ) + { + if ( caster ) + { + for ( auto ent : entitiesInRange ) + { + if ( ent->behavior == &actMonster && ent->monsterTarget == caster->getUID() ) + { + found = ent; + break; + } + } + } + } + if ( !found ) + { + found = entitiesInRange[local_rng.rand() % entitiesInRange.size()]; + } + my->colliderSpellTarget = found->getUID(); + } + } + } + if ( found ) + { + my->colliderSpellEventCooldown = 4 * TICKS_PER_SECOND; + my->colliderSpellEventTrigger = 1 + local_rng.rand() % 75; + serverUpdateEntitySkill(my, 21); + } + } + } if ( my->colliderHideMonster >= 1000 ) // summon a monster when player is near { Entity* found = nullptr; @@ -1015,11 +2244,14 @@ void actColliderDecoration(Entity* my) if ( entity && (entity->behavior == &actPlayer || (entity->behavior == &actMonster && entity->monsterAllyGetPlayerLeader())) ) { real_t tangent = atan2(entity->y - my->y, entity->x - my->x); - lineTraceTarget(my, my->x, my->y, tangent, 32.0, 0, false, entity); - if ( hit.entity == entity ) + if ( entityDist(entity, my) < 40.0 ) { - found = entity; - break; + lineTraceTarget(my, my->x, my->y, tangent, 32.0, 0, false, entity); + if ( hit.entity == entity ) + { + found = entity; + break; + } } } } @@ -1057,7 +2289,8 @@ void actColliderDecoration(Entity* my) strcpy(stats->name, "enslaved ghoul"); stats->setAttribute("special_npc", "enslaved ghoul"); } - if ( stats->type == AUTOMATON ) + stats->setAttribute("spawn_no_sleep", "1"); + if ( stats->type == AUTOMATON && strcmp(map.filename, "automat.lmp") ) { monster->monsterStoreType = 1; // damaged } @@ -1074,8 +2307,16 @@ void actColliderDecoration(Entity* my) } if ( successes == 1 ) { - messagePlayer(found->skill[2], MESSAGE_INTERACTION, Language::get(6234), - getMonsterLocalizedName((Monster)type).c_str(), Language::get(my->getColliderLangName())); + if ( my->getColliderOnJumpLangEntry() == 6815 ) + { + messagePlayer(found->skill[2], MESSAGE_INTERACTION, Language::get(my->getColliderOnJumpLangEntry()), + Language::get(my->getColliderLangName())); + } + else + { + messagePlayer(found->skill[2], MESSAGE_INTERACTION, Language::get(6234), + getMonsterLocalizedName((Monster)type).c_str(), Language::get(my->getColliderLangName())); + } } else if ( successes > 1 ) { @@ -1108,8 +2349,9 @@ void actColliderDecoration(Entity* my) } } -void Entity::colliderHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster) +void Entity::colliderHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster, bool messages, bool doSound) { + updateEntityOldHPBeforeMagicHit(*this, magicProjectile); auto oldHP = colliderCurrentHP; colliderCurrentHP -= damage; //Decrease object health. if ( caster ) @@ -1124,34 +2366,42 @@ void Entity::colliderHandleDamageMagic(int damage, Entity &magicProjectile, Enti { if ( oldHP > 0 ) { - if ( magicProjectile.behavior == &actBomb ) + if ( messages ) { - messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(3617), items[magicProjectile.skill[21]].getIdentifiedName(), Language::get(getColliderLangName())); - } - else - { - messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(getColliderLangName())); + if ( magicProjectile.behavior == &actBomb ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(3617), items[magicProjectile.skill[21]].getIdentifiedName(), Language::get(getColliderLangName())); + } + else + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(getColliderLangName())); + } } if ( isColliderWall() ) { Compendium_t::Events_t::eventUpdateWorld(caster->skill[2], Compendium_t::CPDM_BARRIER_DESTROYED, "breakable barriers", 1); } + + players[caster->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_COMMON, this); } } else { - if ( magicProjectile.behavior == &actBomb ) - { - messagePlayer(caster->skill[2], MESSAGE_COMBAT_BASIC, Language::get(3618), items[magicProjectile.skill[21]].getIdentifiedName(), Language::get(getColliderLangName())); - } - else + if ( messages ) { - messagePlayer(caster->skill[2], MESSAGE_COMBAT_BASIC, Language::get(378), Language::get(getColliderLangName())); + if ( magicProjectile.behavior == &actBomb ) + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT_BASIC, Language::get(3618), items[magicProjectile.skill[21]].getIdentifiedName(), Language::get(getColliderLangName())); + } + else + { + messagePlayer(caster->skill[2], MESSAGE_COMBAT_BASIC, Language::get(378), Language::get(getColliderLangName())); + } } } - updateEnemyBar(caster, this, Language::get(getColliderLangName()), colliderCurrentHP, colliderMaxHP, - false, DamageGib::DMG_DEFAULT); } + updateEnemyBar(caster, this, Language::get(getColliderLangName()), colliderCurrentHP, colliderMaxHP, + false, DamageGib::DMG_DEFAULT); } int sound = 28; //damage.ogg @@ -1159,7 +2409,10 @@ void Entity::colliderHandleDamageMagic(int damage, Entity &magicProjectile, Enti { sound = getColliderSfxOnHit(); } - playSoundEntity(this, sound, 64); + if ( doSound ) + { + playSoundEntity(this, sound, 64); + } } void actFloorDecoration(Entity* my) @@ -1182,6 +2435,68 @@ void actFloorDecoration(Entity* my) return; } + if ( my->floorDecorationDestroyIfNoWall >= 0 ) + { + int x = static_cast(my->x) >> 4; + int y = static_cast(my->y) >> 4; + std::vector> coords; + switch ( my->floorDecorationDestroyIfNoWall ) + { + case 0: + // east + coords.push_back(std::make_pair(x + 1, y)); + break; + case 1: + // southeast + coords.push_back(std::make_pair(x + 1, y)); + coords.push_back(std::make_pair(x, y + 1)); + break; + case 2: + // south + coords.push_back(std::make_pair(x, y + 1)); + break; + case 3: + // southwest + coords.push_back(std::make_pair(x, y + 1)); + coords.push_back(std::make_pair(x - 1, y)); + break; + case 4: + // west + coords.push_back(std::make_pair(x - 1, y)); + break; + case 5: + // northwest + coords.push_back(std::make_pair(x, y - 1)); + coords.push_back(std::make_pair(x - 1, y)); + break; + case 6: + // north + coords.push_back(std::make_pair(x, y - 1)); + break; + case 7: + // northeast + coords.push_back(std::make_pair(x, y - 1)); + coords.push_back(std::make_pair(x + 1, y)); + break; + default: + break; + } + + for ( auto& pair : coords ) + { + int x = pair.first; + int y = pair.second; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + if ( !map.tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map.height] ) + { + list_RemoveNode(my->mynode); + return; + } + } + } + } + if ( my->floorDecorationInteractText1 == 0 ) { // no text. @@ -1231,26 +2546,71 @@ void actFloorDecoration(Entity* my) if ( charIsWordSeparator(c) ) { break; } key += c; } - if ( ScriptTextParser.allEntries.find(key) != ScriptTextParser.allEntries.end() ) + auto find = ScriptTextParser.allEntries.find(key); + if ( find != ScriptTextParser.allEntries.end() ) { - if ( players[i]->isLocalPlayer() ) + if ( find->second.objectType == ScriptTextParser_t::OBJ_SIGN ) { - if ( players[i]->isLocalPlayerAlive() ) + if ( players[i]->isLocalPlayer() ) { - players[i]->signGUI.openSign(key, my->getUID()); + if ( players[i]->isLocalPlayerAlive() ) + { + players[i]->signGUI.openSign(key, my->getUID()); + } + } + else if ( multiplayer == SERVER && i > 0 && !client_disconnected[i] ) + { + strcpy((char*)net_packet->data, "SIGN"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + strcpy((char*)(&net_packet->data[8]), key.c_str()); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 8 + strlen(key.c_str()) + 1; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + return; + } + else if ( find->second.objectType == ScriptTextParser_t::OBJ_MESSAGE ) + { + Uint8 r = 255; + Uint8 g = 255; + Uint8 b = 255; + for ( auto& var : find->second.variables ) + { + if ( var.type == ScriptTextParser_t::VariableTypes::COLOR_R ) + { + r = var.numericValue; + } + else if ( var.type == ScriptTextParser_t::VariableTypes::COLOR_G ) + { + g = var.numericValue; + } + else if ( var.type == ScriptTextParser_t::VariableTypes::COLOR_B ) + { + b = var.numericValue; + } } + messagePlayerColor(i, MESSAGE_INSPECTION, makeColorRGB(r, g, b), find->second.formattedText.c_str()); + return; + } + else if ( find->second.objectType == ScriptTextParser_t::OBJ_BUBBLE_SIGN ) + { + players[i]->worldUI.worldTooltipDialogue.createDialogueTooltip(my->getUID(), + Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_SIGNPOST, find->second.formattedText.c_str()); + return; } - else if ( multiplayer == SERVER && i > 0 && !client_disconnected[i] ) + else if ( find->second.objectType == ScriptTextParser_t::OBJ_BUBBLE_GRAVE ) { - strcpy((char*)net_packet->data, "SIGN"); - SDLNet_Write32(my->getUID(), &net_packet->data[4]); - strcpy((char*)(&net_packet->data[8]), key.c_str()); - net_packet->address.host = net_clients[i - 1].host; - net_packet->address.port = net_clients[i - 1].port; - net_packet->len = 8 + strlen(key.c_str()) + 1; - sendPacketSafe(net_sock, -1, net_packet, i - 1); + players[i]->worldUI.worldTooltipDialogue.createDialogueTooltip(my->getUID(), + Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_GRAVE, find->second.formattedText.c_str()); + return; + } + else if ( find->second.objectType == ScriptTextParser_t::OBJ_BUBBLE_DIALOGUE ) + { + players[i]->worldUI.worldTooltipDialogue.createDialogueTooltip(my->getUID(), + Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_NPC, find->second.formattedText.c_str()); + return; } - return; } } @@ -1418,6 +2778,18 @@ int TextSourceScript::textSourceProcessScriptTag(std::string& input, std::string x2 = std::min(std::max(x1, x2), (int)map.width - 1); y2 = std::min(std::max(y1, y2), (int)map.height - 1); + if ( findTag.compare("@explode=") == 0 ) + { + int explosionSprite = 0; + size_t foundSeperator = tagValue.find(";"); + if ( foundSeperator != std::string::npos ) + { + std::string first_str = tagValue.substr(foundSeperator + 1, tagValue.length() - foundSeperator); + explosionSprite = std::stoi(first_str); + } + return ((x1 & 0xFF) << 16) + ((y1 & 0xFF) << 24) + ((explosionSprite & 0xFFFF) << 0); + } + return (x1 & 0xFF) + ((x2 & 0xFF) << 8) + ((y1 & 0xFF) << 16) + ((y2 & 0xFF) << 24); } else @@ -1495,10 +2867,26 @@ int TextSourceScript::textSourceProcessScriptTag(std::string& input, std::string { return TO_ITEMS; } + else if ( !tagValue.compare("gold") ) + { + return TO_GOLD; + } + else if ( !tagValue.compare("bell") ) + { + return TO_BELL; + } else if ( !tagValue.compare("players") ) { return TO_PLAYERS; } + else if ( !tagValue.compare("breakable") ) + { + return TO_BREAKABLE; + } + else if ( !tagValue.compare("collider") ) + { + return TO_COLLIDER; + } else { for ( int i = 0; i < NUMMONSTERS; ++i ) @@ -1529,6 +2917,10 @@ int TextSourceScript::textSourceProcessScriptTag(std::string& input, std::string { return TRIGGER_ATTACHED_VISIBLE; } + else if ( !tagValue.compare("attached#interacted") ) + { + return TRIGGER_ATTACHED_INTERACTED; + } else if ( !tagValue.compare("always") ) { return TRIGGER_ATTACHED_ALWAYS; @@ -1538,6 +2930,17 @@ int TextSourceScript::textSourceProcessScriptTag(std::string& input, std::string return k_ScriptError; } } + else if ( findTag.compare("@breakablespawn=") == 0 || findTag.compare("@breakablejumpspawn=") == 0 ) + { + for ( int i = 0; i < NUMMONSTERS; ++i ) + { + if ( !tagValue.compare(monstertypename[i]) ) + { + return TO_NOTHING + i; + } + } + return k_ScriptError; + } return std::stoi(tagValue); } @@ -1633,8 +3036,12 @@ void TextSourceScript::handleTextSourceScript(Entity& src, std::string input) if ( (entity->behavior == &actMonster && attachTo == TO_MONSTERS) || (entity->behavior == &actPlayer && attachTo == TO_PLAYERS) || (entity->behavior == &actItem && attachTo == TO_ITEMS) + || (entity->behavior == &actGoldBag && attachTo == TO_GOLD) + || (entity->behavior == &actBell && attachTo == TO_BELL) + || (entity->isColliderBreakableContainer() && attachTo == TO_BREAKABLE) + || (entity->isDamageableCollider() && attachTo == TO_COLLIDER) || (entity->behavior == &actMonster - && attachTo >= TO_NOTHING && attachTo <= TO_DUMMYBOT + && attachTo >= TO_NOTHING && attachTo < TO_MONSTER_MAX && entity->getRace() == (attachTo - TO_NOTHING)) ) { // found our entity. @@ -1716,6 +3123,10 @@ void TextSourceScript::handleTextSourceScript(Entity& src, std::string input) { result = CLASS_BARBARIAN; } + if ( !enabledDLCPack3 && result >= CLASS_BARD && result <= CLASS_PALADIN ) + { + result = CLASS_BARBARIAN; + } std::vector applyToEntities; if ( processOnAttachedEntity ) @@ -2150,6 +3561,10 @@ void TextSourceScript::handleTextSourceScript(Entity& src, std::string input) } if ( multiplayer == SERVER ) { + if ( entity->behavior == &actPlayer && entity->skill[2] > 0 ) + { + serverUpdateEffects(entity->skill[2]); + } entity->serverUpdateEffectsForEntity(true); } } @@ -2177,6 +3592,10 @@ void TextSourceScript::handleTextSourceScript(Entity& src, std::string input) } if ( multiplayer == SERVER ) { + if ( entity->behavior == &actPlayer && entity->skill[2] > 0 ) + { + serverUpdateEffects(entity->skill[2]); + } entity->serverUpdateEffectsForEntity(true); } } @@ -2329,6 +3748,75 @@ void TextSourceScript::handleTextSourceScript(Entity& src, std::string input) } } } + else if ( (*it).find("@damage=") != std::string::npos ) + { + int result = textSourceProcessScriptTag(input, "@damage=", src); + if ( result != k_ScriptError ) + { + int damage = result; + if ( processOnAttachedEntity ) + { + for ( auto entity : attachedEntities ) + { + if ( entity->behavior == &actChest ) + { + entity->chestHealth -= damage; + } + else if ( entity->behavior == &actMonster ) + { + entity->modHP(-damage); + } + else if ( entity->behavior == &actDoor ) + { + entity->doorHealth -= damage; + } + else if ( entity->behavior == &actFurniture ) + { + entity->furnitureHealth -= damage; + } + else if ( entity->isDamageableCollider() ) + { + entity->colliderCurrentHP -= damage; + } + } + } + } + } + else if ( (*it).find("@explode=") != std::string::npos ) + { + int result = textSourceProcessScriptTag(input, "@explode=", src); + if ( result != k_ScriptError ) + { + int explosionSprite = (result >> 0) & 0xFFFF; + int x1 = (result >> 16) & 0xFF; + int y1 = (result >> 24) & 0xFF; + if ( processOnAttachedEntity ) + { + for ( auto entity : attachedEntities ) + { + if ( explosionSprite > 0 ) + { + spawnExplosionFromSprite(explosionSprite, entity->x, entity->y, -4 + local_rng.rand() % 8); + } + else + { + spawnExplosion(entity->x, entity->y, -4 + local_rng.rand() % 8); + } + } + } + else + { + if ( explosionSprite > 0 ) + { + spawnExplosionFromSprite(explosionSprite, x1 * 16.0 + 8.0, y1 * 16.0 + 8.0, -4 + local_rng.rand() % 8); + } + else + { + spawnExplosion(x1 * 16.0 + 8.0, y1 * 16.0 + 8.0, -4 + local_rng.rand() % 8); + } + } + } + } else if ( (*it).find("@nodropitems=") != std::string::npos ) { int result = textSourceProcessScriptTag(input, "@nodropitems=", src); @@ -2907,6 +4395,149 @@ void TextSourceScript::handleTextSourceScript(Entity& src, std::string input) } } } + else if ( (*it).find("@addtobreakable=") != std::string::npos ) + { + int result = textSourceProcessScriptTag(input, "@addtobreakable=", src); + if ( result != k_ScriptError ) + { + int x1 = result & 0xFF; + int x2 = (result >> 8) & 0xFF; + int y1 = (result >> 16) & 0xFF; + int y2 = (result >> 24) & 0xFF; + std::vector applyToEntities; + if ( processOnAttachedEntity + && (textSourceScript.getAttachedToEntityType(src.textSourceIsScript) == textSourceScript.TO_ITEMS + || textSourceScript.getAttachedToEntityType(src.textSourceIsScript) == textSourceScript.TO_GOLD) ) + { + for ( auto entity : attachedEntities ) + { + if ( textSourceScript.getAttachedToEntityType(src.textSourceIsScript) == textSourceScript.TO_ITEMS ) + { + if ( entity->behavior == &actItem ) + { + applyToEntities.push_back(entity); + } + } + else if ( textSourceScript.getAttachedToEntityType(src.textSourceIsScript) == textSourceScript.TO_GOLD ) + { + if ( entity->behavior == &actGoldBag ) + { + applyToEntities.push_back(entity); + } + } + } + Entity* breakable = nullptr; + for ( node_t* node = map.entities->first; node; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity && entity->isColliderBreakableContainer() ) + { + int findx = static_cast(entity->x) >> 4; + int findy = static_cast(entity->y) >> 4; + if ( findx >= x1 && findx <= x2 && findy >= y1 && findy <= y2 ) + { + breakable = entity; + } + } + } + if ( breakable ) + { + for ( auto entity : applyToEntities ) + { + if ( entity->behavior == &actItem ) + { + if ( breakable->colliderContainedEntity != 0 ) + { + break; // only 1 item + } + entity->itemContainer = breakable->getUID(); + } + else + { + entity->goldInContainer = breakable->getUID(); + } + entity->flags[INVISIBLE] = true; + serverUpdateEntityFlag(entity, INVISIBLE); + breakable->colliderContainedEntity = entity->getUID(); + } + } + } + else + { + // not implemented, needs to be attached to items. + } + } + } + else if ( (*it).find("@breakablespawn=") != std::string::npos ) + { + int result = textSourceProcessScriptTag(input, "@breakablespawn=", src); + if ( result != k_ScriptError ) + { + std::vector applyToEntities; + if ( processOnAttachedEntity && textSourceScript.getAttachedToEntityType(src.textSourceIsScript) == textSourceScript.TO_BREAKABLE ) + { + for ( auto entity : attachedEntities ) + { + if ( entity->isColliderBreakableContainer() ) + { + applyToEntities.push_back(entity); + } + } + + if ( result >= TO_NOTHING && result < TO_MONSTER_MAX ) + { + int monsterType = NOTHING + (result - TO_NOTHING); + for ( auto entity : applyToEntities ) + { + if ( entity->colliderHideMonster != 0 ) + { + continue; + } + entity->colliderHideMonster = monsterType; + } + } + } + else + { + // not implemented, needs to be attached to breakables. + } + } + } + else if ( (*it).find("@breakablejumpspawn=") != std::string::npos ) + { + int result = textSourceProcessScriptTag(input, "@breakablejumpspawn=", src); + if ( result != k_ScriptError ) + { + std::vector applyToEntities; + if ( processOnAttachedEntity && textSourceScript.getAttachedToEntityType(src.textSourceIsScript) == textSourceScript.TO_BREAKABLE ) + { + for ( auto entity : attachedEntities ) + { + if ( entity->isColliderBreakableContainer() ) + { + applyToEntities.push_back(entity); + } + } + + if ( result >= TO_NOTHING && result < TO_MONSTER_MAX ) + { + int monsterType = NOTHING + (result - TO_NOTHING); + for ( auto entity : applyToEntities ) + { + if ( entity->colliderHideMonster != 0 ) + { + continue; + } + entity->colliderHideMonster = 1000 + monsterType; + } + } + } + else + { + // not implemented, needs to be attached to breakables. + } + } + } else { for ( int i = 0; i < NUMSTATS; ++i ) @@ -3250,6 +4881,35 @@ void Entity::actTextSource() } else { + if ( textSourceScript.getTriggerType(textSourceIsScript) == textSourceScript.TRIGGER_ATTACHED_INTERACTED ) + { + if ( textSourceScript.getAttachedToEntityType(textSourceIsScript) == textSourceScript.TO_BELL ) + { + bool doEffect = false; + for ( node_t* node = children.first; node; node = node->next ) + { + Uint32 entityUid = *((Uint32*)node->element); + Entity* child = uidToEntity(entityUid); + if ( child ) + { + if ( child->behavior == &actBell ) + { + if ( (child->skill[0] & 0xFF) // BELL_ACTIVE_TIMER countdown + || child->skill[10] > 0 ) // BELL_BULB_BROKEN + { + doEffect = true; + } + } + } + } + if ( doEffect ) + { + textSourceScript.setScriptType(textSourceIsScript, textSourceScript.SCRIPT_ATTACHED_FIRED); + powered = true; + } + } + } + if ( textSourceScript.getTriggerType(textSourceIsScript) == textSourceScript.TRIGGER_ATTACHED_EXISTS ) { textSourceScript.setScriptType(textSourceIsScript, textSourceScript.SCRIPT_ATTACHED_FIRED); @@ -3494,6 +5154,7 @@ void TextSourceScript::playerClearInventory(bool clearStats) spellcastingAnimationManager_deactivate(&cast_animation[clientnum]); stats[clientnum]->freePlayerEquipment(); list_FreeAll(&stats[clientnum]->inventory); + list_FreeAll(&stats[clientnum]->void_chest_inventory); players[clientnum]->shootmode = true; players[clientnum]->inventoryUI.appraisal.timer = 0; players[clientnum]->inventoryUI.appraisal.current_item = 0; @@ -3550,6 +5211,52 @@ std::string TextSourceScript::getScriptFromEntity(Entity& src) return buf; } +Entity* TextSourceScript::createScriptEntityInMapGen(int x, int y, const char* text) +{ + Entity* scriptEntity = newEntity(132, 1, map.entities, nullptr); // text script + setSpriteAttributes(scriptEntity, nullptr, nullptr); + scriptEntity->x = x * 16.0; + scriptEntity->y = y * 16.0; + scriptEntity->textSourceDelay = 1; + + if ( text ) + { + addScriptToTextSource(*scriptEntity, text); + } + + return scriptEntity; +} + +void TextSourceScript::addScriptToTextSource(Entity& src, const char* text) +{ + if ( !text ) { return; } + + for ( int skill = 4; skill < 60; ++skill ) + { + if ( skill == 28 ) { continue; } + src.skill[skill] = 0; + } + + int skillnum = 4; + int len = strlen(text); + int encodeIndex = 0; + for ( int s = 0; s < len; ++s ) + { + src.skill[skillnum] |= ((text[s]) << (encodeIndex * 8)); + ++encodeIndex; + if ( encodeIndex == 4 ) + { + encodeIndex = 0; + ++skillnum; + if ( skillnum == 28 ) { ++skillnum; } + } + if ( skillnum >= 60 ) + { + break; + } + } +} + void TextSourceScript::parseScriptInMapGeneration(Entity& src) { std::string script = getScriptFromEntity(src); @@ -3620,8 +5327,12 @@ void TextSourceScript::parseScriptInMapGeneration(Entity& src) if ( (entity->behavior == &actMonster && attachTo == TO_MONSTERS) || (entity->behavior == &actPlayer && attachTo == TO_PLAYERS) || (entity->behavior == &actItem && attachTo == TO_ITEMS) + || (entity->behavior == &actGoldBag && attachTo == TO_GOLD) + || (entity->behavior == &actBell && attachTo == TO_BELL) + || (entity->isColliderBreakableContainer() && attachTo == TO_BREAKABLE) + || (entity->isDamageableCollider() && attachTo == TO_COLLIDER) || (entity->behavior == &actMonster - && attachTo >= TO_NOTHING && attachTo <= TO_DUMMYBOT + && attachTo >= TO_NOTHING && attachTo < TO_MONSTER_MAX && entity->getRace() == (attachTo - TO_NOTHING)) ) { // found our entity. @@ -3669,7 +5380,7 @@ void bellAttractMonsters(Entity* my) && myStats && entityDist(my, entity) > TOUCHRANGE ) { - if ( !myStats->EFFECTS[EFF_DISTRACTED_COOLDOWN] + if ( !myStats->getEffectActive(EFF_DISTRACTED_COOLDOWN) && entity->monsterSetPathToLocation(my->x / 16, my->y / 16, 1, GeneratePathTypes::GENERATE_PATH_DEFAULT, true) && entity->children.first ) { @@ -3721,7 +5432,16 @@ int getBellDmgOnEntity(Entity* entity) damage *= mult; } - if ( stats->helmet ) + if ( entity->onEntityTrapHitSacredPath(nullptr) ) + { + if ( entity->behavior == &actPlayer ) + { + messagePlayerColor(entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + Language::get(6491)); + } + playSoundEntity(entity, 166, 128); + } + else if ( stats->helmet ) { bool shapeshifted = (entity->behavior == &actPlayer && entity->effectShapeshift != NOTHING); @@ -3740,7 +5460,10 @@ int getBellDmgOnEntity(Entity* entity) } damage = 0; } - stats->helmet->status = BROKEN; + if ( !entity->spellEffectPreserveItem(stats->helmet) ) + { + stats->helmet->status = BROKEN; + } } else if ( stats->helmet->type == HELM_MINING ) { @@ -3766,9 +5489,12 @@ int getBellDmgOnEntity(Entity* entity) } } damage *= mult; - if ( stats->helmet->status > BROKEN ) + if ( !entity->spellEffectPreserveItem(stats->helmet) ) { - stats->helmet->status = (Status)((int)stats->helmet->status - 1); + if ( stats->helmet->status > BROKEN ) + { + stats->helmet->status = (Status)((int)stats->helmet->status - 1); + } } } @@ -3791,9 +5517,10 @@ int getBellDmgOnEntity(Entity* entity) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 0; net_packet->data[5] = stats->helmet->status; + SDLNet_Write16((int)stats->helmet->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -4233,8 +5960,8 @@ void actBell(Entity* my) bellBreakBulb(my, true); my->flags[BURNING] = false; my->flags[INVISIBLE] = true; - serverUpdateEntitySkill(my, INVISIBLE); - serverUpdateEntitySkill(my, BURNING); + serverUpdateEntityFlag(my, INVISIBLE); + serverUpdateEntityFlag(my, BURNING); if ( BELL_PULLED_TO_BREAK != 0 ) { @@ -4254,9 +5981,11 @@ void actBell(Entity* my) if ( my->flags[INVISIBLE] && my->flags[BURNING] ) { my->flags[BURNING] = false; + my->flags[BURNABLE] = false; if ( multiplayer != CLIENT ) { - serverUpdateEntitySkill(my, BURNING); + serverUpdateEntityFlag(my, BURNING); + serverUpdateEntityFlag(my, BURNABLE); } } @@ -4849,17 +6578,9 @@ void actBell(Entity* my) { int amount = 15; entity->modHP(amount); - entity->modMP(amount); - if ( svFlags & SV_FLAG_HUNGER ) - { - if ( entity->behavior == &actPlayer && stats->playerRace == RACE_INSECTOID && stats->stat_appearance == 0 ) - { - Sint32 hungerPointPerMana = entity->playerInsectoidHungerValueOfManaPoint(*stats); - stats->HUNGER += amount * hungerPointPerMana; - stats->HUNGER = std::min(999, stats->HUNGER); - serverUpdateHunger(entity->skill[2]); - } - } + int mpAmount = entity->modMP(amount); + entity->playerInsectoidIncrementHungerToMP(mpAmount); + playSoundEntity(entity, 168, 128); spawnDamageGib(entity, NOTE_DOUBLE_EIGHTH, DamageGib::DMG_STRONGEST, DamageGibDisplayType::DMG_GIB_SPRITE, true); spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); diff --git a/src/actgib.cpp b/src/actgib.cpp index e56753fa1..eacce0b2b 100644 --- a/src/actgib.cpp +++ b/src/actgib.cpp @@ -19,6 +19,7 @@ #include "player.hpp" #include "prng.hpp" #include "ui/GameUI.hpp" +#include "scores.hpp" /*------------------------------------------------------------------------------- @@ -33,11 +34,22 @@ #define GIB_VELY my->vel_y #define GIB_VELZ my->vel_z #define GIB_GRAVITY my->fskill[3] +#define GIB_SHRINK my->fskill[4] #define GIB_LIFESPAN my->skill[4] -#define GIB_PLAYER my->skill[11] #define GIB_POOF my->skill[5] #define GIB_LIGHTING my->skill[6] #define GIB_DMG_MISS my->skill[7] +#define GIB_SWIRL my->fskill[5] +#define GIB_OSC_H my->fskill[6] +#define GIB_VEL_DECAY my->fskill[7] +#define GIB_ORBIT_X my->fskill[8] +#define GIB_ORBIT_Y my->fskill[9] +#define GIB_FOCI_FLOAT1 my->fskill[10] +#define GIB_FOCI_FLOAT2 my->fskill[11] +#define GIB_FOCI_FLOAT3 my->fskill[12] +#define GIB_DELAY_MOVE my->skill[8] +#define GIB_HIT_GROUND my->skill[9] +#define GIB_PLAYER my->skill[11] void poof(Entity* my) { if (GIB_POOF) { @@ -58,7 +70,7 @@ void actGib(Entity* my) // don't update gibs that have no velocity if ( my->z == 8 && fabs(GIB_VELX) < .01 && fabs(GIB_VELY) < .01 ) { - poof(my); + poof(my); my->removeLightField(); list_RemoveNode(my->mynode); return; @@ -67,37 +79,106 @@ void actGib(Entity* my) // remove gibs that have exceeded their life span if ( my->ticks > GIB_LIFESPAN && GIB_LIFESPAN ) { - poof(my); - my->removeLightField(); - list_RemoveNode(my->mynode); - return; + if ( GIB_SHRINK > 0.0001 ) + { + my->scalex -= GIB_SHRINK; + my->scaley -= GIB_SHRINK; + my->scalez -= GIB_SHRINK; + if ( my->scalex < 0.0 ) + { + poof(my); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + else + { + poof(my); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } } if ( my->flags[OVERDRAW] - && players[clientnum] && players[clientnum]->entity && players[clientnum]->entity->skill[3] == 1 ) + && players[clientnum] && players[clientnum]->entity && players[clientnum]->entity->skill[3] != 0 ) { // debug cam, don't draw overdrawn. my->flags[INVISIBLE] = true; } // horizontal motion - my->yaw += sqrt(GIB_VELX * GIB_VELX + GIB_VELY * GIB_VELY) * .05; - my->x += GIB_VELX; - my->y += GIB_VELY; - GIB_VELX = GIB_VELX * .95; - GIB_VELY = GIB_VELY * .95; + if ( GIB_DELAY_MOVE > 0 ) + { + --GIB_DELAY_MOVE; + if ( GIB_LIFESPAN ) + { + ++GIB_LIFESPAN; + } + } + else + { + my->yaw += sqrt(GIB_VELX * GIB_VELX + GIB_VELY * GIB_VELY) * .05; + + my->x += GIB_VELX; + my->y += GIB_VELY; + if ( GIB_VEL_DECAY > 0.001 ) + { + GIB_VELX = GIB_VELX * GIB_VEL_DECAY; + GIB_VELY = GIB_VELY * GIB_VEL_DECAY; + } + else + { + GIB_VELX = GIB_VELX * .95; + GIB_VELY = GIB_VELY * .95; + } + } if ( GIB_LIGHTING ) { my->removeLightField(); } + if ( my->actGibHitGroundEvent == 1 ) + { + if ( my->sprite == 245 && my->flags[SPRITE] ) + { + if ( my->ticks % 5 == 0 ) + { + spawnGreasePuddleSpawner(my->parent == 0 ? nullptr : uidToEntity(my->parent), my->x, my->y, 30 * TICKS_PER_SECOND); + } + if ( GIB_HIT_GROUND == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 245, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(GIB_VELY, GIB_VELX); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(GIB_VELX * GIB_VELX + GIB_VELY * GIB_VELY); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->flags[BRIGHT] = true; + fx->pitch = PI / 2; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + fx->flags[SPRITE] = true; + } + } + } + } + // gravity if ( my->z < 8 ) { - GIB_VELZ += GIB_GRAVITY; - my->z += GIB_VELZ; - my->roll += 0.1; + if ( GIB_DELAY_MOVE == 0 ) + { + GIB_VELZ += GIB_GRAVITY; + my->z += GIB_VELZ; + + my->roll += 0.1; + } if ( GIB_LIGHTING && my->flags[SPRITE] && my->sprite >= 180 && my->sprite <= 184 ) { @@ -137,6 +218,17 @@ void actGib(Entity* my) } else { + if ( GIB_HIT_GROUND == 0 ) + { + GIB_HIT_GROUND = 1; + if ( my->sprite >= 1895 && my->sprite <= 1903 ) + { + spawnMiscPuddle(my, my->x, my->y, my->sprite + 8); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } GIB_VELZ = 0; my->z = 8; my->roll = PI / 2.0; @@ -160,6 +252,590 @@ void actGib(Entity* my) } } +void actFociGib(Entity* my) +{ + // remove gibs that have exceeded their life span + //if ( multiplayer != CLIENT ) + { + if ( my->ticks > GIB_LIFESPAN && GIB_LIFESPAN ) + { + if ( GIB_SHRINK > 0.0001 ) + { + my->scalex -= GIB_SHRINK; + my->scaley -= GIB_SHRINK; + my->scalez -= GIB_SHRINK; + if ( my->scalex < 0.0 ) + { + poof(my); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + else + { + poof(my); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + } + +//#ifdef USE_FMOD +// if ( my->entity_sound ) +// { +// bool isPlaying = false; +// my->entity_sound->isPlaying(&isPlaying); +// if ( isPlaying ) +// { +// FMOD_VECTOR position; +// position.x = (float)(my->x / (real_t)16.0); +// position.y = (float)(0.0); +// position.z = (float)(my->y / (real_t)16.0); +// fmod_result = my->entity_sound->set3DAttributes(&position, nullptr); +// } +// } +//#endif + + Entity* parent = uidToEntity(my->parent); + + // horizontal motion + //if ( multiplayer != CLIENT ) + { + if ( GIB_DELAY_MOVE > 0 ) + { + --GIB_DELAY_MOVE; + if ( GIB_LIFESPAN ) + { + ++GIB_LIFESPAN; + } + } + else + { + my->focalx = 0.0; + my->focaly = 0.0; + bool tryHitEntity = false; + + if ( GIB_SWIRL > 0.00001 ) + { + my->yaw += GIB_SWIRL; + Uint32 vortexStart = TICKS_PER_SECOND / 8; + if ( my->ticks >= vortexStart ) + { + real_t vortexTime = 0.5 * TICKS_PER_SECOND; + real_t ratio = 2.0 + 2.0 * sin((-PI / 2) + (PI)*std::min((Uint32)vortexTime, my->ticks - vortexStart) / vortexTime); + my->focalx += ratio * cos(my->yaw); + my->focaly += ratio * sin(my->yaw); + } + } + else + { + if ( my->actGibMagicParticle > 0 ) + { + my->pitch = atan(my->vel_z / std::max(1.0, sqrt(GIB_VELX * GIB_VELX + GIB_VELY * GIB_VELY))); + + if ( my->sprite == 2153 || my->sprite == 2155 ) + { + if ( my->ticks % 4 == 0 ) + { + my->roll += PI / 2;// (local_rng.rand() % 8)* PI / 2; + } + my->focalx = 4.0; + my->flags[INVISIBLE] = true; + } + else if ( my->sprite >= 233 && my->sprite <= 244 ) + { + if ( my->ticks % 4 == 0 ) + { + if ( my->sprite < 244 ) + { + ++my->sprite; + } + } + } + else if ( my->sprite == 2154 ) + { + my->x = GIB_ORBIT_X; + my->y = GIB_ORBIT_Y; + + real_t scale = 2.0; + GIB_FOCI_FLOAT2 += 0.2; + real_t ratio = scale + scale * sin(GIB_FOCI_FLOAT2); + + real_t dir = atan2(my->vel_y, my->vel_x); + my->x -= 2.0 * cos(dir) * sin(std::min(PI, GIB_FOCI_FLOAT2)); + my->y -= 2.0 * sin(dir) * sin(std::min(PI, GIB_FOCI_FLOAT2)); + + if ( GIB_FOCI_FLOAT1 == 0 ) + { + GIB_FOCI_FLOAT1 = -1 + ((local_rng.rand() % 2) ? 2 : 0); + } + + my->x += 4.0 * GIB_FOCI_FLOAT1 * cos(dir + PI / 2) * sin(std::min(PI, GIB_FOCI_FLOAT2 * 0.4)); + my->y += 4.0 * GIB_FOCI_FLOAT1 * sin(dir + PI / 2) * sin(std::min(PI, GIB_FOCI_FLOAT2 * 0.4)); + + if ( GIB_FOCI_FLOAT2 >= PI ) + { + //GIB_ORBIT_X += my->vel_x; + //GIB_ORBIT_Y += my->vel_y; + + real_t dist = clipMove(&GIB_ORBIT_X, &GIB_ORBIT_Y, my->vel_x, my->vel_y, my); + if ( multiplayer != CLIENT ) + { + if ( dist != sqrt(pow(my->vel_x, 2) + pow(my->vel_y, 2)) ) + { + if ( hit.entity ) + { + tryHitEntity = true; + } + } + } + } + else + { + /*if ( parent ) + { + GIB_ORBIT_X = parent->x; + GIB_ORBIT_Y = parent->y; + }*/ + } + + GIB_FOCI_FLOAT3 += 0.3; + my->focalz = 0.25; + my->roll = fmod(GIB_FOCI_FLOAT3, 2 * PI); + } + } + } + + if ( abs(GIB_OSC_H) > 0.00001 ) + { + Uint32 vortexStart = 0;// TICKS_PER_SECOND / 8; + if ( my->ticks >= vortexStart ) + { + //my->yaw = atan2(my->vel_y, my->vel_x); + real_t vortexTime = 0.2 * TICKS_PER_SECOND; + real_t magnitude = GIB_OSC_H; + real_t ratio = magnitude * sin((PI) * ((my->ticks - vortexStart) / vortexTime)); + real_t tangent = 0.0;// my->yaw - atan2(my->vel_y, my->vel_x); + my->focalx += ratio * cos(tangent); + my->focaly += ratio * sin(tangent); + } + } + + bool doMove = true; + if ( my->sprite == 2154 /*&& GIB_FOCI_FLOAT2 < PI*/ ) + { + doMove = false; + } + else + { + real_t dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); + if ( multiplayer != CLIENT ) + { + if ( dist != sqrt(pow(my->vel_x, 2) + pow(my->vel_y, 2)) ) + { + if ( hit.entity ) + { + tryHitEntity = true; + } + } + } + } + + if ( multiplayer != CLIENT ) + { + spell_t* spell = nullptr; + if ( my->children.first && my->children.first->element ) + { + spell = (spell_t*)(my->children.first->element); + } + + /*Entity* particle = spawnMagicParticle(my); + particle->sprite = 942; + particle->x = my->x + my->sizex; + particle->y = my->y + my->sizey; + particle->z = 0; + + particle = spawnMagicParticle(my); + particle->sprite = 942; + particle->x = my->x - my->sizex; + particle->y = my->y + my->sizey; + particle->z = 0; + + particle = spawnMagicParticle(my); + particle->sprite = 942; + particle->x = my->x + my->sizex; + particle->y = my->y - my->sizey; + particle->z = 0; + + particle = spawnMagicParticle(my); + particle->sprite = 942; + particle->x = my->x - my->sizex; + particle->y = my->y - my->sizey; + particle->z = 0;*/ + + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end() && spell; ++it ) + { + list_t* currentList = *it; + bool lastEntity = false; + for ( node_t* node = currentList->first;; ) + { + Entity* entity = nullptr; + if ( node == nullptr ) // at the end of the list try the hit.entity from clipMove + { + lastEntity = true; + if ( tryHitEntity ) + { + entity = hit.entity; + tryHitEntity = false; + } + else + { + break; + } + } + else + { + entity = (Entity*)node->element; + node = node->next; + } + if ( entity == parent || entity == my ) + { + continue; + } + + bool nonCreatureDamage = true; + if ( spell->ID == SPELL_FOCI_SNOW ) + { + nonCreatureDamage = false; + } + + if ( entity->behavior != &actMonster + && !entity->isInertMimic() + && entity->behavior != &actPlayer ) + { + if ( nonCreatureDamage ) + { + if ( entity->behavior != &actDoor + && entity->behavior != &::actIronDoor + && !(entity->isDamageableCollider() && entity->isColliderDamageableByMagic()) + && entity->behavior != &::actChest + && entity->behavior != &::actFurniture ) + { + if ( (spell->ID == SPELL_FOCI_FIRE || spell->ID == SPELL_BREATHE_FIRE) ) + { + if ( entity->behavior == &actBell || entity->behavior == &actGreasePuddleSpawner ) + { + if ( entityInsideEntity(entity, my) ) + { + if ( entity->SetEntityOnFire(parent) && entity->flags[BURNING] ) + { + if ( entity->behavior == &actBell ) + { + if ( parent && parent->behavior == &actPlayer ) + { + entity->skill[13] = parent->getUID(); // burning inflicted by for bell + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_INTERACTION, Language::get(6297)); + } + } + } + } + } + } + } + continue; + } + } + else + { + continue; + } + } + + if ( !lastEntity && !entityInsideEntity(entity, my) ) + { + continue; + } + + auto hitprops = getParticleEmitterHitProps(my->getUID(), entity); + if ( hitprops ) + { + if ( hitprops->hits > 0 ) + { + continue; + } + } + + if ( entity->getStats() ) + { + if ( !entity->monsterIsTargetable(nonCreatureDamage ? true : false) ) + { + if ( hitprops ) + { + hitprops->hits++; + } + continue; + } + + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( parent && parent->checkFriend(entity) && parent->friendlyFireProtection(entity) ) + { + if ( hitprops ) + { + hitprops->hits++; + } + continue; + } + } + } + else if ( entity->flags[PASSABLE] ) + { + if ( hitprops ) + { + hitprops->hits++; + } + continue; + } + + Uint32 targetUid = entity->getUID(); + if ( Entity* entity = newEntity(my->sprite, 1, map.entities, nullptr) ) + { + entity->behavior = &actMagicMissile; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + entity->yaw = my->yaw; + entity->vel_x = cos(my->yaw); + entity->vel_y = sin(my->yaw); + entity->parent = parent ? parent->getUID() : 0; + + entity->flags[INVISIBLE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[UNCLICKABLE] = true; + + entity->skill[4] = 0; // life start + entity->skill[5] = 2; //lifetime + entity->actmagicSpray = 2; + entity->actmagicOrbitHitTargetUID4 = targetUid; + entity->actmagicReflectionCount = my->actmagicReflectionCount; + entity->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + entity->actmagicFromSpellbook = my->actmagicFromSpellbook; + + node_t* node = list_AddNodeFirst(&entity->children); + node->element = copySpell(spell); + ((spell_t*)node->element)->caster = parent ? parent->getUID() : 0; + node->deconstructor = &spellDeconstructor; + node->size = sizeof(spell_t); + + --entity_uids; + entity->setUID(-3); + } + + if ( hitprops ) + { + hitprops->hits++; + } + } + } + } + + if ( my->sprite == 2154 ) + { + // no decay + } + else if ( GIB_VEL_DECAY > 0.001 ) + { + GIB_VELX = GIB_VELX * GIB_VEL_DECAY; + GIB_VELY = GIB_VELY * GIB_VEL_DECAY; + } + else + { + GIB_VELX = GIB_VELX * .95; + GIB_VELY = GIB_VELY * .95; + } + } + } + + if ( GIB_LIGHTING ) + { + my->removeLightField(); + } + + if ( my->actGibMagicParticle > 0 ) + { + if ( my->sprite == 2154 ) + { + if ( GIB_FOCI_FLOAT2 < PI ) + { + } + else + { + int sprite = my->actGibMagicParticle; + if ( local_rng.rand() % 2 == 0 ) + { + sprite = 260; + } + if ( Entity* fx = spawnMagicParticleCustom(my, sprite, my->scalex, sprite == 260 ? 1.0 : 5.0) ) + { + fx->flags[SPRITE] = true;// my->flags[SPRITE]; + fx->ditheringDisabled = true; + fx->fskill[1] = 0.1; // decay size + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + + real_t dir = atan2(my->vel_y, my->vel_x); + fx->x -= 4.0 * cos(dir); + fx->y -= 4.0 * sin(dir); + } + } + } + else if ( my->sprite == 2153 || my->sprite == 2155 ) + { + /*if ( ticks % 4 == 0 ) + { + if ( Entity* particle = spawnMagicParticleCustom(my, 1764, my->scalex * 1.0, 1) ) + { + particle->z = 6.0; + particle->x = my->x; + particle->y = my->y; + particle->yaw = local_rng.rand() % 360 * PI / 180; + particle->vel_x = 0.05 * cos(particle->yaw); + particle->vel_y = 0.05 * sin(particle->yaw); + particle->ditheringDisabled = true; + } + }*/ + + if ( my->ticks % 2 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->actGibMagicParticle, my->scalex, 10.0) ) + { + fx->ditheringDisabled = true; + fx->fskill[1] = 0.05; // decay size + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + + fx->roll = my->roll + PI / 2; + } + } + } + else if ( my->sprite == 2152 || my->sprite == 256 ) + { + //if ( Entity* fx = spawnMagicParticleCustom(my, my->actGibMagicParticle, 1.0, 1.0) ) + //{ + // fx->flags[SPRITE] = my->flags[SPRITE]; + // fx->ditheringDisabled = true; + // fx->fskill[1] = 0.05; // decay size + // fx->focalx = my->focalx; + // fx->focaly = my->focaly; + // fx->focalz = my->focalz; + + // real_t dir = atan2(my->vel_y, my->vel_x); + // fx->x -= 2.0 * cos(dir); + // fx->y -= 2.0 * sin(dir); + //} + if ( Entity* fx = spawnMagicParticleCustom(my, 256, 1.0, 1.0) ) + { + fx->flags[SPRITE] = true; + fx->ditheringDisabled = true; + fx->fskill[1] = 0.05; // decay size + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + + real_t dir = atan2(my->vel_y, my->vel_x); + fx->x -= 2.0 * cos(dir); + fx->y -= 2.0 * sin(dir); + } + } + else if ( my->sprite == 2156 ) + { + my->roll = fmod(my->roll + 0.1, 2 * PI); + if ( Entity* fx = spawnMagicParticleCustom(my, my->actGibMagicParticle, 1.0, 1.0) ) + { + fx->flags[SPRITE] = my->flags[SPRITE]; + fx->ditheringDisabled = true; + fx->fskill[1] = 0.05; // decay size + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + + real_t dir = atan2(my->vel_y, my->vel_x); + fx->x -= 2.0 * cos(dir); + fx->y -= 2.0 * sin(dir); + } + } + else if ( my->sprite >= 233 && my->sprite <= 244 ) + { + if ( my->ticks % 2 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->actGibMagicParticle, 1.0, 1.0) ) + { + fx->sprite = my->sprite; + fx->flags[SPRITE] = my->flags[SPRITE]; + fx->ditheringDisabled = true; + fx->fskill[1] = 0.025; // decay size + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + + real_t dir = atan2(my->vel_y, my->vel_x); + fx->x -= 2.0 * cos(dir); + fx->y -= 2.0 * sin(dir); + } + } + } + else + { + if ( my->ticks % 2 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->actGibMagicParticle, 1.0, 1.0) ) + { + fx->flags[SPRITE] = true;// = my->flags[SPRITE]; + fx->ditheringDisabled = true; + fx->fskill[1] = 0.05; // decay size + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + + real_t dir = atan2(my->vel_y, my->vel_x); + fx->x -= 2.0 * cos(dir); + fx->y -= 2.0 * sin(dir); + } + } + } + } + + // gravity + if ( GIB_DELAY_MOVE == 0 ) + { + GIB_VELZ += GIB_GRAVITY; + my->z += GIB_VELZ; + } + + if ( GIB_LIGHTING ) + { + my->removeLightField(); + const char* lightname = nullptr; + if ( my->ticks > 0 && my->ticks % 12 == 0 && flickerLights ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, true)); + } + else + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, false)); + } + } +} + void actDamageGib(Entity* my) { my->flags[INVISIBLE] = !spawn_blood && !my->flags[SPRITE] && my->sprite == 5; @@ -185,7 +861,7 @@ void actDamageGib(Entity* my) GIB_VELX = GIB_VELX * .95; GIB_VELY = GIB_VELY * .95; - if ( my->skill[3] == DMG_WEAKER || my->skill[3] == DMG_WEAKEST || my->skill[3] == DMG_MISS ) + if ( my->skill[3] == DMG_WEAKER || my->skill[3] == DMG_WEAKEST || my->skill[3] == DMG_MISS || my->skill[3] == DMG_GUARD ) { real_t scale = 0.2; if ( my->ticks > 10 ) @@ -198,6 +874,7 @@ void actDamageGib(Entity* my) } else if ( my->skill[3] == DMG_STRONGER || my->skill[3] == DMG_STRONGEST + || my->skill[3] == DMG_GUARD || my->skill[3] == DMG_MISS ) { real_t scale = 0.2; @@ -210,7 +887,7 @@ void actDamageGib(Entity* my) { scale *= anim[my->ticks] / 100.0; } - if ( my->skill[3] == DMG_MISS ) + if ( my->skill[3] == DMG_MISS || my->skill[3] == DMG_GUARD ) { scale = 0.2; } @@ -296,9 +973,9 @@ Entity* spawnGib(Entity* parentent, int customGibSprite) if ( (parentstats = parentent->getStats()) != nullptr ) { - if ( multiplayer == CLIENT ) + if ( multiplayer == CLIENT && customGibSprite == -1 ) { - printlog("[%s:%d spawnGib()] spawnGib() called on client, got clientstats. Probably bad?", __FILE__, __LINE__); + printlog("spawnGib() called on client, got clientstats. Probably bad?"); } if ( customGibSprite != -1 ) @@ -351,7 +1028,7 @@ Entity* spawnGib(Entity* parentent, int customGibSprite) gibsprite = 683; break; case 5: - if (parentstats->HP > 0) { + if (parentstats->HP > 0 || parentstats->type == REVENANT_SKULL ) { return nullptr; } gibsprite = 688; @@ -405,31 +1082,326 @@ Entity* spawnGib(Entity* parentent, int customGibSprite) return entity; } -Entity* spawnDamageGib(Entity* parentent, Sint32 dmgAmount, int gibDmgType, int displayType, bool updateClients) +Entity* spawnFociGib(real_t x, real_t y, real_t z, real_t dir, real_t velocityBonus, Uint32 parentUid, int sprite, Uint32 seed) { - if ( !parentent ) + Entity* my = newEntity(sprite, 1, map.entities, nullptr); //Gib entity. + if ( !my ) { return nullptr; } - Entity* entity = newEntity(displayType == DamageGibDisplayType::DMG_GIB_SPRITE ? dmgAmount : -1, 1, map.entities, nullptr); - if ( !entity ) + dir = fmod(dir, 2 * PI); + + BaronyRNG rng; + rng.seedBytes(&seed, sizeof(Uint32)); + +#ifndef NDEBUG + static ConsoleVariable cvar_foci_vel("/foci_vel", 1.f); + static ConsoleVariable cvar_foci_vel_decay("/foci_vel_decay", 0.95); + static ConsoleVariable cvar_foci_life("/foci_life", 1.f); + static ConsoleVariable cvar_foci_spread("/foci_spread", 1.f); + static ConsoleVariable cvar_foci_delay("/foci_delay", 0); + static ConsoleVariable cvar_foci_model("/foci_model", false); + static ConsoleVariable cvar_foci_scale("/foci_scale", 1.f); + static ConsoleVariable cvar_foci_shrink("/foci_shrink", 0.1); + static ConsoleVariable cvar_foci_osc_h("/foci_osc_h", 0.0); + static ConsoleVariable cvar_foci_swirl("/foci_swirl", 0.2); + static ConsoleVariable cvar_foci_particle("/foci_particle", 0); + static ConsoleVariable cvar_foci_invertz("/foci_invertz", false); + static ConsoleVariable cvar_foci_gravity("/foci_gravity", 1.0); + static ConsoleVariable cvar_foci_velz("/foci_velz", 1.0); + + real_t foci_vel = *cvar_foci_vel; + real_t foci_vel_decay = *cvar_foci_vel_decay; + real_t foci_life = *cvar_foci_life; + real_t foci_spread = *cvar_foci_spread; + int foci_delay = *cvar_foci_delay; + bool foci_model = *cvar_foci_model; + real_t foci_scale = *cvar_foci_scale; + real_t foci_shrink = *cvar_foci_shrink; + real_t foci_osc_h = *cvar_foci_osc_h; + real_t foci_swirl = *cvar_foci_swirl; + int foci_particle = *cvar_foci_particle; + bool foci_invertz = *cvar_foci_invertz; + real_t foci_gravity = *cvar_foci_gravity; + real_t foci_velz = *cvar_foci_velz; +#else + real_t foci_vel = 1.0; + real_t foci_vel_decay = 0.95; + real_t foci_life = 1.0; + real_t foci_spread = 1.0; + int foci_delay = 0; + bool foci_model = false; + real_t foci_scale = 1.0; + real_t foci_shrink = 0.1; + real_t foci_osc_h = 0.0; + real_t foci_swirl = 0.2; + int foci_particle = 0; + bool foci_invertz = false; + real_t foci_gravity = 1.0; + real_t foci_velz = 1.0; +#endif + + int sfx = 164; + int vol = 64; + + my->x = x; + my->y = y; + my->z = z; + int lifetime = (0.4 * TICKS_PER_SECOND) + (rng.rand() % 5); + my->flags[INVISIBLE] = false; + + if ( sprite == 2153 ) { - return nullptr; + my->flags[INVISIBLE] = true; + foci_vel = 1.5; + foci_vel_decay = 0.95; + foci_life = 1.0; + foci_spread = 1.0; + foci_delay = 0; + + sfx = 817 + local_rng.rand() % 2; + vol = 128; + + foci_model = true; + foci_scale = 1.0; + foci_shrink = 0.1; + foci_osc_h = 0.0; + foci_swirl = 0.0; + + foci_particle = sprite; + foci_invertz = false; + foci_gravity = 1.0; + foci_velz = 1.0; } - entity->x = parentent->x; - entity->y = parentent->y; - entity->z = parentent->z - 4; - entity->parent = parentent->getUID(); - entity->sizex = 1; - entity->sizey = 1; - real_t vel = (local_rng.rand() % 10) / 20.f; - entity->vel_z = -.5; - if ( gibDmgType == DMG_STRONGER || gibDmgType == DMG_STRONGEST || gibDmgType == DMG_MISS ) + else if ( sprite == 2154 ) { - vel = 0.25; - entity->vel_z = -.4; - } + foci_vel = 1.5; + foci_vel_decay = 0.95; + foci_life = 1.0; + foci_spread = 0.25; + foci_delay = 0; + + sfx = 814; + vol = 92; + + foci_model = true; + foci_scale = 1.0; + foci_shrink = 0.2; + foci_osc_h = 0.0; + foci_swirl = 0.0; + + foci_particle = 257; + foci_invertz = false; + foci_gravity = 0.0; + foci_velz = 0.0; + + my->x += 6.0 * cos(dir); + my->y += 6.0 * sin(dir); + lifetime += 10; + } + else if ( sprite == 255 || (sprite >= 233 && sprite <= 244) ) + { + foci_vel = 1.5; + foci_vel_decay = 0.95; + foci_life = 1.0; + foci_spread = 1.0; + foci_delay = 0; + + foci_model = false; + foci_scale = 1.0; + foci_shrink = 0.1; + foci_osc_h = 1.0; + foci_swirl = 0.0; + + foci_particle = sprite; + foci_invertz = false; + foci_gravity = 1.0; + foci_velz = 1.0; + } + else if ( sprite == 256 || sprite == 2152 ) + { + foci_vel = 1.5; + foci_vel_decay = 0.95; + foci_life = 1.0; + foci_spread = 0.5; + foci_delay = 0; + + sfx = 813; + vol = 128; + + if ( sprite == 2152 ) + { + foci_model = true; + } + else + { + foci_model = false; + } + foci_scale = 1.0; + foci_shrink = 0.1; + foci_osc_h = 0.0; + foci_swirl = 0.0; + + foci_particle = sprite; + foci_invertz = false; + foci_gravity = -0.2; + foci_velz = -0.25; + } + else if ( sprite == 2156 ) + { + foci_vel = 1.5; + foci_vel_decay = 0.95; + foci_life = 1.0; + foci_spread = 0.5; + foci_delay = 0; + + sfx = 815; + vol = 128; + + foci_model = true; + foci_scale = 1.0; + foci_shrink = 0.1; + foci_osc_h = 0.0; + foci_swirl = 0.0; + + foci_particle = 2157; + foci_invertz = false; + foci_gravity = -0.2; + foci_velz = -0.25; + } + + my->parent = parentUid; + my->behavior = &actFociGib; + my->ditheringDisabled = true; + my->flags[SPRITE] = !foci_model; + my->flags[PASSABLE] = true; + my->flags[NOUPDATE] = false; + my->flags[UPDATENEEDED] = false; + my->flags[UNCLICKABLE] = true; + my->flags[NOCLIP_CREATURES] = true; + //my->flags[BRIGHT] = true; + my->lightBonus = vec4_t{ 0.25f, 0.25f, 0.25f, 0.f }; + + my->sizex = 3; + my->sizey = 3; + real_t spread = 0.2 * foci_spread; + my->yaw = dir - spread + ((rng.rand() % 21) * (spread / 10)); + my->pitch = 0.0; //(rng.rand() % 360)* PI / 180.0; + my->roll = 0.0; //(rng.rand() % 360)* PI / 180.0; + double vel = velocityBonus + foci_vel * (20 + (rng.rand() % 5)) / 10.f; + my->vel_x = vel * cos(my->yaw); + my->vel_y = vel * sin(my->yaw); + + /*real_t movementDir = atan2(parent->vel_y, parent->vel_x); + real_t particleDir = fmod(my->yaw, 2 * PI); + real_t yawDiff = movementDir - particleDir; + while ( yawDiff > PI ) + { + yawDiff -= 2 * PI; + } + while ( yawDiff <= -PI ) + { + yawDiff += 2 * PI; + } + messagePlayer(0, MESSAGE_DEBUG, "%.2f", yawDiff); + if ( abs(yawDiff) <= PI ) + { + entity->vel_x += parent->vel_x; + entity->vel_y += parent->vel_y; + }*/ + my->vel_z = .6; + my->scalex = foci_scale; + my->scaley = foci_scale; + my->scalez = foci_scale; + real_t gravity = -0.035 - 0.002 * (rng.rand() % 6); + gravity *= foci_gravity; + my->vel_z *= foci_velz; + if ( foci_invertz ) + { + gravity *= -1; + my->vel_z *= -0.5; + } + + // swirl + if ( foci_swirl > 0.0001 ) + { + //entity->yaw += (my->ticks % 10) * (2 * PI / 5); + lifetime += 5; + } + lifetime *= foci_life; + + GIB_GRAVITY = gravity; + GIB_LIGHTING = 1; + GIB_LIFESPAN = lifetime; + GIB_DELAY_MOVE = foci_delay; // delay velx/y movement + GIB_SHRINK = foci_shrink * foci_scale; // shrink at end of life + GIB_SWIRL = foci_swirl; + GIB_OSC_H = foci_osc_h;// *((my->ticks % 2) ? 1 : -1); + GIB_VEL_DECAY = foci_vel_decay; + my->actGibMagicParticle = foci_particle; + GIB_ORBIT_X = my->x; + GIB_ORBIT_Y = my->y; + + playSoundEntityLocal(my, sfx, vol); +//#ifdef USE_FMOD +// my->entity_sound = playSoundEntityLocal(my, sfx, vol); +//#else +//#endif + + if ( multiplayer == SERVER ) + { + for ( int c = 1; c < MAXPLAYERS; c++ ) + { + if ( client_disconnected[c] || players[c]->isLocalPlayer() ) + { + continue; + } + strcpy((char*)net_packet->data, "FOCI"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + SDLNet_Write16((Sint16)(x * 32), &net_packet->data[8]); + SDLNet_Write16((Sint16)(y * 32), &net_packet->data[10]); + SDLNet_Write16((Sint16)(z * 32), &net_packet->data[12]); + SDLNet_Write16((Sint16)(dir * 256), &net_packet->data[14]); + SDLNet_Write16((Sint16)(sprite), &net_packet->data[16]); + SDLNet_Write32(seed, &net_packet->data[18]); + SDLNet_Write16((Sint16)(velocityBonus * 256), &net_packet->data[22]); + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 24; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } + } + + return my; +} + +Entity* spawnDamageGib(Entity* parentent, Sint32 dmgAmount, int gibDmgType, int displayType, bool updateClients) +{ + if ( gibDmgType == DMG_DETECT_MONSTER ) { return nullptr; } + if ( !parentent ) + { + return nullptr; + } + + Entity* entity = newEntity(displayType == DamageGibDisplayType::DMG_GIB_SPRITE ? dmgAmount : -1, 1, map.entities, nullptr); + if ( !entity ) + { + return nullptr; + } + entity->x = parentent->x; + entity->y = parentent->y; + entity->z = parentent->z - 4; + entity->parent = parentent->getUID(); + entity->sizex = 1; + entity->sizey = 1; + real_t vel = (local_rng.rand() % 10) / 20.f; + entity->vel_z = -.5; + if ( gibDmgType == DMG_STRONGER || gibDmgType == DMG_STRONGEST || gibDmgType == DMG_MISS || gibDmgType == DMG_GUARD ) + { + vel = 0.25; + entity->vel_z = -.4; + } if ( parentent->isDamageableCollider() ) { entity->z -= 4; @@ -438,6 +1410,38 @@ Entity* spawnDamageGib(Entity* parentent, Sint32 dmgAmount, int gibDmgType, int { entity->z += 8.0; } + if ( (parentent->behavior == &actMonster || parentent->behavior == &actDeathGhost) ) + { + if ( parentent->behavior == &actDeathGhost ) + { + if ( node_t* node = list_Node(&parentent->children, 2) ) + { + if ( Entity* entity2 = (Entity*)node->element ) + { + if ( Entity::getMonsterTypeFromSprite(entity2->sprite) == DUCK_SMALL ) + { + if ( node_t* node = list_Node(&entity2->children, 2) ) + { + if ( Entity* entity3 = (Entity*)node->element ) + { + entity->z = entity3->z - 4; + } + } + } + } + } + } + else if ( parentent->behavior == &actMonster && Entity::getMonsterTypeFromSprite(parentent->sprite) == DUCK_SMALL ) + { + if ( node_t* node = list_Node(&parentent->children, 2) ) + { + if ( Entity* entity2 = (Entity*)node->element ) + { + entity->z = entity2->z - 4; + } + } + } + } entity->yaw = (local_rng.rand() % 360) * PI / 180.0; entity->vel_x = vel * cos(entity->yaw); entity->vel_y = vel * sin(entity->yaw); @@ -494,6 +1498,7 @@ Entity* spawnDamageGib(Entity* parentent, Sint32 dmgAmount, int gibDmgType, int case DMG_POISON: break; case DMG_MISS: + case DMG_GUARD: break; case DMG_HEAL: color = hudColors.characterSheetGreen; @@ -545,11 +1550,16 @@ Entity* spawnGibClient(Sint16 x, Sint16 y, Sint16 z, Sint16 sprite) entity->pitch = (local_rng.rand() % 360) * PI / 180.0; entity->roll = (local_rng.rand() % 360) * PI / 180.0; vel = (local_rng.rand() % 10) / 10.f; + if ( sprite >= 1871 && sprite <= 1876 ) // earth sprite + { + vel *= 0.1; + } entity->vel_x = vel * cos(entity->yaw); entity->vel_y = vel * sin(entity->yaw); entity->vel_z = -.5; entity->fskill[3] = 0.04; entity->behavior = &actGib; + entity->ditheringDisabled = true; entity->flags[PASSABLE] = true; entity->flags[NOUPDATE] = true; entity->flags[UNCLICKABLE] = true; @@ -578,7 +1588,8 @@ void serverSpawnGibForClient(Entity* gib) SDLNet_Write16((Sint16)gib->y, &net_packet->data[6]); SDLNet_Write16((Sint16)gib->z, &net_packet->data[8]); SDLNet_Write16((Sint16)gib->sprite, &net_packet->data[10]); - net_packet->data[12] = gib->flags[SPRITE]; + net_packet->data[12] = gib->flags[SPRITE] ? 1 << 0 : 0; + net_packet->data[12] |= (gib->skill[5] == 1) ? 1 << 1 : 0; // poof net_packet->address.host = net_clients[c - 1].host; net_packet->address.port = net_clients[c - 1].port; net_packet->len = 13; @@ -586,3 +1597,1138 @@ void serverSpawnGibForClient(Entity* gib) } } } + +void spawnGreasePuddleSpawner(Entity* caster, real_t x, real_t y, int duration) +{ + if ( multiplayer == CLIENT ) { return; } + int ox = x / 16; + int oy = y / 16; + + if ( ox >= 0 && ox < map.width && oy >= 0 && oy < map.height ) + { + int mapIndex = oy * MAPLAYERS + ox * MAPLAYERS * map.height; + if ( !map.tiles[mapIndex] ) + { + return; + } + auto entLists = TileEntityList.getEntitiesWithinRadius(ox, oy, 0); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + for ( node_t* node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actGreasePuddleSpawner && entity->skill[0] > 0 ) + { + entity->skill[0] = std::max(entity->skill[0], duration); + return; + } + } + } + Entity* entity = newEntity(1786, 1, map.entities, nullptr); //Blood/gib entity. + real_t x = ox * 16.0 + 8.0; + real_t y = oy * 16.0 + 8.0; + entity->x = x; + entity->y = y; + entity->z = 7.5; + entity->parent = caster ? caster->getUID() : 0; + entity->behavior = &actGreasePuddleSpawner; + entity->sizex = 4; + entity->sizey = 4; + entity->skill[0] = duration; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + entity->flags[SPRITE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[INVISIBLE] = true; + entity->flags[BURNABLE] = true; + TileEntityList.addEntity(*entity); + } +} + +void spawnGreasePuddle(Entity* parent, real_t x, real_t y, int duration, int location) +{ + if ( !parent ) { return; } + int ox = x / 16; + int oy = y / 16; + if ( ox >= 0 && ox < map.width && oy >= 0 && oy < map.height ) + { + Entity* entity = newEntity(1784, 1, map.entities, nullptr); //Blood/gib entity. + real_t x = ox * 16.0 + 8.0; + real_t y = oy * 16.0 + 8.0; + + static const std::vector locations = { + 0 * PI / 4, + 5 * PI / 4, + 2 * PI / 4, + 3 * PI / 4, + 4 * PI / 4, + 7 * PI / 4, + 1 * PI / 4, + 6 * PI / 4, + }; + + x += 4.0 * cos(locations[location]) + 2.0 * (local_rng.rand() % 10) / 10.0; + y += 4.0 * sin(locations[location]) + 2.0 * (local_rng.rand() % 10) / 10.0; + + entity->x = x; + entity->y = y; + entity->z = 8 + (local_rng.rand() % 20) / 100.0; + entity->parent = 0; + entity->sizex = 2; + entity->sizey = 2; + entity->behavior = &actGreasePuddle; + entity->parent = parent->getUID(); + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->flags[UPDATENEEDED] = false; + entity->flags[NOUPDATE] = true; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + if ( multiplayer != CLIENT ) + { + --entity_uids; + } + entity->setUID(-3); + } +} + +void actGreasePuddle(Entity* my) +{ + if ( my->ticks % 10 == 0 ) + { + if ( my->parent == 0 || !uidToEntity(my->parent) ) + { + list_RemoveNode(my->mynode); + return; + } + } +} + +void actGreasePuddleSpawner(Entity* my) +{ + my->flags[INVISIBLE] = true; + int x = my->x / 16; + int y = my->y / 16; + if ( multiplayer != CLIENT ) + { + if ( my->flags[BURNING] ) + { + int player = achievementObserver.checkUidIsFromPlayer(my->parent); + if ( player >= 0 ) + { + if ( achievementObserver.playerAchievements[player].hellsKitchen >= 0 ) + { + achievementObserver.playerAchievements[player].hellsKitchen++; + if ( achievementObserver.playerAchievements[player].hellsKitchen >= 25 ) + { + achievementObserver.playerAchievements[player].hellsKitchen = -1; + steamAchievementClient(player, "BARONY_ACH_HELLS_KITCHEN"); + } + } + } + } + + --my->skill[0]; + if ( my->skill[0] <= 0 ) + { + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + bool foundGrease = false; + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 0); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end() && !foundGrease; ++it ) + { + list_t* currentList = *it; + for ( node_t* node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity && entity->behavior == &actGreasePuddleSpawner && entity != my ) + { + int x2 = entity->x / 16; + int y2 = entity->y / 16; + if ( x2 == x && y2 == y ) + { + foundGrease = true; + break; + } + } + } + } + + if ( !foundGrease ) + { + if ( map.tileHasAttribute(x, y, 0, map_t::TILE_ATTRIBUTE_GREASE) ) + { + map.tileAttributes[0 + (y * MAPLAYERS) + (x * MAPLAYERS * map.height)] &= ~map_t::TILE_ATTRIBUTE_GREASE; + serverUpdateMapTileFlag(x, y, 0, 0, map_t::TILE_ATTRIBUTE_GREASE); + } + } + } + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + if ( !map.tileHasAttribute(x, y, 0, map_t::TILE_ATTRIBUTE_GREASE) ) + { + map.tileAttributes[0 + (y * MAPLAYERS) + (x * MAPLAYERS * map.height)] |= map_t::TILE_ATTRIBUTE_GREASE; + serverUpdateMapTileFlag(x, y, 0, map_t::TILE_ATTRIBUTE_GREASE, 0); + } + + int mapIndex = y * MAPLAYERS + x * MAPLAYERS * map.height; + if ( lavatiles[map.tiles[mapIndex]] ) + { + if ( !my->flags[BURNING] ) + { + my->SetEntityOnFire(nullptr); + } + } + + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 0); + Entity* parent = my->parent == 0 ? nullptr : uidToEntity(my->parent); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + for ( node_t* node = currentList->first; node != nullptr; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( Stat* stats = entity->getStats() ) + { + if ( entity->monsterIsTargetable() ) + { + if ( !stats->getEffectActive(EFF_MAGIC_GREASE) || stats->EFFECTS_TIMERS[EFF_MAGIC_GREASE] < 1 * TICKS_PER_SECOND ) + { + entity->setEffect(EFF_MAGIC_GREASE, true, std::max(5 * TICKS_PER_SECOND, stats->EFFECTS_TIMERS[EFF_MAGIC_GREASE]), + true); + } + } + } + if ( (entity->behavior == &actCampfire && entity->skill[3] > 0 ) + || entity->behavior == &actTorch ) + { + my->SetEntityOnFire(nullptr); + } + if ( my->flags[BURNING] ) + { + if ( entity->flags[BURNABLE] && !entity->flags[BURNING] ) + { + if ( Stat* stats = entity->getStats() ) + { + if ( swimmingtiles[map.tiles[mapIndex]] && entity->behavior == &actPlayer && players[entity->skill[2]]->movement.isPlayerSwimming() ) + { + continue; + } + if ( !entity->monsterIsTargetable(false) ) + { + continue; + } + entity->SetEntityOnFire(parent); + if ( parent && parent->getStats() ) + { + if ( entity->flags[BURNING] ) + { + stats->burningInflictedBy = parent->getUID(); + + bool alertTarget = entity->monsterAlertBeforeHit(parent); + + // alert the monster! + if ( entity->monsterState != MONSTER_STATE_ATTACK && (stats->type < LICH || stats->type >= SHOPKEEPER) ) + { + if ( alertTarget ) + { + entity->monsterAcquireAttackTarget(*parent, MONSTER_STATE_PATH, true); + } + } + + // alert other monsters too + /*if ( alertTarget ) + { + entity->alertAlliesOnBeingHit(parent); + }*/ + entity->updateEntityOnHit(parent, alertTarget); + } + } + } + else + { + if ( entity->behavior == &actDoor + || entity->behavior == &::actIronDoor + || entity->behavior == &actBell + || entity->behavior == &::actFurniture || entity->behavior == &::actChest + || (entity->isDamageableCollider() && entity->isColliderDamageableByMagic()) ) + { + entity->SetEntityOnFire(parent); + } + } + } + } + } + } + } + + if ( my->flags[BURNING] ) + { + ++my->skill[6]; // burning spread counter + if ( my->skill[6] >= TICKS_PER_SECOND ) + { + my->skill[6] = 0; + + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + for ( node_t* node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity && entity->behavior == &actGreasePuddleSpawner && entity != my ) + { + int x2 = entity->x / 16; + int y2 = entity->y / 16; + //if ( x2 == x || y2 == y ) // axis aligned only + { + if ( !entity->flags[BURNING] ) + { + entity->flags[BURNING] = true; + entity->skill[5] = TICKS_PER_SECOND * 5; + serverUpdateEntityFlag(entity, BURNING); + } + } + } + } + } + } + } + } + } + + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + if ( my->flags[BURNING] ) + { + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, "campfire"); + } + } + else + { + my->removeLightField(); + } + if ( my->skill[1] < 4 ) + { + if ( map.tileHasAttribute(x, y, 0, map_t::TILE_ATTRIBUTE_GREASE) ) + { + ++my->skill[1]; + std::vector chances(8); + std::fill(chances.begin(), chances.end(), 1); + for ( int i = 0; i < 8; ++i ) + { + if ( (my->skill[3] >> i) & 1 ) + { + chances[i] = 0; + } + } + int pick = local_rng.discrete(chances.data(), chances.size()); + my->skill[3] |= (1 << pick); + spawnGreasePuddle(my, my->x, my->y, 10 * TICKS_PER_SECOND, pick); + } + } + } +} + +void actMiscPuddle(Entity* my) +{ + if ( !my ) { return; } + + if ( my->skill[0] <= 0 ) + { + if ( my->scalex <= 0.0 ) + { + list_RemoveNode(my->mynode); + return; + } + + my->scalex -= 0.05; + my->scalez = my->scalex; + } + else + { + --my->skill[0]; + + if ( my->scalex < my->fskill[0] ) + { + real_t diff = std::max(0.01, (my->fskill[0] - my->scalex) / 10.0); + my->scalex = std::min(my->scalex + diff, my->fskill[0]); + } + my->scalez = my->scalex; + } +} + +#define LEAF_IDLE_BOUNCE_TIME 50 +void actLeafParticle(Entity* my) +{ + Entity* parent = nullptr; + + auto& particle_life = my->skill[0]; + auto& anim_bounce = my->skill[1]; + auto& anim_bounce_timer = my->skill[3]; + auto& float_oscillate_dir = my->skill[4]; + auto& anim_spin = my->skill[5]; + + auto& float_oscillate_amt = my->fskill[0]; + auto& anim_bounce_fall = my->fskill[1]; + auto& pos_x_center = my->fskill[4]; + auto& pos_y_center = my->fskill[5]; + auto& pos_z_start = my->fskill[6]; + auto& pos_z_end = my->fskill[7]; + auto& anim_bounce_rise_amt = my->fskill[8]; + auto& rotation_offset = my->fskill[9]; + + if ( my->parent != 0 ) + { + parent = uidToEntity(my->parent); + if ( !parent ) + { + spawnPoof(my->x, my->y, my->z, 0.25); + list_RemoveNode(my->mynode); + return; + } + } + else + { + if ( particle_life <= 0 ) + { + spawnPoof(my->x, my->y, my->z, 0.25); + list_RemoveNode(my->mynode); + return; + } + --particle_life; + } + + my->focalz = 2.0; + + bool grounded = false; + bool noFloor = false; + int mapx = my->x / 16; + int mapy = my->y / 16; + if ( my->parent == 0 ) // check no floor + { + if ( mapx >= 0 && mapx < map.width && mapy >= 0 && mapy < map.height ) + { + int mapIndex = mapy * MAPLAYERS + mapx * MAPLAYERS * map.height; + if ( !map.tiles[mapIndex] ) + { + noFloor = true; + } + } + } + + if ( my->z >= 5.5 && !noFloor ) + { + grounded = true; + + // reset to 0 + if ( float_oscillate_amt < 0.0 ) + { + float_oscillate_amt += 0.025; + float_oscillate_amt = std::min(float_oscillate_amt, 0.0); + } + else if ( float_oscillate_amt > 0.0 ) + { + float_oscillate_amt -= 0.025; + float_oscillate_amt = std::max(float_oscillate_amt, 0.0); + } + } + else if ( float_oscillate_dir == 1 ) + { + float_oscillate_amt += 0.025; + if ( float_oscillate_amt >= 1.0 ) + { + float_oscillate_amt = 1.0; + if ( parent && parent->fskill[10] > 0.05 && (parent->skill[7] == 100 || parent->skill[8] >= 20) ) + { + // spinning with at least 20 ticks left / strong spin, lock to 1 angle + } + else + { + float_oscillate_dir = 2; + } + } + } + else + { + float_oscillate_amt -= 0.025; + if ( float_oscillate_amt <= -1.0 ) + { + float_oscillate_amt = -1.0; + if ( parent && parent->fskill[10] > 0.05 && (parent->skill[7] == 100 || parent->skill[8] >= 20) ) + { + // spinning with at least 20 ticks left / strong spin, lock to 1 angle + } + else + { + float_oscillate_dir = 1; + } + } + } + + if ( anim_bounce == 0 ) + { + // rise up + anim_bounce_fall += std::max(0.01, (1.0 - anim_bounce_fall) / std::max(12.0, anim_bounce_rise_amt)); + if ( anim_bounce_fall >= 1.0 ) + { + anim_bounce_fall = 1.0; + if ( anim_spin != 0 ) + { + // don't fall down + } + else + { + anim_bounce = 1; + } + } + } + else + { + anim_bounce_fall -= std::max(0.01, (anim_bounce_fall) / 10.0); + anim_bounce_fall = std::max(0.0, anim_bounce_fall); + } + + real_t rate = (sin(float_oscillate_amt * PI / 2)); + my->roll = (PI / 4) * rate; + /*if ( keystatus[SDLK_g] ) + { + my->yaw += 0.05; + }*/ + if ( parent ) + { + pos_x_center = parent->x; + pos_y_center = parent->y; + my->yaw = parent->yaw + rotation_offset; + + if ( parent->fskill[10] > 0.25 ) + { + if ( anim_spin == 0 ) + { + anim_spin = 1; + + anim_bounce = 0; + anim_bounce_fall = 0.0; + pos_z_start = my->z; + + real_t boost = 4.0 + 0.25 * (local_rng.rand() % 9); // 4-6 + if ( parent->skill[7] != 100 ) + { + anim_bounce_rise_amt = 12.0 + 0.25 * (local_rng.rand() % 13); // 12-15.0 random rise + boost /= 3; + pos_z_end = std::max(my->z - boost, -7.5) - pos_z_start; + } + else + { + anim_bounce_rise_amt = 12.0 + 0.25 * (local_rng.rand() % 13); // 12-15.0 random rise + anim_bounce_rise_amt *= 10.0; + real_t maxHeight = 0.0 + 5.0 * rotation_offset / (2 * PI); + if ( my->z - boost > maxHeight ) + { + pos_z_end = std::min(my->z - boost, maxHeight) - pos_z_start; + } + else + { + pos_z_end = std::max(my->z - boost, maxHeight) - pos_z_start; + } + } + } + } + else + { + if ( anim_spin == 1 ) + { + anim_spin = 0; + } + } + } + real_t faceDir = my->yaw; + my->x = pos_x_center + 4.0 * cos(faceDir) - 2.0 * rate * cos(faceDir + PI / 2); + my->y = pos_y_center + 4.0 * sin(faceDir) - 2.0 * rate * sin(faceDir + PI / 2); + if ( anim_bounce == 0 ) + { + // rise up + my->z = pos_z_start + pos_z_end * sin(anim_bounce_fall * PI / 2); + } + else + { + // fall down faster as anim_bounce_fall 1 - 0 + my->vel_z = 0.1 * (1.0 - anim_bounce_fall) * abs(cos(float_oscillate_amt)); + my->z += my->vel_z; + } + + if ( noFloor ) + { + // fall down + } + else + { + my->z = std::min(my->z, 5.5); + } + + if ( !parent ) + { + //if ( multiplayer != CLIENT ) + { + //my->vel_x *= 0.8; + //my->vel_y *= 0.8; + //if ( abs(my->vel_x) > 0.01 || abs(my->vel_y) > 0.01 ) + //{ + // clipMove(&pos_x_center, &pos_y_center, my->vel_x, my->vel_y, my); + //} + if ( anim_bounce_timer == 0 ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i]->entity && entityInsideEntity(players[i]->entity, my) ) + { + if ( abs(players[i]->entity->vel_x > 0.1) || abs(players[i]->entity->vel_y) > 0.1 ) + { + anim_bounce_timer = LEAF_IDLE_BOUNCE_TIME; + real_t tangent = atan2(my->y - players[i]->entity->y, my->x - players[i]->entity->x); + my->vel_x = 1.5 * cos(tangent); + my->vel_y = 1.5 * sin(tangent); + float_oscillate_dir = 1 + local_rng.rand() % 2; + anim_bounce = 0; + anim_bounce_fall = 0.0; + pos_z_start = my->z; + anim_bounce_rise_amt = 12.0 + 0.25 * (local_rng.rand() % 13); // 12-15.0 random rise + real_t boost = 4.0 + 0.25 * (local_rng.rand() % 9); // 4-6 + pos_z_end = std::max(my->z - boost, -7.5) - pos_z_start; + break; + } + } + } + } + else + { + --anim_bounce_timer; + } + } + } + else + { + if ( parent->skill[3] == LEAF_IDLE_BOUNCE_TIME - 1 ) + { + anim_bounce = 0; + float_oscillate_dir = 1 + local_rng.rand() % 2; + anim_bounce_fall = 0.0; + pos_z_start = my->z; + real_t boost = 4.0 + 0.25 * (local_rng.rand() % 9); // 4-6 + anim_bounce_rise_amt = 12.0 + 0.25 * (local_rng.rand() % 13); // 12-15.0 + pos_z_end = std::max(my->z - boost, -7.5) - pos_z_start; + } + } +} + +Entity* spawnLeafPile(real_t x, real_t y, bool trap) +{ + if ( multiplayer == CLIENT ) { return nullptr; } + if ( Entity* leaf = newEntity(1913, 1, map.entities, nullptr) ) + { + leaf->x = x; + leaf->y = y; + leaf->z = 0.0; + leaf->yaw = map_rng.rand() % 360 * (PI / 180.0); + leaf->sizex = 4; + leaf->sizey = 4; + leaf->behavior = &actLeafPile; + leaf->skill[0] = 0; + leaf->skill[10] = 0; // not map gen + leaf->skill[11] = trap ? 0 : 1; + leaf->flags[NOCLIP_CREATURES] = true; + leaf->flags[UPDATENEEDED] = true; + leaf->flags[NOUPDATE] = false; + leaf->flags[PASSABLE] = true; + leaf->flags[UNCLICKABLE] = true; + + int mapx = static_cast(x) / 16; + int mapy = static_cast(y) / 16; + int mapIndex = mapy * MAPLAYERS + mapx * MAPLAYERS * map.height; + if ( mapx > 0 && mapx < map.width && mapy > 0 && mapy < map.height ) + { + if ( !map.tiles[mapIndex] || swimmingtiles[map.tiles[mapIndex]] || lavatiles[map.tiles[mapIndex]] + || map.tiles[OBSTACLELAYER + mapIndex] ) + { + leaf->skill[0] = 4.0 * TICKS_PER_SECOND; // lifetime on wrong terrain + } + } + else + { + leaf->skill[0] = 4.0 * TICKS_PER_SECOND; // lifetime on wrong terrain + } + return leaf; + } + return nullptr; +} + +void actLeafPile(Entity* my) +{ + if ( multiplayer != CLIENT ) + { + if ( my->skill[0] > 0 ) + { + --my->skill[0]; + if ( my->skill[0] <= 0 ) + { + list_RemoveNode(my->mynode); + return; + } + } + } + + my->flags[INVISIBLE] = true; + + if ( my->skill[1] == 0 ) + { + my->skill[1] = 1; + + real_t leafEndZ = -7.5; + if ( my->skill[10] == 1 ) + { + // map gen + leafEndZ = 0.0 + local_rng.rand() % 3; + } + for ( int i = 0; i < 3; ++i ) + { + Entity* leaf = newEntity(1912, 1, map.entities, nullptr); //Gib entity. + if ( leaf != NULL ) + { + leaf->x = my->x; + leaf->y = my->y; + leaf->z = 5.0 - i * 0.5; + leaf->fskill[6] = leaf->z; + leaf->fskill[7] = leafEndZ - leaf->fskill[6]; + leaf->vel_z = 0.0; + leaf->yaw = my->yaw + i * 2 * PI / 3; + leaf->sizex = 2; + leaf->sizey = 2; + leaf->scalex = 0.5; + leaf->scaley = 0.5; + leaf->scalez = 0.5; + leaf->fskill[4] = my->x; + leaf->fskill[5] = my->y; + leaf->fskill[9] = i * 2 * PI / 3; + leaf->parent = my->getUID(); + leaf->behavior = &actLeafParticle; + leaf->flags[NOCLIP_CREATURES] = true; + leaf->flags[UPDATENEEDED] = false; + leaf->flags[NOUPDATE] = true; + leaf->flags[PASSABLE] = true; + leaf->flags[UNCLICKABLE] = true; + if ( multiplayer != CLIENT ) + { + --entity_uids; + } + leaf->setUID(-3); + } + } + } + + auto& spinStrength = my->skill[7]; + auto& spinTimer = my->skill[8]; + + if ( multiplayer != CLIENT ) + { + if ( my->skill[3] == 0 ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( auto it : entLists ) + { + if ( my->skill[3] != 0 ) + { + break; + } + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actMonster || entity->behavior == &actPlayer ) + { + if ( !entity->monsterIsTargetable() ) { continue; } + if ( abs(entity->vel_x > 0.1) || abs(entity->vel_y) > 0.1 ) + { + if ( entityInsideEntity(entity, my) ) + { + my->skill[3] = LEAF_IDLE_BOUNCE_TIME; + my->skill[4] = 100; + playSoundEntityLocal(my, 754 + local_rng.rand() % 2, 64); + entity->setEffect(EFF_NOISE_VISIBILITY, (Uint8)2, 2 * TICKS_PER_SECOND, false); + if ( multiplayer == SERVER ) + { + for ( int c = 1; c < MAXPLAYERS; ++c ) // send to other players + { + if ( client_disconnected[c] || players[c]->isLocalPlayer() ) + { + continue; + } + strcpy((char*)net_packet->data, "LEAF"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + net_packet->data[8] = 1; + net_packet->data[9] = (Uint8)my->skill[3]; + net_packet->data[10] = (Uint8)my->skill[4]; + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 11; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } + } + break; + } + } + } + } + } + } + + if ( my->fskill[10] < 0.05 ) + { + my->vel_x *= 0.95; + my->vel_y *= 0.95; + } + else + { + my->vel_x *= 0.995; + my->vel_y *= 0.995; + } + if ( abs(my->vel_x) > 0.01 || abs(my->vel_y) > 0.01 ) + { + real_t result = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); + if ( result != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y) ) + { + if ( spinStrength == 100 ) + { + real_t bouncePenalty = 1.0; + if ( hit.side == HORIZONTAL ) + { + my->vel_x = -my->vel_x * bouncePenalty; + } + else if ( hit.side == VERTICAL ) + { + my->vel_y = -my->vel_y * bouncePenalty; + } + else if ( hit.side == 0 ) + { + my->vel_x = -my->vel_y * bouncePenalty; + my->vel_y = -my->vel_x * bouncePenalty; + } + } + else + { + my->vel_x = 0.f; + my->vel_y = 0.f; + } + } + } + } + + if ( my->skill[3] > 0 ) + { + --my->skill[3]; + } + + if ( spinStrength > 0 ) + { + real_t amt = spinStrength / 100.0; + + my->fskill[10] += std::max(0.01, (amt - my->fskill[10]) / 10.0); + my->fskill[10] = std::min(amt, my->fskill[10]); + my->yaw = normaliseAngle2PI(my->yaw); + + if ( spinStrength == 100 ) + { + my->skill[5]++; + if ( my->skill[5] == 2 * TICKS_PER_SECOND && multiplayer != CLIENT ) + { + CastSpellProps_t spellProps; + spellProps.caster_x = my->x; + spellProps.caster_y = my->y; + spellProps.target_x = my->x; + spellProps.target_y = my->y; + castSpell(my->getUID(), getSpellFromID(SPELL_SLAM), false, true, false, &spellProps); + + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + real_t dist = 10000.0; + Entity* closestEntity = nullptr; + for ( auto it : entLists ) + { + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actMonster || entity->behavior == &actPlayer ) + { + if ( !entity->monsterIsTargetable() ) { continue; } + if ( Stat* entityStats = entity->getStats() ) + { + if ( entityStats->type == MYCONID || entityStats->type == DRYAD ) + { + continue; + } + } + real_t newDist = entityDist(my, entity); + if ( newDist < dist && newDist < 64.0 ) + { + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + real_t d = lineTraceTarget(my, my->x, my->y, tangent, 64.0, 0, false, entity); + if ( hit.entity == entity ) + { + closestEntity = entity; + dist = newDist; + } + } + } + } + } + + if ( closestEntity ) + { + real_t tangent = atan2(closestEntity->y - my->y, closestEntity->x - my->x); + my->vel_x = 0.75 * cos(tangent); + my->vel_y = 0.75 * sin(tangent); + } + } + } + } + else + { + my->skill[5] = 0; + my->fskill[10] -= std::max(0.01, my->fskill[10] / 100.0); + my->fskill[10] = std::max(0.0, my->fskill[10]); + } + + my->yaw += 0.2 * (1 + sin(-PI / 2 + my->fskill[10] * PI / 2)); + my->yaw = normaliseAngle2PI(my->yaw); + + if ( my->skill[4] > 0 ) + { + my->yaw += 0.025 * (my->skill[4]) / 100.0; + my->yaw = normaliseAngle2PI(my->yaw); + --my->skill[4]; + } + + if ( spinTimer > 0 ) + { + --spinTimer; + if ( spinTimer == 0 ) + { + spinStrength = 0; + } + } + +#ifdef USE_FMOD + bool isPlaying = false; + if ( my->entity_sound ) + { + my->entity_sound->isPlaying(&isPlaying); + if ( isPlaying ) + { + FMOD_VECTOR position; + position.x = (float)(my->x / (real_t)16.0); + position.y = (float)(0.0); + position.z = (float)(my->y / (real_t)16.0); + my->entity_sound->set3DAttributes(&position, nullptr); + } + } +#endif + + if ( multiplayer != CLIENT ) + { + int spinEvent = 0; + if ( my->skill[6] == 0 ) + { + my->skill[6] = TICKS_PER_SECOND * 5 + local_rng.rand() % (TICKS_PER_SECOND * 10); + } + else + { + if ( spinTimer == 0 ) + { + --my->skill[6]; + if ( my->skill[6] == 0 ) + { + if ( my->skill[11] == 0 ) // trapped + { + spinEvent = local_rng.rand() % 8 == 0 ? 2 : 1; + } + else + { + spinEvent = 1; + } +#ifdef USE_FMOD + bool isPlaying = false; + if ( my->entity_sound ) + { + my->entity_sound->isPlaying(&isPlaying); + } + if ( !isPlaying ) + { + my->entity_sound = playSoundEntityLocal(my, 752 + local_rng.rand() % 2, 128); + } +#endif + } + } + } + + if ( /*keystatus[SDLK_g] ||*/ spinEvent == 2 ) + { + spinStrength = 100; + spinTimer = 225; + + if ( local_rng.rand() % 2 == 0 ) + { + std::vector dirs; + for ( int i = 0; i < 4; ++i ) + { + if ( !checkObstacle(my->x + 16.0 * cos(i * PI / 2), my->y + 16.0 * sin(i * PI / 2), my, nullptr) ) + { + dirs.push_back(i * PI / 2); + } + } + if ( dirs.size() > 0 ) + { + real_t newDir = dirs[local_rng.rand() % dirs.size()]; + my->vel_x = 0.5 * cos(newDir); + my->vel_y = 0.5 * sin(newDir); + } + else + { + real_t newDir = (local_rng.rand() % 4) * PI / 2; + my->vel_x = 0.5 * cos(newDir); + my->vel_y = 0.5 * sin(newDir); + } + } + + if ( multiplayer == SERVER ) + { + for ( int c = 1; c < MAXPLAYERS; ++c ) // send to other players + { + if ( client_disconnected[c] || players[c]->isLocalPlayer() ) + { + continue; + } + strcpy((char*)net_packet->data, "LEAF"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + net_packet->data[8] = 2; + net_packet->data[9] = (Uint8)spinStrength; + net_packet->data[10] = (Uint8)spinTimer; + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 11; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } + } + } + if ( /*keystatus[SDLK_h] ||*/ spinEvent == 1 ) + { + //keystatus[SDLK_h] = 0; + spinStrength = 75; + spinTimer = 50; + + std::vector dirs; + for ( int i = 0; i < 4; ++i ) + { + if ( !checkObstacle(my->x + 16.0 * cos(i * PI / 2), my->y + 16.0 * sin(i * PI / 2), my, nullptr) ) + { + dirs.push_back(i * PI / 2); + } + } + if ( dirs.size() > 0 ) + { + real_t newDir = dirs[local_rng.rand() % dirs.size()]; + my->vel_x = 0.5 * cos(newDir); + my->vel_y = 0.5 * sin(newDir); + } + else + { + real_t newDir = (local_rng.rand() % 4) * PI / 2; + my->vel_x = 0.5 * cos(newDir); + my->vel_y = 0.5 * sin(newDir); + } + + if ( multiplayer == SERVER ) + { + for ( int c = 1; c < MAXPLAYERS; ++c ) // send to other players + { + if ( client_disconnected[c] || players[c]->isLocalPlayer() ) + { + continue; + } + strcpy((char*)net_packet->data, "LEAF"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + net_packet->data[8] = 2; + net_packet->data[9] = (Uint8)spinStrength; + net_packet->data[10] = (Uint8)spinTimer; + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 11; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } + } + } + } +} + +Entity* spawnMiscPuddle(Entity* parentent, real_t x, real_t y, int sprite, bool updateClients) +{ + if ( sprite == 0 ) + { + return nullptr; + } + if ( parentent ) + { + x = parentent->x; + y = parentent->y; + } + + int mapx = static_cast(x) / 16; + int mapy = static_cast(y) / 16; + int mapIndex = mapy * MAPLAYERS + mapx * MAPLAYERS * map.height; + if ( mapx > 0 && mapx < map.width && mapy > 0 && mapy < map.height ) + { + if ( !map.tiles[mapIndex] || map.tiles[OBSTACLELAYER + mapIndex] ) + { + return nullptr; + } + + Entity* puddle = newEntity(sprite, 1, map.entities, nullptr); //Gib entity. + if ( puddle != NULL ) + { + puddle->x = x; + puddle->y = y; + puddle->z = 8.0 + (local_rng.rand() % 20) / 100.0; + puddle->sizex = 2; + puddle->sizey = 2; + puddle->behavior = &actMiscPuddle; + int randomScale = local_rng.rand() % 10; + puddle->fskill[0] = (100 - randomScale) / 100.f; // end scale + + puddle->scalex = 0.0; + puddle->scalez = puddle->scalex; + puddle->skill[0] = TICKS_PER_SECOND * 3 + local_rng.rand() % (2 * TICKS_PER_SECOND); + puddle->yaw = (local_rng.rand() % 360) * PI / 180.0; + puddle->flags[UPDATENEEDED] = false; + puddle->flags[NOUPDATE] = true; + puddle->flags[PASSABLE] = true; + puddle->flags[UNCLICKABLE] = true; + if ( multiplayer != CLIENT ) + { + --entity_uids; + } + puddle->setUID(-3); + + if ( updateClients && multiplayer == SERVER ) + { + serverSpawnMiscParticlesAtLocation(x, y, 0, PARTICLE_EFFECT_MISC_PUDDLE, sprite); + } + } + return puddle; + } + + return nullptr; +} \ No newline at end of file diff --git a/src/actgold.cpp b/src/actgold.cpp index a8d504884..c341f7a5b 100644 --- a/src/actgold.cpp +++ b/src/actgold.cpp @@ -20,6 +20,7 @@ #include "prng.hpp" #include "scores.hpp" #include "collision.hpp" +#include "mod_tools.hpp" /*------------------------------------------------------------------------------- @@ -33,6 +34,7 @@ void actGoldBag(Entity* my) { int i; + my->goldTelepathy = 0; if ( my->ticks == 1 ) { @@ -93,6 +95,20 @@ void actGoldBag(Entity* my) } #endif + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i]->isLocalPlayer() ) + { + if ( players[i]->entity && players[i]->entity->isBlind() ) + { + if ( stats[i]->type == GNOME ) + { + my->goldTelepathy |= (1 << i); + } + } + } + } + // pick up gold if ( multiplayer != CLIENT ) { @@ -111,6 +127,11 @@ void actGoldBag(Entity* my) { playSoundEntity(players[i]->entity, 242 + local_rng.rand() % 4, 64 ); } + if ( stats[i]->type == GNOME ) + { + my->goldAmount += my->goldAmountBonus; + my->goldAmountBonus = 0; + } stats[i]->GOLD += my->goldAmount; if ( multiplayer == SERVER && i > 0 && !players[i]->isLocalPlayer() ) { @@ -133,6 +154,31 @@ void actGoldBag(Entity* my) messagePlayer(i, MESSAGE_INTERACTION | MESSAGE_INVENTORY, Language::get(484), my->goldAmount); } + for ( int player = 0; player < MAXPLAYERS; ++player ) + { + if ( players[player]->mechanics.donationRevealedOnFloor == my->getUID() ) + { + messagePlayerColor(i, MESSAGE_HINT, makeColorRGB(255, 255, 0), Language::get(6943)); // you discovered a gift + + for ( int player2 = 0; player2 < MAXPLAYERS; ++player2 ) // relay to other players + { + if ( player2 != i && !client_disconnected[player2] ) + { + messagePlayerColor(player2, MESSAGE_HINT, makeColorRGB(255, 255, 0), Language::get(6944), stats[i]->name); // an ally discovered a gift + } + } + + players[player]->mechanics.updateSustainedSpellEvent(SPELL_DONATION, 150.0, 1.0, nullptr); + break; + } + } + + if ( my->goldDroppedByPlayer == 0 ) + { + Compendium_t::Events_t::eventUpdateCodex(i, Compendium_t::CPDM_GOLD_COLLECTED, "gold", my->goldAmount); + Compendium_t::Events_t::eventUpdateCodex(i, Compendium_t::CPDM_GOLD_COLLECTED_RUN, "gold", my->goldAmount); + } + // remove gold entity list_RemoveNode(my->mynode); return; @@ -150,6 +196,7 @@ void actGoldBag(Entity* my) real_t groundheight = my->sprite == 1379 ? 7.75 : 6.25; my->flags[BURNING] = false; + my->flags[NOCLIP_CREATURES] = true; if ( my->goldBouncing == 0 ) { diff --git a/src/acthudweapon.cpp b/src/acthudweapon.cpp index 0e8a5848a..7df4dde1a 100644 --- a/src/acthudweapon.cpp +++ b/src/acthudweapon.cpp @@ -33,6 +33,54 @@ -------------------------------------------------------------------------------*/ +static ConsoleVariable cvar_hudweapon_yaw_ang("/hudweapon_yaw_ang", 0.0); +static ConsoleVariable cvar_hudweapon_yaw_spd("/hudweapon_yaw_spd", 0.0); +static ConsoleVariable cvar_hudweapon_pitch_ang("/hudweapon_pitch_ang", 0.0); +static ConsoleVariable cvar_hudweapon_pitch_spd("/hudweapon_pitch_spd", 0.0); +static ConsoleVariable cvar_hudweapon_roll_ang("/hudweapon_roll_ang", 0.0); +static ConsoleVariable cvar_hudweapon_roll_spd("/hudweapon_roll_spd", 1.0); +static ConsoleVariable cvar_hudweapon_x_ang("/hudweapon_x_ang", 2.0); +static ConsoleVariable cvar_hudweapon_x_spd("/hudweapon_x_spd", 0.5); +static ConsoleVariable cvar_hudweapon_y_ang("/hudweapon_y_ang", -4.0); +static ConsoleVariable cvar_hudweapon_y_spd("/hudweapon_y_spd", 1.0); +static ConsoleVariable cvar_hudweapon_z_ang("/hudweapon_z_ang", 0.0); +static ConsoleVariable cvar_hudweapon_z_spd("/hudweapon_z_spd", 0.0); +static ConsoleVariable cvar_hudweapon_timescale("/hudweapon_timescale", 0.5); +static ConsoleVariable cvar_hudweapon_timescale2("/hudweapon_timescale2", 0.675); +static ConsoleVariable cvar_hudweapon_timescale3("/hudweapon_timescale3", 1.0); +//static ConsoleVariable cvar_claymore_toggle("/claymore_toggle", 0); +void hudWeaponAnimateVariable(real_t& variable, real_t target, real_t speed) +{ + if ( variable < target ) + { + variable = std::min(variable + speed * *cvar_hudweapon_timescale, target); + } + else if ( variable > target ) + { + variable = std::max(variable - speed * *cvar_hudweapon_timescale, target); + } +} + +struct HUDFlail_t +{ + real_t pitch = 0.0; + real_t roll = 0.0; + real_t yaw = 0.0; + real_t spin = 0.0; + real_t spin2 = 0.0; + real_t bounce = 0.0; + real_t rollSpin = 2 * PI; + real_t prevYaw = 0.0; + int spinState = 0; + bool needsInit = true; + + static void reset(HUDFlail_t& my) + { + my = HUDFlail_t(); + } +}; +HUDFlail_t HUDFlail[MAXPLAYERS]; + #define HUDARM_PLAYERNUM my->skill[11] #define HUD_SHAPESHIFT_HIDE my->skill[12] #define HUD_LASTSHAPESHIFT_FORM my->skill[13] @@ -73,6 +121,9 @@ void actHudArm(Entity* my) my->y = parent->y; my->z = parent->z - 2.5; + my->mistformGLRender = players[HUDARM_PLAYERNUM]->entity->mistformGLRender > 0.9 ? players[HUDARM_PLAYERNUM]->entity->mistformGLRender + : 0.0; + Monster playerRace = players[HUDARM_PLAYERNUM]->entity->getMonsterFromPlayerRace(stats[HUDARM_PLAYERNUM]->playerRace); int playerAppearance = stats[HUDARM_PLAYERNUM]->stat_appearance; if ( players[HUDARM_PLAYERNUM]->entity->effectShapeshift != NOTHING ) @@ -157,6 +208,26 @@ void actHudArm(Entity* my) { my->sprite = 802; } + else if ( stats[HUDARM_PLAYERNUM]->gloves->type == BONE_BRACERS ) + { + my->sprite = 2107; + } + else if ( stats[HUDARM_PLAYERNUM]->gloves->type == BLACKIRON_GAUNTLETS ) + { + my->sprite = 2109; + } + else if ( stats[HUDARM_PLAYERNUM]->gloves->type == SILVER_GAUNTLETS ) + { + my->sprite = 2111; + } + else if ( stats[HUDARM_PLAYERNUM]->gloves->type == QUILTED_GLOVES ) + { + my->sprite = 2113; + } + else if ( stats[HUDARM_PLAYERNUM]->gloves->type == CHAIN_GLOVES ) + { + my->sprite = 2115; + } if ( stats[HUDARM_PLAYERNUM]->weapon == nullptr ) { my->scalex = 0.5f; @@ -229,6 +300,28 @@ void actHudArm(Entity* my) case RAT: my->sprite = 859; break; + case DRYAD: + my->sprite = 2320; + break; + case MYCONID: + my->sprite = 2328; + break; + case GREMLIN: + if ( stats[HUDARM_PLAYERNUM]->sex == FEMALE ) + { + my->sprite = 2324; + } + else + { + my->sprite = 2326; + } + break; + case SALAMANDER: + my->sprite = 2330; + break; + case GNOME: + my->sprite = 2322; + break; default: my->sprite = 634; break; @@ -294,6 +387,8 @@ void actHudArm(Entity* my) #define HUDWEAPON_BOW_HAS_QUIVER my->skill[9] #define HUDWEAPON_BOW_FORCE_RELOAD my->skill[10] #define HUDWEAPON_PLAYERNUM my->skill[11] +#define HUDWEAPON_DELAY_TICK my->skill[14] +#define HUDWEAPON_PARRY_TICK my->skill[15] #define HUDWEAPON_MOVEX my->fskill[0] #define HUDWEAPON_MOVEY my->fskill[1] #define HUDWEAPON_MOVEZ my->fskill[2] @@ -303,7 +398,6 @@ void actHudArm(Entity* my) #define HUDWEAPON_OLDVIBRATEX my->fskill[6] #define HUDWEAPON_OLDVIBRATEY my->fskill[7] #define HUDWEAPON_OLDVIBRATEZ my->fskill[8] - enum CrossbowAnimState : int { CROSSBOW_ANIM_NONE, @@ -369,6 +463,11 @@ void actHudWeapon(Entity* my) return; } + if ( HUDWEAPON_DELAY_TICK > 0 ) + { + --HUDWEAPON_DELAY_TICK; + } + if ( multiplayer == CLIENT ) { if ( stats[HUDWEAPON_PLAYERNUM]->HP <= 0 ) @@ -405,6 +504,9 @@ void actHudWeapon(Entity* my) return; } + my->mistformGLRender = players[HUDWEAPON_PLAYERNUM]->entity->mistformGLRender > 0.9 ? players[HUDWEAPON_PLAYERNUM]->entity->mistformGLRender + : 0.0; + // reduce throwGimpTimer (allows player to throw items again) if ( throwGimpTimer > 0 ) { @@ -484,6 +586,11 @@ void actHudWeapon(Entity* my) } } + if ( HUDWEAPON_CHOP >= 10 && HUDWEAPON_CHOP <= 12 ) // power strike charge + { + hideWeapon = true; + } + HUDWEAPON_HIDEWEAPON = hideWeapon; HUDWEAPON_SHOOTING_RANGED_WEAPON = RANGED_ANIM_IDLE; @@ -498,7 +605,7 @@ void actHudWeapon(Entity* my) my->flags[INVISIBLE_DITHER] = false; - if ( players[HUDWEAPON_PLAYERNUM]->entity->skill[3] == 1 ) // debug cam or player invisible + if ( players[HUDWEAPON_PLAYERNUM]->entity->skill[3] != 0 ) // debug cam or player invisible { my->flags[INVISIBLE] = true; if (parent != nullptr) @@ -580,7 +687,8 @@ void actHudWeapon(Entity* my) if ( rangedweapon ) { if ( stats[HUDWEAPON_PLAYERNUM]->weapon - && stats[HUDWEAPON_PLAYERNUM]->weapon->type != CROSSBOW ) + && stats[HUDWEAPON_PLAYERNUM]->weapon->type != CROSSBOW + && stats[HUDWEAPON_PLAYERNUM]->weapon->type != BLACKIRON_CROSSBOW ) { my->sprite++; } @@ -588,7 +696,10 @@ void actHudWeapon(Entity* my) } } } - else if ( stats[HUDWEAPON_PLAYERNUM]->weapon && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW) ) + else if ( stats[HUDWEAPON_PLAYERNUM]->weapon + && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW) ) { HUDWEAPON_SHOOTING_RANGED_WEAPON = RANGED_ANIM_BEING_DRAWN; if ( rangedWeaponUseQuiverOnAttack(stats[HUDWEAPON_PLAYERNUM]) ) @@ -624,6 +735,10 @@ void actHudWeapon(Entity* my) { my->sprite = 987; } + else if ( stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW ) + { + my->sprite = 2260; + } } else if ( HUDWEAPON_CHOP == CROSSBOW_CHOP_RELOAD_ENDING ) { @@ -695,12 +810,14 @@ void actHudWeapon(Entity* my) bool swingweapon = false; if ( players[HUDWEAPON_PLAYERNUM]->entity - && input.binaryToggle("Attack") + && (input.binaryToggle("Attack") + || (cast_animation[HUDWEAPON_PLAYERNUM].spellWaitingAttackInput() + && inputs.hasController(HUDWEAPON_PLAYERNUM) && input.binaryToggle("Cast Spell")) ) && shootmode && !gamePaused && players[HUDWEAPON_PLAYERNUM]->entity->isMobile() - && !(input.binaryToggle("Defend") && stats[HUDWEAPON_PLAYERNUM]->defending) - && HUDWEAPON_OVERCHARGE < MAXCHARGE ) + && (!(input.binaryToggle("Defend") && stats[HUDWEAPON_PLAYERNUM]->defending) || cast_animation[HUDWEAPON_PLAYERNUM].spellWaitingAttackInput() ) + && HUDWEAPON_OVERCHARGE < Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM]) ) { swingweapon = true; } @@ -721,8 +838,9 @@ void actHudWeapon(Entity* my) bool thrownWeapon = stats[HUDWEAPON_PLAYERNUM]->weapon && (itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) == THROWN || itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) == GEM - || stats[HUDWEAPON_PLAYERNUM]->weapon->type == FOOD_CREAMPIE); + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == FOOD_CREAMPIE || stats[HUDWEAPON_PLAYERNUM]->weapon->type == TOOL_DUCK); bool castStrikeAnimation = (players[HUDWEAPON_PLAYERNUM]->entity->skill[9] == MONSTER_POSE_SPECIAL_WINDUP1); + bool flail = !hideWeapon && stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type == STEEL_FLAIL; // weapon switch animation if ( players[HUDWEAPON_PLAYERNUM]->hud.weaponSwitch ) @@ -730,7 +848,10 @@ void actHudWeapon(Entity* my) players[HUDWEAPON_PLAYERNUM]->hud.weaponSwitch = false; if ( !hideWeapon ) { - if ( stats[HUDWEAPON_PLAYERNUM]->weapon && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW) ) + if ( stats[HUDWEAPON_PLAYERNUM]->weapon + && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW) ) { swingweapon = false; HUDWEAPON_CHARGE = 0; @@ -785,6 +906,13 @@ void actHudWeapon(Entity* my) HUDWEAPON_CHOP = 0; } + if ( flail ) + { + swingweapon = false; + HUDWEAPON_CHOP = 0; + HUDWEAPON_CHARGE = 0; + HUDWEAPON_OVERCHARGE = 0; + } if ( thrownWeapon && HUDWEAPON_CHOP > 3 ) { // prevent thrown weapon rapid firing. @@ -798,6 +926,7 @@ void actHudWeapon(Entity* my) if ( rangedweapon && stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type != CROSSBOW && stats[HUDWEAPON_PLAYERNUM]->weapon->type != HEAVY_CROSSBOW + && stats[HUDWEAPON_PLAYERNUM]->weapon->type != BLACKIRON_CROSSBOW ) { HUDWEAPON_BOW_FORCE_RELOAD = 1; @@ -841,6 +970,7 @@ void actHudWeapon(Entity* my) if ( rangedweapon && stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type != CROSSBOW && stats[HUDWEAPON_PLAYERNUM]->weapon->type != HEAVY_CROSSBOW + && stats[HUDWEAPON_PLAYERNUM]->weapon->type != BLACKIRON_CROSSBOW && !hideWeapon ) { bowFireRate = bowDrawBaseTicks * (rangedAttackGetSpeedModifier(stats[HUDWEAPON_PLAYERNUM])); @@ -972,8 +1102,10 @@ void actHudWeapon(Entity* my) } } - bool whip = stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type == TOOL_WHIP; + bool whip = !hideWeapon && stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type == TOOL_WHIP; bool bearTrap = !hideWeapon && stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type == TOOL_BEARTRAP; + bool rapier = stats[HUDWEAPON_PLAYERNUM]->weapon && !hideWeapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type == RAPIER; + bool claymore = stats[HUDWEAPON_PLAYERNUM]->weapon && !hideWeapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type == CLAYMORE_SWORD; // main animation if ( HUDWEAPON_CHOP == 0 ) @@ -985,8 +1117,21 @@ void actHudWeapon(Entity* my) HUDWEAPON_CROSSBOW_RELOAD_ANIMATION = CROSSBOW_ANIM_NONE; } bool ignoreAttack = false; - if ( swingweapon && throwGimpTimer > 0 && stats[HUDWEAPON_PLAYERNUM]->weapon && + if ( !castStrikeAnimation && (cast_animation[HUDWEAPON_PLAYERNUM].spellWaitingAttackInput() + || cast_animation[HUDWEAPON_PLAYERNUM].spellIgnoreAttack() + || cast_animation[HUDWEAPON_PLAYERNUM].active + || cast_animation[HUDWEAPON_PLAYERNUM].active_spellbook ) + ) + { + ignoreAttack = true; + if ( cast_animation[HUDWEAPON_PLAYERNUM].spellWaitingAttackInput() ) + { + cast_animation[HUDWEAPON_PLAYERNUM].executeAttackSpell(swingweapon); + } + } + else if ( swingweapon && throwGimpTimer > 0 && stats[HUDWEAPON_PLAYERNUM]->weapon && !hideWeapon && ( stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW || itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) == POTION || itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) == GEM || itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) == THROWN @@ -998,6 +1143,7 @@ void actHudWeapon(Entity* my) else if ( swingweapon && bowGimpTimer > 0 && rangedweapon && stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type != CROSSBOW && stats[HUDWEAPON_PLAYERNUM]->weapon->type != HEAVY_CROSSBOW + && stats[HUDWEAPON_PLAYERNUM]->weapon->type != BLACKIRON_CROSSBOW && !hideWeapon ) { ignoreAttack = true; @@ -1069,7 +1215,11 @@ void actHudWeapon(Entity* my) { pickaxeGimpTimer = 40; } - if ( stats[HUDWEAPON_PLAYERNUM]->weapon->type == IRON_SPEAR || stats[HUDWEAPON_PLAYERNUM]->weapon->type == ARTIFACT_SPEAR ) + if ( stats[HUDWEAPON_PLAYERNUM]->weapon->type == IRON_SPEAR + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == ARTIFACT_SPEAR + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BONE_SPEAR + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == SILVER_GLAIVE + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_TRIDENT ) { HUDWEAPON_CHOP = 7; // spear lunges } @@ -1078,12 +1228,20 @@ void actHudWeapon(Entity* my) HUDWEAPON_CHOP = 4; pickaxeGimpTimer = 20; } + else if ( rapier ) + { + HUDWEAPON_CHOP = 7; // lunges + HUDWEAPON_DELAY_TICK = 0; + } else if ( rangedweapon ) { if ( stats[HUDWEAPON_PLAYERNUM]->weapon->type == SLING || stats[HUDWEAPON_PLAYERNUM]->weapon->type == SHORTBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BONE_SHORTBOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == ARTIFACT_BOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == LONGBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BRANCH_BOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BRANCH_BOW_INFECTED || stats[HUDWEAPON_PLAYERNUM]->weapon->type == COMPOUND_BOW ) { if ( !stats[HUDWEAPON_PLAYERNUM]->defending && !throwGimpTimer ) @@ -1249,7 +1407,8 @@ void actHudWeapon(Entity* my) // set delay before crossbow can fire again throwGimpTimer = 40; - if ( stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW ) + if ( stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW ) { throwGimpTimer *= rangedAttackGetSpeedModifier(stats[HUDWEAPON_PLAYERNUM]); } @@ -1377,7 +1536,8 @@ void actHudWeapon(Entity* my) { HUDWEAPON_CHOP = 7; // magicstaffs lunge } - else if ( itemCategory(item) == THROWN || itemCategory(item) == GEM || item->type == FOOD_CREAMPIE ) + else if ( itemCategory(item) == THROWN || itemCategory(item) == GEM || item->type == FOOD_CREAMPIE + || item->type == TOOL_DUCK ) { if ( !throwGimpTimer ) { @@ -1394,8 +1554,12 @@ void actHudWeapon(Entity* my) // keys and lockpicks HUDWEAPON_MOVEX = 5; HUDWEAPON_CHOP = 3; + if ( pickaxeGimpTimer < TICKS_PER_SECOND / 2 ) + { + pickaxeGimpTimer = TICKS_PER_SECOND / 2; // fix for swapping weapon causing issues. + } Entity* player = players[HUDWEAPON_PLAYERNUM]->entity; - bool foundBomb = false; + bool foundPassableObject = false; if ( stats[HUDWEAPON_PLAYERNUM]->weapon ) { bool clickedOnGUI = false; @@ -1411,14 +1575,15 @@ void actHudWeapon(Entity* my) inputs.setMouse(HUDWEAPON_PLAYERNUM, Inputs::OX, tmpmousex); inputs.setMouse(HUDWEAPON_PLAYERNUM, Inputs::OX, tmpmousey); - if ( clickedOn && clickedOn->behavior == &actBomb && entityDist(clickedOn, players[HUDWEAPON_PLAYERNUM]->entity) < STRIKERANGE ) + if ( clickedOn && (clickedOn->behavior == &actBomb || clickedOn->behavior == &actWallLock) + && entityDist(clickedOn, players[HUDWEAPON_PLAYERNUM]->entity) < STRIKERANGE ) { // found something stats[HUDWEAPON_PLAYERNUM]->weapon->apply(HUDWEAPON_PLAYERNUM, clickedOn); - foundBomb = true; + foundPassableObject = true; } } - if ( !foundBomb ) + if ( !foundPassableObject ) { real_t dist = lineTrace(player, player->x, player->y, player->yaw, STRIKERANGE, 0, false); if ( hit.entity && stats[HUDWEAPON_PLAYERNUM]->weapon ) @@ -1449,6 +1614,10 @@ void actHudWeapon(Entity* my) { HUDWEAPON_MOVEX = 5; HUDWEAPON_CHOP = 3; + if ( pickaxeGimpTimer < TICKS_PER_SECOND / 2 ) + { + pickaxeGimpTimer = TICKS_PER_SECOND / 2; // fix for swapping weapon causing issues. + } Entity* player = players[HUDWEAPON_PLAYERNUM]->entity; lineTrace(player, player->x, player->y, player->yaw, STRIKERANGE, 0, false); if ( hit.entity && stats[HUDWEAPON_PLAYERNUM]->weapon ) @@ -1484,9 +1653,14 @@ void actHudWeapon(Entity* my) messagePlayer(HUDWEAPON_PLAYERNUM, MESSAGE_HINT, Language::get(3336)); } throwGimpTimer = TICKS_PER_SECOND / 2; + if ( pickaxeGimpTimer < TICKS_PER_SECOND / 2 ) + { + pickaxeGimpTimer = TICKS_PER_SECOND / 2; // fix for swapping weapon causing issues. + } } } - else if ((itemCategory(item) == POTION || itemCategory(item) == GEM || itemCategory(item) == THROWN || item->type == FOOD_CREAMPIE ) + else if ((itemCategory(item) == POTION || itemCategory(item) == GEM || itemCategory(item) == THROWN || item->type == FOOD_CREAMPIE + || item->type == TOOL_DUCK) && !throwGimpTimer) { if ( itemCategory(item) == THROWN ) @@ -1531,8 +1705,11 @@ void actHudWeapon(Entity* my) if ( !hideWeapon && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == SLING || stats[HUDWEAPON_PLAYERNUM]->weapon->type == SHORTBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BONE_SHORTBOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == ARTIFACT_BOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == LONGBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BRANCH_BOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BRANCH_BOW_INFECTED || stats[HUDWEAPON_PLAYERNUM]->weapon->type == COMPOUND_BOW) ) { // not drawing bow anymore, reset. @@ -1732,39 +1909,66 @@ void actHudWeapon(Entity* my) { HUDWEAPON_YAW = 0; } - HUDWEAPON_PITCH -= .1; - if ( HUDWEAPON_PITCH < -PI / 4) - { - result = -PI / 4; - HUDWEAPON_PITCH = result; - } HUDWEAPON_ROLL += .25; if ( HUDWEAPON_ROLL > 0 ) { HUDWEAPON_ROLL = 0; } int targetY = -2; - if ( thrownWeapon ) - { - targetY = -1; - HUDWEAPON_MOVEY -= .25; - HUDWEAPON_MOVEX -= .15; - } - else + int targetX = -1; + real_t targetPitch = -PI / 4; + if ( flail ) { + targetX = 2; + HUDWEAPON_MOVEX += .15; HUDWEAPON_MOVEY -= .45; - HUDWEAPON_MOVEX -= .35; + if ( HUDWEAPON_MOVEX > targetX ) + { + HUDWEAPON_MOVEX = targetX; + } + + targetPitch = -PI / 32; + HUDWEAPON_PITCH -= .1; + if ( HUDWEAPON_PITCH < targetPitch ) + { + HUDWEAPON_PITCH = targetPitch; + } } - if ( HUDWEAPON_MOVEX < -1 ) + else { - HUDWEAPON_MOVEX = -1; + HUDWEAPON_PITCH -= .1; + if ( HUDWEAPON_PITCH < targetPitch ) + { + HUDWEAPON_PITCH = targetPitch; + } + + if ( thrownWeapon ) + { + targetY = -1; + HUDWEAPON_MOVEY -= .25; + HUDWEAPON_MOVEX -= .15; + } + else + { + HUDWEAPON_MOVEY -= .45; + HUDWEAPON_MOVEX -= .35; + } + if ( HUDWEAPON_MOVEX < targetX ) + { + HUDWEAPON_MOVEX = targetX; + } } if ( HUDWEAPON_MOVEY < targetY ) { HUDWEAPON_MOVEY = targetY; } int targetZ = -6; - if ( whip ) + if ( flail ) + { + targetZ = -5; + HUDWEAPON_MOVEZ -= .65; + } + else if ( whip ) { targetZ = -6; HUDWEAPON_MOVEZ -= .32; @@ -1782,50 +1986,88 @@ void actHudWeapon(Entity* my) { HUDWEAPON_MOVEZ -= .65; } + + bool parry = (claymore) && input.binaryToggle("Defend"); + if ( HUDWEAPON_MOVEZ < targetZ ) { HUDWEAPON_MOVEZ = targetZ; - if ( HUDWEAPON_PITCH == result && HUDWEAPON_ROLL == 0 && HUDWEAPON_YAW == 0 && HUDWEAPON_MOVEX == -1 && HUDWEAPON_MOVEY == targetY ) + if ( HUDWEAPON_PITCH == targetPitch && HUDWEAPON_ROLL == 0 && HUDWEAPON_YAW == 0 && HUDWEAPON_MOVEX == targetX && HUDWEAPON_MOVEY == targetY ) { - if ( !swingweapon ) + if ( claymore && (HUDWEAPON_OVERCHARGE > 0 + /*|| (*cvar_claymore_toggle == 2 && HUDWEAPON_CHARGE >= Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM]) / 2)*/) ) { - HUDWEAPON_CHOP++; - if ( !bearTrap ) + /*if ( *cvar_claymore_toggle == 2 ) { - players[HUDWEAPON_PLAYERNUM]->entity->attack(1, HUDWEAPON_CHARGE, nullptr); - } - if ( stats[HUDWEAPON_PLAYERNUM]->weapon - && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW) ) + HUDWEAPON_CHARGE = Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM]); + }*/ + HUDWEAPON_CHOP = 4; + HUDWEAPON_OVERCHARGE = 0; + } + else if ( !swingweapon && flail && HUDWEAPON_OVERCHARGE == 0 ) + { + HUDWEAPON_CHARGE = 0; + HUDWEAPON_OVERCHARGE = 0; + HUDWEAPON_CHOP = 24; + } + else if ( !swingweapon || parry ) + { + HUDWEAPON_CHOP++; + if ( parry ) { - throwGimpTimer = 40; // fix for swapping weapon to crossbow while charging. + input.consumeBinaryToggle("Defend"); + HUDWEAPON_CHOP = 21; + pickaxeGimpTimer = 40; + HUDWEAPON_PARRY_TICK = my->ticks; + players[HUDWEAPON_PLAYERNUM]->entity->attack(MONSTER_POSE_PARRY, 35, nullptr); + HUDWEAPON_MOVEX = 0.0; + HUDWEAPON_MOVEY = -1; + HUDWEAPON_MOVEZ = -2; + HUDWEAPON_YAW = 2 * PI / 5; + HUDWEAPON_PITCH = 0.2; + HUDWEAPON_ROLL = -2 * PI / 5; } - else if ( (stats[HUDWEAPON_PLAYERNUM]->weapon - && stats[HUDWEAPON_PLAYERNUM]->weapon->type == TOOL_PICKAXE) || whip ) + else { - if ( pickaxeGimpTimer < 20 ) + if ( !bearTrap ) { - pickaxeGimpTimer = 20; // fix for swapping weapon from pickaxe causing issues. + players[HUDWEAPON_PLAYERNUM]->entity->attack(1, HUDWEAPON_CHARGE, nullptr); + } + if ( stats[HUDWEAPON_PLAYERNUM]->weapon + && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW) ) + { + throwGimpTimer = 40; // fix for swapping weapon to crossbow while charging. + } + else if ( (stats[HUDWEAPON_PLAYERNUM]->weapon + && stats[HUDWEAPON_PLAYERNUM]->weapon->type == TOOL_PICKAXE) || whip ) + { + if ( pickaxeGimpTimer < 20 ) + { + pickaxeGimpTimer = 20; // fix for swapping weapon from pickaxe causing issues. + } } - } - if ( multiplayer == CLIENT && (thrownWeapon || (stats[HUDWEAPON_PLAYERNUM]->weapon && itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) == POTION)) ) - { - Item* item = stats[HUDWEAPON_PLAYERNUM]->weapon; - if ( item ) + if ( multiplayer == CLIENT && (thrownWeapon || (stats[HUDWEAPON_PLAYERNUM]->weapon && itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) == POTION)) ) { - item->count--; - if ( item->count <= 0 ) + Item* item = stats[HUDWEAPON_PLAYERNUM]->weapon; + if ( item ) { - if ( item->node ) - { - list_RemoveNode(item->node); - } - else + item->count--; + if ( item->count <= 0 ) { - free(item); + if ( item->node ) + { + list_RemoveNode(item->node); + } + else + { + free(item); + } + stats[HUDWEAPON_PLAYERNUM]->weapon = NULL; } - stats[HUDWEAPON_PLAYERNUM]->weapon = NULL; } } } @@ -1839,7 +2081,14 @@ void actHudWeapon(Entity* my) } else { - HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, MAXCHARGE); + if ( flail ) + { + HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + (ticks % 4 == 0 ? 1 : 0), Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM])); + } + else + { + HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM])); + } } } } @@ -1886,6 +2135,30 @@ void actHudWeapon(Entity* my) HUDWEAPON_CHOP++; } } + else if ( flail ) + { + static ConsoleVariable cvar_anim_flail("/anim_flail", 1.0); + static ConsoleVariable cvar_anim_flail_x1("/anim_flail_x1", 0.3); + static ConsoleVariable cvar_anim_flail_x2("/anim_flail_x2", 8.0); + static ConsoleVariable cvar_anim_flail_x3("/anim_flail_x3", 0.0); + HUDWEAPON_PITCH += .55 * *cvar_anim_flail; + if ( HUDWEAPON_PITCH >= PI * *cvar_anim_flail_x1 ) + { + HUDWEAPON_PITCH = PI * *cvar_anim_flail_x1; + } + HUDWEAPON_MOVEX += 1.5 * *cvar_anim_flail; + if ( HUDWEAPON_MOVEX > *cvar_anim_flail_x2 ) + { + HUDWEAPON_MOVEX = *cvar_anim_flail_x2; + } + HUDWEAPON_MOVEZ += .8; + + if ( HUDWEAPON_MOVEZ > 0.0 ) + { + HUDWEAPON_MOVEZ = 0.0; + HUDWEAPON_CHOP++; + } + } else { HUDWEAPON_PITCH += .75; @@ -1923,11 +2196,27 @@ void actHudWeapon(Entity* my) && item->type != FOOD_CREAMPIE && !(item->type >= ARTIFACT_ORB_BLUE && item->type <= ARTIFACT_ORB_GREEN) && !(itemIsThrowableTinkerTool(item)) - && item->type != TOOL_WHIP ) + && item->type != TOOL_DUCK + && item->type != RAPIER + && item->type != TOOL_WHIP + && item->type != STEEL_FLAIL ) { if ( stats[HUDWEAPON_PLAYERNUM]->weapon->type != TOOL_PICKAXE && itemCategory(item) != THROWN ) { - HUDWEAPON_CHOP = 4; + if ( claymore ) + { + HUDWEAPON_CHOP = 7; + HUDWEAPON_YAW = 3 * PI / 4; + HUDWEAPON_MOVEX = sin(HUDWEAPON_YAW) * 1; + HUDWEAPON_MOVEY = cos(HUDWEAPON_YAW) * -6; + HUDWEAPON_MOVEZ = -4; + HUDWEAPON_PITCH = 0.0; + HUDWEAPON_ROLL = - PI / 2; + } + else + { + HUDWEAPON_CHOP = 4; + } } else { @@ -1954,8 +2243,11 @@ void actHudWeapon(Entity* my) { if ( stats[HUDWEAPON_PLAYERNUM]->weapon->type == SLING || stats[HUDWEAPON_PLAYERNUM]->weapon->type == SHORTBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BONE_SHORTBOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == ARTIFACT_BOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == LONGBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BRANCH_BOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BRANCH_BOW_INFECTED || stats[HUDWEAPON_PLAYERNUM]->weapon->type == COMPOUND_BOW ) { if (bowFire) @@ -2047,7 +2339,9 @@ void actHudWeapon(Entity* my) { HUDWEAPON_MOVEY = 0; } + HUDWEAPON_MOVEZ -= .35; + if ( HUDWEAPON_MOVEZ < 0 ) { HUDWEAPON_MOVEZ = 0; @@ -2082,6 +2376,22 @@ void actHudWeapon(Entity* my) rateY = .55; rateRoll = .35; } + else if ( claymore ) + { + real_t factor = 0.75; + rateY *= factor; + rateRoll *= factor; + HUDWEAPON_PITCH -= 0.25 * factor; + if ( HUDWEAPON_PITCH < targetPitch ) + { + HUDWEAPON_PITCH = targetPitch; + } + HUDWEAPON_YAW -= 0.25; + if ( HUDWEAPON_YAW < 0.0 ) + { + HUDWEAPON_YAW = 0.0; + } + } else { HUDWEAPON_YAW = 0; @@ -2113,7 +2423,7 @@ void actHudWeapon(Entity* my) HUDWEAPON_ROLL = targetRoll; if (HUDWEAPON_PITCH == targetPitch && HUDWEAPON_MOVEX == 0 && HUDWEAPON_MOVEY == targetY && HUDWEAPON_MOVEZ == targetZ) { - if (!swingweapon) + if (!swingweapon || claymore ) { HUDWEAPON_CHOP++; if ( !bearTrap ) @@ -2121,7 +2431,9 @@ void actHudWeapon(Entity* my) players[HUDWEAPON_PLAYERNUM]->entity->attack(2, HUDWEAPON_CHARGE, nullptr); } if ( stats[HUDWEAPON_PLAYERNUM]->weapon - && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW) ) + && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW) ) { throwGimpTimer = 40; // fix for swapping weapon to crossbow while charging. } @@ -2130,6 +2442,10 @@ void actHudWeapon(Entity* my) if (players[HUDWEAPON_PLAYERNUM]->entity->skill[3] == 0) // debug cam OFF { camera_shakex += .07; + if ( claymore ) + { + camera_shakex += .07; + } } if ( whip && pickaxeGimpTimer < 20 ) { @@ -2138,7 +2454,7 @@ void actHudWeapon(Entity* my) } else { - HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, MAXCHARGE); + HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM])); } } } @@ -2175,6 +2491,17 @@ void actHudWeapon(Entity* my) HUDWEAPON_MOVEY += 10; } } + else if ( claymore ) + { + HUDWEAPON_MOVEX = sin(HUDWEAPON_YAW) * 1; + HUDWEAPON_MOVEY = cos(HUDWEAPON_YAW) * -6; + HUDWEAPON_YAW += .25; + if ( HUDWEAPON_YAW > (3 * PI) / 4 ) + { + HUDWEAPON_YAW = (3 * PI) / 4; + HUDWEAPON_CHOP++; + } + } else { HUDWEAPON_MOVEX = sin(HUDWEAPON_YAW) * 1; @@ -2195,6 +2522,11 @@ void actHudWeapon(Entity* my) if ( stats[HUDWEAPON_PLAYERNUM]->weapon && !hideWeapon ) { int weaponSkill = getWeaponSkill(stats[HUDWEAPON_PLAYERNUM]->weapon); + /*if ( claymore && *cvar_claymore_toggle == 1 ) + { + HUDWEAPON_CHOP = 1; + } + else*/ if ( weaponSkill == PRO_SWORD || stats[HUDWEAPON_PLAYERNUM]->weapon->type == STEEL_HALBERD ) { HUDWEAPON_CHOP = 7; // swords + halberds can stab @@ -2267,6 +2599,7 @@ void actHudWeapon(Entity* my) } else if ( HUDWEAPON_CHOP == 7 ) // prepare for third swing { + real_t pitchLimit = .2; HUDWEAPON_MOVEX -= .35; if ( HUDWEAPON_MOVEX < 0 ) { @@ -2282,13 +2615,7 @@ void actHudWeapon(Entity* my) { HUDWEAPON_MOVEZ = -2; } - HUDWEAPON_YAW -= .15; - if ( HUDWEAPON_YAW < 2 * PI / 5 ) - { - result = 2 * PI / 5; - HUDWEAPON_YAW = result; - } - real_t pitchLimit = .2; + if ( playerRace == SPIDER ) { pitchLimit = -PI / 2; @@ -2302,29 +2629,83 @@ void actHudWeapon(Entity* my) { HUDWEAPON_PITCH = pitchLimit; } + + HUDWEAPON_YAW -= .15; + if ( HUDWEAPON_YAW < 2 * PI / 5 ) + { + result = 2 * PI / 5; + HUDWEAPON_YAW = result; + } HUDWEAPON_ROLL -= .15; + + bool parry = (rapier || claymore) && input.binaryToggle("Defend"); + if (HUDWEAPON_ROLL < -2 * PI / 5) { HUDWEAPON_ROLL = -2 * PI / 5; if (HUDWEAPON_PITCH == pitchLimit && HUDWEAPON_YAW == result && HUDWEAPON_MOVEX == 0 && HUDWEAPON_MOVEY == -1 && (HUDWEAPON_MOVEZ == -2)) { - if (!swingweapon) + if ( claymore + && (HUDWEAPON_OVERCHARGE > 0 + /*|| (*cvar_claymore_toggle == 2 && HUDWEAPON_CHARGE >= Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM]) / 2)*/) ) + { + /*if ( *cvar_claymore_toggle == 2 ) + { + HUDWEAPON_CHARGE = Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM]); + }*/ + HUDWEAPON_CHOP = 4; + HUDWEAPON_OVERCHARGE = 0; + } + else if ( (!swingweapon || parry) && HUDWEAPON_DELAY_TICK == 0) { HUDWEAPON_CHOP++; - if ( !bearTrap ) + if ( parry ) + { + input.consumeBinaryToggle("Defend"); + HUDWEAPON_CHOP = 21; + pickaxeGimpTimer = 40; + HUDWEAPON_PARRY_TICK = my->ticks; + players[HUDWEAPON_PLAYERNUM]->entity->attack(MONSTER_POSE_PARRY, 35, nullptr); + } + else if ( rapier ) + { + players[HUDWEAPON_PLAYERNUM]->entity->attack(3, HUDWEAPON_CHARGE, nullptr); + } + else if ( !bearTrap && !flail ) { if ( stats[HUDWEAPON_PLAYERNUM]->weapon && hideWeapon ) { players[HUDWEAPON_PLAYERNUM]->entity->attack(1, HUDWEAPON_CHARGE, nullptr); } + else if ( !hideWeapon && stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type == MAGICSTAFF_SCEPTER ) + { + int chargeAmount = HUDWEAPON_CHARGE; + if ( HUDWEAPON_OVERCHARGE >= (Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM]) - 3) ) + { + int staffCharge = stats[HUDWEAPON_PLAYERNUM]->weapon->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX; + if ( staffCharge > 0 ) + { + int decrement = std::min(staffCharge, 5); + stats[HUDWEAPON_PLAYERNUM]->weapon->appearance -= decrement; + chargeAmount = 100; + } + else + { + chargeAmount = 99; + } + } + players[HUDWEAPON_PLAYERNUM]->entity->attack(3, chargeAmount, nullptr); + } else { players[HUDWEAPON_PLAYERNUM]->entity->attack(3, HUDWEAPON_CHARGE, nullptr); } } if ( stats[HUDWEAPON_PLAYERNUM]->weapon - && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW) ) + && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW) ) { throwGimpTimer = 40; // fix for swapping weapon to crossbow while charging. } @@ -2338,7 +2719,7 @@ void actHudWeapon(Entity* my) } else { - HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, MAXCHARGE); + HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM])); } } } @@ -2404,9 +2785,24 @@ void actHudWeapon(Entity* my) } else if ( !(stats[HUDWEAPON_PLAYERNUM]->weapon && stats[HUDWEAPON_PLAYERNUM]->weapon->type == TOOL_BEARTRAP) ) { - if ( itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) != MAGICSTAFF + /*if ( claymore && *cvar_claymore_toggle == 1 ) + { + HUDWEAPON_CHARGE = Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM]); + HUDWEAPON_CHOP = 4; + HUDWEAPON_OVERCHARGE = 0; + } + else*/ + if ( rapier ) + { + HUDWEAPON_CHOP = 7; + } + else if ( stats[HUDWEAPON_PLAYERNUM]->weapon + && itemCategory(stats[HUDWEAPON_PLAYERNUM]->weapon) != MAGICSTAFF && stats[HUDWEAPON_PLAYERNUM]->weapon->type != CRYSTAL_SPEAR && stats[HUDWEAPON_PLAYERNUM]->weapon->type != IRON_SPEAR + && stats[HUDWEAPON_PLAYERNUM]->weapon->type != BLACKIRON_TRIDENT + && stats[HUDWEAPON_PLAYERNUM]->weapon->type != BONE_SPEAR + && stats[HUDWEAPON_PLAYERNUM]->weapon->type != SILVER_GLAIVE && stats[HUDWEAPON_PLAYERNUM]->weapon->type != ARTIFACT_SPEAR ) { HUDWEAPON_CHOP = 1; @@ -2483,7 +2879,10 @@ void actHudWeapon(Entity* my) } if ( !bearTrap ) { + Item* tmp = stats[HUDWEAPON_PLAYERNUM]->weapon; + stats[HUDWEAPON_PLAYERNUM]->weapon = nullptr; players[HUDWEAPON_PLAYERNUM]->entity->attack(PLAYER_POSE_GOLEM_SMASH, MAXCHARGE, nullptr); + stats[HUDWEAPON_PLAYERNUM]->weapon = tmp; } } } @@ -2533,6 +2932,7 @@ void actHudWeapon(Entity* my) if ( HUDWEAPON_YAW == -.1 && HUDWEAPON_PITCH == 0 && HUDWEAPON_MOVEZ == 0 && HUDWEAPON_MOVEY == 0 && HUDWEAPON_MOVEX == 0 ) { HUDWEAPON_CHOP = 0; + players[HUDWEAPON_PLAYERNUM]->hud.weaponSwitch = true; } } } @@ -2646,7 +3046,7 @@ void actHudWeapon(Entity* my) } else { - HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, MAXCHARGE); + HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM])); } } } @@ -2717,7 +3117,14 @@ void actHudWeapon(Entity* my) } else if ( HUDWEAPON_MOVEX < 0 ) { - HUDWEAPON_MOVEX = std::min(HUDWEAPON_MOVEX + .15, 0.0); + real_t reloadSpeed = 1.0; + if ( stats[HUDWEAPON_PLAYERNUM]->weapon + && (stats[HUDWEAPON_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDWEAPON_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW) ) + { + reloadSpeed += 5 * std::max(0.0, 1.0 - rangedAttackGetSpeedModifier(stats[HUDWEAPON_PLAYERNUM])); + } + HUDWEAPON_MOVEX = std::min(HUDWEAPON_MOVEX + .15 * reloadSpeed, 0.0); if ( HUDWEAPON_MOVEX > -1 ) { if ( HUDWEAPON_CROSSBOW_RELOAD_ANIMATION == CROSSBOW_ANIM_SHOOT @@ -2993,7 +3400,7 @@ void actHudWeapon(Entity* my) } else { - HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, MAXCHARGE); + HUDWEAPON_CHARGE = std::min(HUDWEAPON_CHARGE + 1, Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM])); } } } @@ -3049,24 +3456,207 @@ void actHudWeapon(Entity* my) } } } + else if ( HUDWEAPON_CHOP == 21 ) // parry + { + //HUDWEAPON_MOVEX = std::min(static_cast(1.0), HUDWEAPON_MOVEX + .15); // forward/back + //HUDWEAPON_MOVEY = std::min(static_cast(1.0), HUDWEAPON_MOVEY + .25); // left/right + //HUDWEAPON_MOVEZ = std::min(static_cast(0.0), HUDWEAPON_MOVEZ + .05); // up/down + real_t spd = 1.0; - if ( HUDWEAPON_CHARGE == MAXCHARGE || castStrikeAnimation - || players[HUDWEAPON_PLAYERNUM]->entity->skill[9] == MONSTER_POSE_SPECIAL_WINDUP2 - || shakeRangedWeapon ) + real_t z_ang = *cvar_hudweapon_z_ang; + real_t z_spd = *cvar_hudweapon_z_spd; + real_t roll_ang = *cvar_hudweapon_roll_ang; + if ( rapier ) + { + z_ang -= 4.0; + z_spd = 1.0; + roll_ang += -1.0; + } + + hudWeaponAnimateVariable(HUDWEAPON_MOVEX, *cvar_hudweapon_x_ang, *cvar_hudweapon_x_spd * spd); + hudWeaponAnimateVariable(HUDWEAPON_MOVEY, *cvar_hudweapon_y_ang, *cvar_hudweapon_y_spd * spd); + hudWeaponAnimateVariable(HUDWEAPON_MOVEZ, z_ang, z_spd * spd); + hudWeaponAnimateVariable(HUDWEAPON_PITCH, *cvar_hudweapon_pitch_ang, *cvar_hudweapon_pitch_spd * spd); + hudWeaponAnimateVariable(HUDWEAPON_ROLL, roll_ang, *cvar_hudweapon_roll_spd * spd); + hudWeaponAnimateVariable(HUDWEAPON_YAW, *cvar_hudweapon_yaw_ang, *cvar_hudweapon_yaw_spd * spd); + if ( (abs(HUDWEAPON_ROLL - roll_ang) < 0.001 || *cvar_hudweapon_roll_spd == 0.f) + && (abs(HUDWEAPON_PITCH - *cvar_hudweapon_pitch_ang) < 0.001 || *cvar_hudweapon_pitch_spd == 0.f) + && (abs(HUDWEAPON_YAW - *cvar_hudweapon_yaw_ang) < 0.001 || *cvar_hudweapon_yaw_spd == 0.f) + && (abs(HUDWEAPON_MOVEX - *cvar_hudweapon_x_ang) < 0.001 || *cvar_hudweapon_x_spd == 0.f) + && (abs(HUDWEAPON_MOVEY - *cvar_hudweapon_y_ang) < 0.001 || *cvar_hudweapon_y_spd == 0.f) + && (abs(HUDWEAPON_MOVEZ - z_ang) < 0.001 || z_spd == 0.f) ) + { + HUDWEAPON_CHOP = 22; + } + } + else if ( HUDWEAPON_CHOP == 22 ) { - if ( ticks % 5 == 0 && players[HUDWEAPON_PLAYERNUM]->entity->skill[9] == MONSTER_POSE_SPECIAL_WINDUP2 ) + hudWeaponAnimateVariable(HUDWEAPON_MOVEX, 1.0, 0.15); + if ( abs(HUDWEAPON_MOVEX - 1.0) < 0.001 ) { - camera_shakey += 6; + HUDWEAPON_CHOP = 23; } - if ( ticks % 2 == 0 ) + } + else if ( HUDWEAPON_CHOP == 23 ) + { + /*if ( swingweapon ) { - if ( shakeRangedWeapon ) + HUDWEAPON_CHOP = 7; + } + else*/ + { + real_t scale = *cvar_hudweapon_timescale2; + real_t scaleRoll = *cvar_hudweapon_timescale3; + //hudWeaponAnimateVariable(HUDWEAPON_MOVEX, 0.0, 0.25 * scale); + //hudWeaponAnimateVariable(HUDWEAPON_MOVEY, 0.0, 1.0 * scale); + //hudWeaponAnimateVariable(HUDWEAPON_MOVEZ, 0.0, 0.25 * scale); + //hudWeaponAnimateVariable(HUDWEAPON_PITCH, 0.0, 0.1 * scale); + //hudWeaponAnimateVariable(HUDWEAPON_ROLL, 0.0, 0.025 * scaleRoll); + //hudWeaponAnimateVariable(HUDWEAPON_YAW, -.1, 0.2 * scale); + HUDWEAPON_MOVEX *= scale; + HUDWEAPON_MOVEY *= scale; + HUDWEAPON_MOVEZ *= scale; + HUDWEAPON_YAW = HUDWEAPON_YAW + (-.1 - HUDWEAPON_YAW) * (1.0 - scale); + HUDWEAPON_PITCH *= scale; + HUDWEAPON_ROLL *= scale; + if ( abs(HUDWEAPON_MOVEX) < 0.01 ) { - HUDWEAPON_MOVEX -= HUDWEAPON_OLDVIBRATEX; - HUDWEAPON_MOVEY -= HUDWEAPON_OLDVIBRATEY; - HUDWEAPON_OLDVIBRATEX = (local_rng.rand() % 30 - 10) / 150.f; - HUDWEAPON_OLDVIBRATEY = (local_rng.rand() % 30 - 10) / 150.f; - HUDWEAPON_MOVEX += HUDWEAPON_OLDVIBRATEX; + HUDWEAPON_MOVEX = 0; + } + if ( abs(HUDWEAPON_MOVEY) < 0.01 ) + { + HUDWEAPON_MOVEY = 0; + } + if ( abs(HUDWEAPON_MOVEZ) < 0.01 ) + { + HUDWEAPON_MOVEZ = 0; + } + if ( abs(-.1 - HUDWEAPON_YAW) < 0.01 ) + { + HUDWEAPON_YAW = -.1; + } + if ( abs(HUDWEAPON_PITCH) < 0.01 ) + { + HUDWEAPON_PITCH = 0; + } + if ( abs(HUDWEAPON_ROLL) < 0.01 ) + { + HUDWEAPON_ROLL = 0; + } + if ( (HUDWEAPON_YAW == -.1 && HUDWEAPON_ROLL == 0 && HUDWEAPON_PITCH == 0 && HUDWEAPON_MOVEZ == 0 && HUDWEAPON_MOVEY == 0 && HUDWEAPON_MOVEX == 0) + || ((my->ticks - HUDWEAPON_PARRY_TICK) > TICKS_PER_SECOND) ) + { + //messagePlayer(0, MESSAGE_DEBUG, "Parry ticks: %d", my->ticks - HUDWEAPON_PARRY_TICK); + + HUDWEAPON_YAW = -.1; + HUDWEAPON_ROLL = 0; + HUDWEAPON_PITCH = 0; + HUDWEAPON_MOVEX = 0; + HUDWEAPON_MOVEY = 0; + HUDWEAPON_MOVEZ = 0; + + HUDWEAPON_PARRY_TICK = 0; + HUDWEAPON_CHARGE = 0; + HUDWEAPON_OVERCHARGE = 0; + if ( pickaxeGimpTimer > 2 ) + { + pickaxeGimpTimer = 2; + } + if ( swingweapon && (rapier || claymore) ) + { + /*if ( claymore && *cvar_claymore_toggle == 1) + { + HUDWEAPON_CHOP = 1; + } + else*/ + { + HUDWEAPON_CHOP = 7; + } + } + else + { + HUDWEAPON_CHOP = 0; + } + } + } + } + else if ( HUDWEAPON_CHOP == 24 ) + { + //static ConsoleVariable cvar_anim_flail2("/anim_flail2", 0.25); + real_t rate = 0.25; + if ( HUDWEAPON_MOVEX > 0 ) + { + HUDWEAPON_MOVEX = std::max(HUDWEAPON_MOVEX - 1 * rate, 0.0); + } + else if ( HUDWEAPON_MOVEX < 0 ) + { + HUDWEAPON_MOVEX = std::min(HUDWEAPON_MOVEX + 1 * rate, 0.0); + } + if ( HUDWEAPON_MOVEY > 0 ) + { + HUDWEAPON_MOVEY = std::max(HUDWEAPON_MOVEY - 1 * rate, 0.0); + } + else if ( HUDWEAPON_MOVEY < 0 ) + { + HUDWEAPON_MOVEY = std::min(HUDWEAPON_MOVEY + 1 * rate, 0.0); + } + if ( HUDWEAPON_MOVEZ > 0 ) + { + HUDWEAPON_MOVEZ = std::max(HUDWEAPON_MOVEZ - 1 * rate, 0.0); + } + else if ( HUDWEAPON_MOVEZ < 0 ) + { + HUDWEAPON_MOVEZ = std::min(HUDWEAPON_MOVEZ + 1 * rate, 0.0); + } + if ( HUDWEAPON_YAW > -.1 ) + { + HUDWEAPON_YAW = std::max(HUDWEAPON_YAW - .1 * rate, -.1); + } + else if ( HUDWEAPON_YAW < -.1 ) + { + HUDWEAPON_YAW = std::min(HUDWEAPON_YAW + .1 * rate, -.1); + } + if ( HUDWEAPON_PITCH > 0 ) + { + HUDWEAPON_PITCH = std::max(HUDWEAPON_PITCH - .1 * rate, 0.0); + } + else if ( HUDWEAPON_PITCH < 0 ) + { + HUDWEAPON_PITCH = std::min(HUDWEAPON_PITCH + .1 * rate, 0.0); + } + if ( HUDWEAPON_ROLL > 0 ) + { + HUDWEAPON_ROLL = std::max(HUDWEAPON_ROLL - .1 * rate, 0.0); + } + else if ( HUDWEAPON_ROLL < 0 ) + { + HUDWEAPON_ROLL = std::min(HUDWEAPON_ROLL + .1 * rate, 0.0); + } + + if ( HUDWEAPON_MOVEX == 0 && HUDWEAPON_MOVEY == 0 && HUDWEAPON_MOVEZ == 0 + && HUDWEAPON_YAW == -.1 && HUDWEAPON_PITCH == 0 && HUDWEAPON_ROLL == 0 ) + { + HUDWEAPON_CHOP = 0; + } + } + + if ( HUDWEAPON_CHARGE == Stat::getMaxAttackCharge(stats[HUDWEAPON_PLAYERNUM]) || castStrikeAnimation + || players[HUDWEAPON_PLAYERNUM]->entity->skill[9] == MONSTER_POSE_SPECIAL_WINDUP2 + || shakeRangedWeapon ) + { + if ( ticks % 5 == 0 && players[HUDWEAPON_PLAYERNUM]->entity->skill[9] == MONSTER_POSE_SPECIAL_WINDUP2 ) + { + camera_shakey += 6; + } + if ( ticks % 2 == 0 ) + { + if ( shakeRangedWeapon ) + { + HUDWEAPON_MOVEX -= HUDWEAPON_OLDVIBRATEX; + HUDWEAPON_MOVEY -= HUDWEAPON_OLDVIBRATEY; + HUDWEAPON_OLDVIBRATEX = (local_rng.rand() % 30 - 10) / 150.f; + HUDWEAPON_OLDVIBRATEY = (local_rng.rand() % 30 - 10) / 150.f; + HUDWEAPON_MOVEX += HUDWEAPON_OLDVIBRATEX; HUDWEAPON_MOVEY += HUDWEAPON_OLDVIBRATEY; } else @@ -3097,7 +3687,7 @@ void actHudWeapon(Entity* my) { if ( ticks % 5 == 0 ) { - Entity* entity = spawnGib(my); + Entity* entity = spawnGib(my, 16); entity->flags[INVISIBLE] = false; entity->flags[SPRITE] = true; entity->flags[NOUPDATE] = true; @@ -3118,12 +3708,18 @@ void actHudWeapon(Entity* my) } } + // init defaults + my->focalx = 0.0; + my->focaly = 0.0; + my->focalz = -4.0; + // move the weapon if (players[HUDWEAPON_PLAYERNUM] == nullptr || players[HUDWEAPON_PLAYERNUM]->entity == nullptr) { return; } double defaultpitch = PI / 8.f; + if (stats[HUDWEAPON_PLAYERNUM]->weapon == nullptr || hideWeapon) { if ( playerRace == RAT ) @@ -3168,6 +3764,38 @@ void actHudWeapon(Entity* my) { my->z -= -2 * .5; } + else if ( playerRace == GREMLIN ) + { + my->z -= -1 * .5; + } + else if ( playerRace == GNOME ) + { + my->z -= -2 * .5; + } + else if ( playerRace == DRYAD ) + { + if ( players[HUDWEAPON_PLAYERNUM]->entity->z >= 1.5 ) + { + my->z -= -3.0 * .5; + } + else + { + my->z -= -2.0 * .5; + } + } + else if ( playerRace == MYCONID ) + { + my->z -= -1.0 * .5; + } + + if ( stats[HUDWEAPON_PLAYERNUM]->getEffectActive(EFF_MINIMISE) ) + { + my->z += -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HUDWEAPON_PLAYERNUM]->getEffectActive(EFF_MINIMISE) & 0xF) * .5; + } + if ( stats[HUDWEAPON_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) ) + { + my->z -= -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HUDWEAPON_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) & 0xF) * .5; + } } else { @@ -3183,7 +3811,7 @@ void actHudWeapon(Entity* my) { defaultpitch = -PI / 8.f; } - if ( item->type == CROSSBOW || item->type == HEAVY_CROSSBOW ) + if ( item->type == CROSSBOW || item->type == HEAVY_CROSSBOW || item->type == BLACKIRON_CROSSBOW ) { my->x = 6 + HUDWEAPON_MOVEX; my->y = 1.5 + HUDWEAPON_MOVEY; @@ -3205,8 +3833,11 @@ void actHudWeapon(Entity* my) } else if ( item->type == SLING || item->type == SHORTBOW + || item->type == BONE_SHORTBOW || item->type == ARTIFACT_BOW || item->type == LONGBOW + || item->type == BRANCH_BOW + || item->type == BRANCH_BOW_INFECTED || item->type == COMPOUND_BOW ) { my->x = 6 + HUDWEAPON_MOVEX; @@ -3241,6 +3872,62 @@ void actHudWeapon(Entity* my) } my->roll = HUDWEAPON_ROLL; } + else if ( item->type == BOLAS ) + { + my->x = 6 + HUDWEAPON_MOVEX + 3 * (itemCategory(item) == POTION); + my->y = 3 + HUDWEAPON_MOVEY - 3 * (itemCategory(item) == POTION); + my->z = (cameras[HUDWEAPON_PLAYERNUM].z * .5 - players[HUDWEAPON_PLAYERNUM]->entity->z) + 7 + HUDWEAPON_MOVEZ - 3 * (itemCategory(item) == POTION); + my->yaw = HUDWEAPON_YAW - camera_shakex2; + my->pitch = defaultpitch + HUDWEAPON_PITCH - camera_shakey2 / 200.f; + my->roll = HUDWEAPON_ROLL + (PI / 2) * (itemCategory(item) == POTION); + + static real_t bolasSpinVars[MAXPLAYERS] = { 0.0 }; + static real_t bolasSpinVars2[MAXPLAYERS] = { 0.0 }; + real_t& bolasSpinScale = bolasSpinVars[HUDWEAPON_PLAYERNUM]; + real_t& bolasSpinYaw = bolasSpinVars2[HUDWEAPON_PLAYERNUM]; + if ( HUDWEAPON_CHOP == 1 || HUDWEAPON_CHOP == 4 || HUDWEAPON_CHOP == 7 ) + { + bolasSpinScale += 0.05; + if ( bolasSpinScale >= 1.0 ) + { + bolasSpinScale = 1.0; + } + real_t prev = fmod(bolasSpinYaw, 2 * PI); + bolasSpinYaw += bolasSpinScale * -0.35; + real_t next = fmod(bolasSpinYaw, 2 * PI); + if ( (prev > -PI / 4 && next < -PI / 4) + || (prev > -5 * PI / 4 && next < -5 * PI / 4)) + { + playSoundEntityLocal(players[HUDWEAPON_PLAYERNUM]->entity, 765 + local_rng.rand() % 2, 64); + } + } + else + { + bolasSpinScale *= 0.95; + if ( bolasSpinScale < 0.0 ) + { + bolasSpinScale = 0.0; + } + if ( bolasSpinYaw < 0.0 ) + { + bolasSpinYaw = fmod(bolasSpinYaw, 2 * PI); // restrict to -2PI + bolasSpinYaw *= 0.9; // return to 0 + } + } + + + my->pitch *= (1.0 - bolasSpinScale); // keep less of original pitch as start spinning + my->pitch += bolasSpinScale * -(PI / 2 - PI / 32); // move to this pitch as more spinning + + my->yaw += bolasSpinYaw; + + my->y += 1 * std::min(bolasSpinScale, 1.0); // move right as max spin + my->z += -5; + my->z -= 3 * sin(HUDWEAPON_ROLL); // weaponswitch animation make rise from bottom + my->focalx = 0.0; + my->focaly = -0.125; + my->focalz = 2.25; + } else { my->x = 6 + HUDWEAPON_MOVEX + 3 * (itemCategory(item) == POTION); @@ -3249,6 +3936,11 @@ void actHudWeapon(Entity* my) my->yaw = HUDWEAPON_YAW - camera_shakex2; my->pitch = defaultpitch + HUDWEAPON_PITCH - camera_shakey2 / 200.f; my->roll = HUDWEAPON_ROLL + (PI / 2) * (itemCategory(item) == POTION); + if ( flail ) + { + my->roll += 0.05 * sin(HUDFlail[HUDWEAPON_PLAYERNUM].rollSpin); + my->pitch += -0.05 * cos(HUDFlail[HUDWEAPON_PLAYERNUM].rollSpin); + } my->focalx = 0; } @@ -3267,6 +3959,38 @@ void actHudWeapon(Entity* my) { my->z -= -2 * .5; } + else if ( playerRace == GREMLIN ) + { + my->z -= -1 * .5; + } + else if ( playerRace == GNOME ) + { + my->z -= -2 * .5; + } + else if ( playerRace == DRYAD ) + { + if ( players[HUDWEAPON_PLAYERNUM]->entity->z >= 1.5 ) + { + my->z -= -3.0 * .5; + } + else + { + my->z -= -2.0 * .5; + } + } + else if ( playerRace == MYCONID ) + { + my->z -= -1.0 * .5; + } + + if ( stats[HUDWEAPON_PLAYERNUM]->getEffectActive(EFF_MINIMISE) ) + { + my->z += -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HUDWEAPON_PLAYERNUM]->getEffectActive(EFF_MINIMISE) & 0xF) * .5; + } + if ( stats[HUDWEAPON_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) ) + { + my->z -= -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HUDWEAPON_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) & 0xF) * .5; + } } if ( !my->flags[OVERDRAW] ) { @@ -3274,17 +3998,113 @@ void actHudWeapon(Entity* my) my->y += 32; my->z -= 3.5; } + + //Entity* particle = spawnMagicParticle(my); + //particle->flags[OVERDRAW] = true; + //particle->sprite = 942; + //particle->x = my->x; + //particle->y = my->y; + //particle->z = my->z; + //particle->focalx = limbs[HUMAN][22][0]; + //particle->focaly = limbs[HUMAN][22][1]; + //particle->focalz = limbs[HUMAN][22][2]; + + //static real_t p = 0.0; + //if ( keystatus[SDLK_F1] ) + //{ + // if ( keystatus[SDLK_LSHIFT] ) + // { + // p -= 0.1; + // } + // else + // { + // p += 0.1; + // } + //} + //static real_t r = 0.0; + //if ( keystatus[SDLK_F2] ) + //{ + // if ( keystatus[SDLK_LSHIFT] ) + // { + // r -= 0.1; + // } + // else + // { + // r += 0.1; + // } + //} + //static real_t y = 0.0; + //if ( keystatus[SDLK_F3] ) + //{ + // if ( keystatus[SDLK_LSHIFT] ) + // { + // y -= 0.1; + // } + // else + // { + // y += 0.1; + // } + //} + //my->pitch += p; + //my->roll += r; + //my->yaw += y; + + //particle->yaw = my->yaw; + //particle->pitch = my->pitch; + //particle->roll = my->roll; + + + //real_t focalx = limbs[HUMAN][20][0]; + //real_t focaly = limbs[HUMAN][20][1]; + //real_t focalz = (limbs[HUMAN][20][2] + my->focalz); + + //// magic code to translate focals into pure coords (doesn't include focaly) + //real_t xoffset = focalz * sin(my->pitch + PI) * cos(my->roll) * cos(my->yaw); + //real_t yoffset = focalz * sin(my->pitch + PI) * cos(my->roll) * sin(my->yaw); + //xoffset += focalz * sin(my->roll + PI) * cos(my->yaw + PI / 2); + //yoffset += focalz * sin(my->roll + PI) * sin(my->yaw + PI / 2); + + //xoffset += focalx * cos(my->yaw) * sin(my->pitch + PI / 2) + focaly * cos(my->yaw + PI / 2); + //yoffset += focalx * sin(my->yaw) * sin(my->pitch + PI / 2) + focaly * sin(my->yaw + PI / 2); + + //real_t zoffset = focalz * cos(my->pitch) * cos(my->roll); + //zoffset += focalx * sin(my->pitch); + //particle->x += xoffset; + //particle->y += yoffset; + //particle->z += zoffset; + ////particle->flags[OVERDRAW] = true; + + //// true particle left to opengl to translate position to check accuracy + //particle = spawnMagicParticle(my); + //particle->flags[OVERDRAW] = true; + //particle->sprite = 943; + //particle->x = my->x; + //particle->y = my->y; + //particle->z = my->z; + //particle->focalx = my->focalx + limbs[HUMAN][20][0]; + //particle->focaly = my->focaly + limbs[HUMAN][20][1]; + //particle->focalz = my->focalz + limbs[HUMAN][20][2]; + //particle->yaw = my->yaw; + //particle->pitch = my->pitch; + //particle->roll = my->roll; } #define HUDSHIELD_DEFEND my->skill[0] #define HUDSHIELD_SNEAKING my->skill[1] #define HUDSHIELD_PLAYERNUM my->skill[2] +#define HUDSHIELD_DEFEND_DELAY_TICK my->skill[3] +#define HUDSHIELD_DEFEND_TIME my->skill[4] #define HUDSHIELD_MOVEX my->fskill[0] #define HUDSHIELD_MOVEY my->fskill[1] #define HUDSHIELD_MOVEZ my->fskill[2] #define HUDSHIELD_YAW my->fskill[3] #define HUDSHIELD_PITCH my->fskill[4] #define HUDSHIELD_ROLL my->fskill[5] +#define HUDSHIELD_FOCI_SPIN my->fskill[6] +#define HUDSHIELD_FOCI_EASE my->fskill[7] +#define HUDSHIELD_FOCI_BOB my->fskill[8] +#define HUDSHIELD_DUCK_CHARGE my->fskill[9] +static ConsoleVariable cvar_hud_toggle_defend("/hud_toggle_defend", false); void actHudShield(Entity* my) { @@ -3342,6 +4162,32 @@ void actHudShield(Entity* my) } } + my->mistformGLRender = 0.0; + if ( playerRace != SPIDER ) + { + if ( players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender > 0.05 ) + { + real_t modulus = fmod(players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender, 1.0); + if ( modulus >= 0.05 && modulus < 0.15 ) // force shield + { + my->mistformGLRender = 0.5; + } + else if ( modulus >= 0.15 && modulus < 0.25 ) // reflector shield + { + my->mistformGLRender = 0.6; + } + else + { + my->mistformGLRender = players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender; + } + } + } + else if ( playerRace == SPIDER ) + { + my->mistformGLRender = players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender > 0.9 ? players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender + : 0.0; + } + bool hideShield = false; if ( playerRace == RAT || playerRace == CREATURE_IMP @@ -3352,6 +4198,8 @@ void actHudShield(Entity* my) bool spellbook = false; bool quiver = false; + bool foci = false; + bool duck = false; if ( stats[HUDSHIELD_PLAYERNUM]->shield && itemCategory(stats[HUDSHIELD_PLAYERNUM]->shield) == SPELLBOOK ) { spellbook = true; @@ -3364,6 +4212,18 @@ void actHudShield(Entity* my) { quiver = true; } + else if ( stats[HUDSHIELD_PLAYERNUM]->shield && itemTypeIsFoci(stats[HUDSHIELD_PLAYERNUM]->shield->type) ) + { + foci = true; + if ( playerRace == CREATURE_IMP ) + { + hideShield = false; + } + } + else if ( stats[HUDSHIELD_PLAYERNUM]->shield && stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_DUCK ) + { + duck = true; + } // when reverting form, render shield as invisible for 2 ticks as it's position needs to settle. if ( HUD_LASTSHAPESHIFT_FORM != playerRace ) @@ -3376,7 +4236,7 @@ void actHudShield(Entity* my) } HUD_LASTSHAPESHIFT_FORM = playerRace; - if ( players[HUDSHIELD_PLAYERNUM]->entity->skill[3] == 1 ) // debug cam or player invisible + if ( players[HUDSHIELD_PLAYERNUM]->entity->skill[3] != 0 ) // debug cam or player invisible { my->flags[INVISIBLE] = true; } @@ -3451,7 +4311,7 @@ void actHudShield(Entity* my) my->flags[INVISIBLE] = true; my->flags[INVISIBLE_DITHER] = false; } - else if ( cast_animation[HUDSHIELD_PLAYERNUM].active ) + else if ( cast_animation[HUDSHIELD_PLAYERNUM].hideShieldFromBasicCast() ) { my->flags[INVISIBLE] = true; my->flags[INVISIBLE_DITHER] = false; @@ -3483,15 +4343,24 @@ void actHudShield(Entity* my) { allowDefend = false; } + else if ( players[HUDSHIELD_PLAYERNUM]->hud.weapon->skill[6] == 0 // hideWeapon == false + && stats[HUDSHIELD_PLAYERNUM]->weapon /*&& stats[HUDSHIELD_PLAYERNUM]->weapon->type == RAPIER*/ + && (players[HUDSHIELD_PLAYERNUM]->hud.weapon->skill[0] == 21 + || players[HUDSHIELD_PLAYERNUM]->hud.weapon->skill[0] == 22) ) + { + allowDefend = false; + } } if ( players[HUDSHIELD_PLAYERNUM] && players[HUDSHIELD_PLAYERNUM]->entity && allowDefend && players[HUDSHIELD_PLAYERNUM]->entity->isMobile() - && !cast_animation[HUDSHIELD_PLAYERNUM].active + && !cast_animation[HUDSHIELD_PLAYERNUM].hideShieldFromBasicCast() && !cast_animation[HUDSHIELD_PLAYERNUM].active_spellbook - && (!spellbook || (spellbook && hideShield)) ) + && (!spellbook || (spellbook && (hideShield || playerRace == SPIDER))) + && HUDSHIELD_DEFEND_DELAY_TICK == 0 + && !((foci || duck) && !hideShield && players[HUDSHIELD_PLAYERNUM]->hud.shieldSwitch) ) { if ( stats[HUDSHIELD_PLAYERNUM]->shield ) { @@ -3501,6 +4370,10 @@ void actHudShield(Entity* my) { defending = true; } + else if ( *cvar_hud_toggle_defend ) + { + defending = true; + } } wouldBeDefending = true; } @@ -3511,6 +4384,11 @@ void actHudShield(Entity* my) } } + if ( HUDSHIELD_DEFEND_DELAY_TICK > 0 ) + { + --HUDSHIELD_DEFEND_DELAY_TICK; + } + if ( stats[HUDSHIELD_PLAYERNUM]->shield && itemTypeIsQuiver(stats[HUDSHIELD_PLAYERNUM]->shield->type) ) { // can't defend with quivers. @@ -3522,8 +4400,25 @@ void actHudShield(Entity* my) || playerRace == TROLL || playerRace == SPIDER ) { - defending = false; - wouldBeDefending = false; + if ( !(playerRace == CREATURE_IMP && foci) ) + { + defending = false; + wouldBeDefending = false; + } + } + + if ( defending ) + { + if ( foci + || (stats[HUDSHIELD_PLAYERNUM]->shield && itemTypeIsInstrument(stats[HUDSHIELD_PLAYERNUM]->shield->type)) + || duck ) + { + if ( players[HUDSHIELD_PLAYERNUM]->messageZone.logWindow || players[HUDSHIELD_PLAYERNUM]->minimap.mapWindow + || FollowerMenu[HUDSHIELD_PLAYERNUM].followerMenuIsOpen() || CalloutMenu[HUDSHIELD_PLAYERNUM].calloutMenuIsOpen() ) + { + defending = false; + } + } } bool dropShield = false; @@ -3537,10 +4432,12 @@ void actHudShield(Entity* my) if (defending) { stats[HUDSHIELD_PLAYERNUM]->defending = true; + HUDSHIELD_DEFEND_TIME++; } else { stats[HUDSHIELD_PLAYERNUM]->defending = false; + HUDSHIELD_DEFEND_TIME = 0; } if ( sneaking && (!defending && !wouldBeDefending) ) { @@ -3550,9 +4447,10 @@ void actHudShield(Entity* my) { stats[HUDSHIELD_PLAYERNUM]->sneaking = false; } + if (multiplayer == CLIENT) { - if (HUDSHIELD_DEFEND != defending || ticks % 120 == 0) + if ((HUDSHIELD_DEFEND > 0 ? true : false) != defending || ticks % 120 == 0) { strcpy((char*)net_packet->data, "SHLD"); net_packet->data[4] = HUDSHIELD_PLAYERNUM; @@ -3573,7 +4471,7 @@ void actHudShield(Entity* my) sendPacketSafe(net_sock, -1, net_packet, 0); } } - HUDSHIELD_DEFEND = defending; + HUDSHIELD_DEFEND = (defending == false) ? 0 : (HUDSHIELD_DEFEND + 1); HUDSHIELD_SNEAKING = sneaking; if ( dropShield ) @@ -3583,7 +4481,9 @@ void actHudShield(Entity* my) return; } - bool crossbow = (stats[HUDSHIELD_PLAYERNUM]->weapon && (stats[HUDSHIELD_PLAYERNUM]->weapon->type == CROSSBOW || stats[HUDSHIELD_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW) ); + bool crossbow = (stats[HUDSHIELD_PLAYERNUM]->weapon + && (stats[HUDSHIELD_PLAYERNUM]->weapon->type == CROSSBOW || stats[HUDSHIELD_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW + || stats[HUDSHIELD_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW) ); bool doCrossbowReloadAnimation = false; bool doBowReload = false; @@ -3616,6 +4516,10 @@ void actHudShield(Entity* my) } } + if ( (foci || duck) && !hideShield ) + { + HUDSHIELD_DEFEND_DELAY_TICK = 10; + } if ( !spellbook ) { players[HUDSHIELD_PLAYERNUM]->hud.shieldSwitch = false; @@ -3646,7 +4550,10 @@ void actHudShield(Entity* my) // main animation if ( defending || cast_animation[HUDSHIELD_PLAYERNUM].active_spellbook ) { - if ( !spellbook ) + if ( duck ) + { + } + else if ( !spellbook ) { if ( HUDSHIELD_MOVEY < 3 ) { @@ -3674,7 +4581,8 @@ void actHudShield(Entity* my) } if ( stats[HUDSHIELD_PLAYERNUM]->shield ) { - if ( stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_TORCH || stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_CRYSTALSHARD ) + if ( stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_TORCH + || stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_CRYSTALSHARD ) { if ( HUDSHIELD_MOVEX < 1.5 ) { @@ -3693,6 +4601,25 @@ void actHudShield(Entity* my) } } } + else if ( itemTypeIsFoci(stats[HUDSHIELD_PLAYERNUM]->shield->type) ) + { + if ( HUDSHIELD_MOVEX < 1.5 ) + { + HUDSHIELD_MOVEX += .5; + if ( HUDSHIELD_MOVEX > 1.5 ) + { + HUDSHIELD_MOVEX = 1.5; + } + } + /*if ( HUDSHIELD_ROLL < PI / 5 ) + { + HUDSHIELD_ROLL += .15; + if ( HUDSHIELD_ROLL > PI / 5 ) + { + HUDSHIELD_ROLL = PI / 5; + } + }*/ + } } } else @@ -3713,7 +4640,9 @@ void actHudShield(Entity* my) } else if ( !hideShield && quiver && hudweapon && rangedWeaponUseQuiverOnAttack(stats[HUDSHIELD_PLAYERNUM]) && hudweapon->skill[7] != RANGED_ANIM_IDLE - && (!crossbow || (crossbow && crossbowReloadAnimation && hudweapon->skill[8] != CROSSBOW_ANIM_RELOAD_START)) ) + && (!crossbow + || (crossbow && crossbowReloadAnimation && hudweapon->skill[8] != CROSSBOW_ANIM_RELOAD_START + && !cast_animation[HUDSHIELD_PLAYERNUM].spellWaitingAttackInput() && !cast_animation[HUDSHIELD_PLAYERNUM].spellIgnoreAttack()) )) { // skill[7] == 1 is hudweapon bow drawing, skill[8] is the crossbow reload animation state. if ( hudweapon->skill[7] == RANGED_ANIM_FIRED && (!crossbow || (crossbow && hudweapon->skill[8] == CROSSBOW_ANIM_SHOOT)) ) @@ -3975,6 +4904,119 @@ void actHudShield(Entity* my) my->yaw += PI / 2 - HUDSHIELD_YAW / 4; my->focalz -= 0.5; } + else if ( my->sprite == items[TOOL_FRYING_PAN].fpindex && !hideShield ) + { + my->roll += PI / 8; + } + else if ( duck && !hideShield ) + { + my->z += -1; + my->yaw += (PI / 3) + -HUDSHIELD_YAW; + if ( defending && HUDSHIELD_DEFEND_TIME > 0 ) + { + HUDSHIELD_DUCK_CHARGE += 1.0; + } + + real_t chargeAnim = std::min(1.0, HUDSHIELD_DUCK_CHARGE / 10.0); + + if ( HUDSHIELD_DUCK_CHARGE >= 1.0 ) + { + my->pitch += (-PI / 16) * chargeAnim; + my->y += (3 - 3) * chargeAnim; + my->x += 2 * chargeAnim; + //my->yaw += (PI / 3) * chargeAnim; + my->z += -1 * chargeAnim; + + if ( ticks % 2 && HUDSHIELD_DUCK_CHARGE >= 30.0 ) + { + my->x += (local_rng.rand() % 30 - 10) / 80.f; + my->y += (local_rng.rand() % 30 - 10) / 80.f; + my->z += (local_rng.rand() % 30 - 10) / 80.f; + } + } + + if ( HUDSHIELD_DUCK_CHARGE >= 60.0 || (!defending && HUDSHIELD_DUCK_CHARGE >= 30.0) ) + { + if ( playerThrowDuck(HUDSHIELD_PLAYERNUM, stats[HUDSHIELD_PLAYERNUM]->shield, std::min(50, (int)HUDSHIELD_DUCK_CHARGE)) ) + { + my->flags[INVISIBLE] = true; + my->flags[INVISIBLE_DITHER] = false; + return; + } + } + } + else if ( foci && !hideShield ) + { + //my->yaw += PI / 2 - HUDSHIELD_YAW / 4; + my->focalz -= 1.5; + if ( defending ) + { + int rate = 20; + int chargeTimeInit = (float)(TICKS_PER_SECOND / 4); + chargeTimeInit *= getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_FOCI_CAST_TIME, getSpellIDFromFoci(stats[HUDSHIELD_PLAYERNUM]->shield->type), + nullptr, stats[HUDSHIELD_PLAYERNUM], nullptr); + chargeTimeInit = std::max(TICKS_PER_SECOND, chargeTimeInit + TICKS_PER_SECOND); + if ( HUDSHIELD_DEFEND_TIME < chargeTimeInit ) + { + rate = 10; + if ( (chargeTimeInit - HUDSHIELD_DEFEND_TIME) > TICKS_PER_SECOND ) + { + HUDSHIELD_FOCI_SPIN += 0.3; + } + else + { + HUDSHIELD_FOCI_SPIN += 0.15 + 0.25 * (chargeTimeInit - HUDSHIELD_DEFEND_TIME) / (real_t)TICKS_PER_SECOND; + } + + } + else + { + HUDSHIELD_FOCI_SPIN += 0.15; + } + if ( my->ticks % rate == 0 ) + { + Entity* entity = spawnGib(my, 16); + entity->flags[INVISIBLE] = false; + entity->flags[SPRITE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[OVERDRAW] = true; + entity->lightBonus = vec4(0.2f, 0.2f, 0.2f, 0.f); + entity->z = std::max(my->z, entity->z); + entity->z -= 2.0; + //entity->sizex = 1; //MAKE 'EM SMALL PLEASE! + //entity->sizey = 1; + entity->scalex = 0.25f; //MAKE 'EM SMALL PLEASE! + entity->scaley = 0.25f; + entity->scalez = 0.25f; + entity->sprite = 16; //TODO: Originally. 22. 16 -- spark sprite instead? + entity->yaw = ((local_rng.rand() % 6) * 60) * PI / 180.0; + entity->pitch = (local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + entity->vel_x = cos(entity->yaw) * .1; + entity->vel_y = sin(entity->yaw) * .1; + entity->vel_z = -.15; + entity->fskill[3] = 0.01; + entity->skill[11] = HUDSHIELD_PLAYERNUM; + } + HUDSHIELD_FOCI_EASE = std::min(1.0, HUDSHIELD_FOCI_EASE + 0.05); + HUDSHIELD_FOCI_BOB += 0.05; + my->z += 0.5 * sin(HUDSHIELD_FOCI_BOB); + } + my->yaw += HUDSHIELD_FOCI_SPIN * sin(HUDSHIELD_FOCI_EASE * PI / 2); + } + + if ( !foci || hideShield || !defending ) + { + HUDSHIELD_FOCI_SPIN = 0.0; + HUDSHIELD_FOCI_EASE = 0.0; + HUDSHIELD_FOCI_BOB = 0.0; + } + + if ( !duck || hideShield || !defending ) + { + HUDSHIELD_DUCK_CHARGE = 0.0; + } Entity*& hudarm = players[HUDSHIELD_PLAYERNUM]->hud.arm; if ( playerRace == SPIDER && hudarm && players[HUDSHIELD_PLAYERNUM]->entity->bodyparts.at(0) ) @@ -4001,6 +5043,38 @@ void actHudShield(Entity* my) { my->z -= -2 * .5; } + else if ( playerRace == GREMLIN ) + { + my->z -= -1 * .5; + } + else if ( playerRace == GNOME ) + { + my->z -= -2 * .5; + } + else if ( playerRace == DRYAD ) + { + if ( players[HUDSHIELD_PLAYERNUM]->entity->z >= 1.5 ) + { + my->z -= -3.0 * .5; + } + else + { + my->z -= -2.0 * .5; + } + } + else if ( playerRace == MYCONID ) + { + my->z -= -1.0 * .5; + } + + if ( stats[HUDSHIELD_PLAYERNUM]->getEffectActive(EFF_MINIMISE) ) + { + my->z += -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HUDSHIELD_PLAYERNUM]->getEffectActive(EFF_MINIMISE) & 0xF) * .5; + } + if ( stats[HUDSHIELD_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) ) + { + my->z -= -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HUDSHIELD_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) & 0xF) * .5; + } // torch/lantern flames if ( playerRace == TROLL || playerRace == SPIDER || playerRace == CREATURE_IMP || playerRace == RAT ) @@ -4010,53 +5084,326 @@ void actHudShield(Entity* my) } if (stats[HUDSHIELD_PLAYERNUM]->shield && !swimming - && players[HUDSHIELD_PLAYERNUM]->entity->skill[3] == 0 - && !cast_animation[HUDSHIELD_PLAYERNUM].active + && !cast_animation[HUDSHIELD_PLAYERNUM].hideShieldFromBasicCast() && !cast_animation[HUDSHIELD_PLAYERNUM].active_spellbook && !players[HUDSHIELD_PLAYERNUM]->hud.shieldSwitch) { if (itemCategory(stats[HUDSHIELD_PLAYERNUM]->shield) == TOOL) { - if (stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_TORCH) - { - if ( flickerLights || my->ticks % TICKS_PER_SECOND == 1 ) - { - if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) + if ( players[HUDSHIELD_PLAYERNUM]->entity->skill[3] == 0 ) + { + if (stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_TORCH) + { + if ( flickerLights || my->ticks % TICKS_PER_SECOND == 1 ) + { + if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) + { + entity->flags[OVERDRAW] = true; + entity->z -= 2.5 * cos(HUDSHIELD_ROLL); + entity->y += 2.5 * sin(HUDSHIELD_ROLL); + entity->skill[11] = HUDSHIELD_PLAYERNUM; + } + } + } + else if ( stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_CRYSTALSHARD ) + { + if ( flickerLights || my->ticks % TICKS_PER_SECOND == 1 ) { + /*Entity* entity = spawnFlame(my, SPRITE_CRYSTALFLAME); entity->flags[OVERDRAW] = true; entity->z -= 2.5 * cos(HUDSHIELD_ROLL); entity->y += 2.5 * sin(HUDSHIELD_ROLL); - entity->skill[11] = HUDSHIELD_PLAYERNUM; + entity->skill[11] = HUDSHIELD_PLAYERNUM;*/ } - } - } - else if ( stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_CRYSTALSHARD ) - { - if ( flickerLights || my->ticks % TICKS_PER_SECOND == 1 ) - { - /*Entity* entity = spawnFlame(my, SPRITE_CRYSTALFLAME); - entity->flags[OVERDRAW] = true; - entity->z -= 2.5 * cos(HUDSHIELD_ROLL); - entity->y += 2.5 * sin(HUDSHIELD_ROLL); - entity->skill[11] = HUDSHIELD_PLAYERNUM;*/ - } - } - else if (stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_LANTERN) - { - if ( flickerLights || my->ticks % TICKS_PER_SECOND == 1 ) - { - if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) + } + else if (stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_LANTERN) + { + if ( flickerLights || my->ticks % TICKS_PER_SECOND == 1 ) { - entity->flags[OVERDRAW] = true; - entity->skill[11] = HUDSHIELD_PLAYERNUM; - entity->z += 1; + if ( Entity* entity = spawnFlame(my, SPRITE_FLAME) ) + { + entity->flags[OVERDRAW] = true; + entity->skill[11] = HUDSHIELD_PLAYERNUM; + entity->z += 1; + } } - } - } + } + } + + /*if ( multiplayer != CLIENT ) + { + if ( stats[HUDSHIELD_PLAYERNUM]->shield->type == TOOL_FOCI_FIRE ) + { + static ConsoleVariable cvar_foci_charge_init("/foci_charge_init", 1.f); + int chargeTimeInit = (float)(TICKS_PER_SECOND / 4) * *cvar_foci_charge_init; + if ( HUDSHIELD_DEFEND >= chargeTimeInit ) + { + static ConsoleVariable cvar_foci_charge("/foci_charge", 1.f); + int chargeTime = (float)(TICKS_PER_SECOND / 4) * *cvar_foci_charge; + if ( (HUDSHIELD_DEFEND - chargeTimeInit) % chargeTime == 0 ) + { + auto spell = getSpellFromID(SPELL_FOCI_FIRE); + int mpcost = getCostOfSpell(spell, players[HUDSHIELD_PLAYERNUM]->entity); + if ( mpcost <= stats[HUDSHIELD_PLAYERNUM]->MP ) + { + if ( players[HUDSHIELD_PLAYERNUM]->entity->safeConsumeMP(mpcost) ) + { + castSpell(players[HUDSHIELD_PLAYERNUM]->entity->getUID(), spell, false, false); + } + } + } + } + } + }*/ } } } +#define HUDADDITIONAL_PLAYERNUM my->skill[2] +void actHudAdditional2(Entity* my) +{ + my->flags[UNCLICKABLE] = true; + + auto& camera_shakex2 = cameravars[HUDADDITIONAL_PLAYERNUM].shakex2; + auto& camera_shakey2 = cameravars[HUDADDITIONAL_PLAYERNUM].shakey2; + auto& hudFlail = HUDFlail[HUDADDITIONAL_PLAYERNUM]; + + my->flags[INVISIBLE_DITHER] = false; + + // isn't active during intro/menu sequence + if ( intro == true ) + { + my->flags[INVISIBLE] = true; + hudFlail.needsInit = true; + return; + } + else + { + if ( multiplayer == CLIENT ) + { + if ( stats[HUDADDITIONAL_PLAYERNUM]->HP <= 0 ) + { + my->flags[INVISIBLE] = true; + my->flags[INVISIBLE_DITHER] = false; + hudFlail.needsInit = true; + return; + } + } + + // this entity only exists so long as the player exists + if ( players[HUDADDITIONAL_PLAYERNUM] == nullptr || players[HUDADDITIONAL_PLAYERNUM]->entity == nullptr || !players[HUDADDITIONAL_PLAYERNUM]->hud.weapon ) + { + list_RemoveNode(my->mynode); + hudFlail.needsInit = true; + return; + } + + if ( !(stats[HUDADDITIONAL_PLAYERNUM]->weapon && stats[HUDADDITIONAL_PLAYERNUM]->weapon->type == STEEL_FLAIL) ) + { + my->flags[INVISIBLE] = true; + my->flags[INVISIBLE_DITHER] = false; + hudFlail.needsInit = true; + return; + } + } + + my->flags[INVISIBLE] = players[HUDADDITIONAL_PLAYERNUM]->hud.weapon->flags[INVISIBLE]; + my->flags[INVISIBLE_DITHER] = players[HUDADDITIONAL_PLAYERNUM]->hud.weapon->flags[INVISIBLE_DITHER]; + + auto& weaponLimb = players[HUDADDITIONAL_PLAYERNUM]->hud.weapon; + if ( (my->flags[INVISIBLE] && !my->flags[INVISIBLE_DITHER]) || weaponLimb->skill[6] != 0 ) // HUDWEAPON_HIDEWEAPON + { + hudFlail.needsInit = true; + return; + } + + if ( hudFlail.needsInit ) + { + HUDFlail_t::reset(hudFlail); + hudFlail.needsInit = false; + } + + my->mistformGLRender = weaponLimb->mistformGLRender; + my->sprite = 1922; + //my->flags[OVERDRAW] = false; + my->x = weaponLimb->x; + my->y = weaponLimb->y; + my->z = weaponLimb->z; + my->focalx = 0; + my->focaly = 0; + my->focalz = 4; + + /*static ConsoleVariable cvar_bounce("/bounce", 1.f); + if ( keystatus[SDLK_F1] ) + { + keystatus[SDLK_F1] = 0; + bounce = *cvar_bounce * ((local_rng.rand() % 2) ? 1 : -1); + } + static ConsoleCommand ccmd_bounce_start("/bounce_start", "", + [](int argc, const char* argv[]) { + if ( argc >= 2 ) + { + real_t var = atoi(argv[1]); + bounce = var / 100.0; + } + });*/ + auto& bounce = hudFlail.bounce; + auto& prevYaw = hudFlail.prevYaw; + + if ( players[HUDADDITIONAL_PLAYERNUM]->entity ) + { + real_t newYaw = atan2(players[HUDADDITIONAL_PLAYERNUM]->entity->vel_y, players[HUDADDITIONAL_PLAYERNUM]->entity->vel_x); + newYaw = fmod(newYaw, 2 * PI); + prevYaw = fmod(prevYaw, 2 * PI); + int diff = static_cast((newYaw - prevYaw) * 180.0 / PI) % 360; + if ( diff < 0 ) + { + diff += 360; + } + if ( diff > 180 ) + { + diff -= 360; + } + // move dir to bounce effect + + real_t mult = 1.0; + { + int diff = static_cast((newYaw - fmod(players[HUDADDITIONAL_PLAYERNUM]->entity->yaw, 2 * PI)) * 180.0 / PI) % 360; + if ( diff < 0 ) + { + diff += 360; + } + if ( diff > 180 ) + { + diff -= 360; + } + if ( abs(diff) > 90 ) // backwards + { + mult *= -1; + } + } + + if ( diff > 0 ) + { + bounce += 0.05f * mult; + } + else if ( diff < 0 ) + { + bounce += -0.05f * mult; + } + prevYaw = newYaw; + } + + static ConsoleVariable cvar_anim_flail_damp("/anim_flail_damp", 2.f); + static ConsoleVariable cvar_anim_flail_mass("/anim_flail_mass", 5.f); + static ConsoleVariable cvar_anim_flail_spring("/anim_flail_spring", 5.f); + static ConsoleVariable cvar_anim_flail_mult("/anim_flail_mult", .1f); + // spring motion + { + real_t accel = (-*cvar_anim_flail_damp * bounce - *cvar_anim_flail_spring * hudFlail.roll) / *cvar_anim_flail_mass; + bounce += accel * *cvar_anim_flail_mult; + hudFlail.roll += bounce * *cvar_anim_flail_mult; + } + + auto& spin = hudFlail.spin; + auto& spin2 = hudFlail.spin2; + auto& rollSpin = hudFlail.rollSpin; + auto& spinState = hudFlail.spinState; + + if ( weaponLimb->skill[0] == 1 ) // HUDWEAPON_CHOP + { + spinState = 1; + spin += std::max(0.01, (1.0 - spin) / 20); + spin = std::min(1.0, spin); + + real_t prevRollSpin = rollSpin; + rollSpin += spin / 3; + + if ( weaponLimb->skill[0] == 1 ) + { + if ( fmod(prevRollSpin, 2 * PI) > PI && fmod(rollSpin, 2 * PI) < PI ) // crossing the 0.0 angle boundary after first swing + { + players[HUDADDITIONAL_PLAYERNUM]->entity->attack(MONSTER_POSE_FLAIL_SWING, 0, nullptr); + } + } + } + else + { + if ( spinState == 1 ) + { + rollSpin = 2 * PI - 0.01; // immediately jump to end of circle + if ( fmod(rollSpin, 2 * PI) >= PI ) + { + spinState = 2; + static ConsoleVariable cvar_anim_flail_atk_spin("/anim_flail_atk_spin", 2.5); + spin2 = 1.0; + spin = *cvar_anim_flail_atk_spin; + + if ( weaponLimb->skill[0] == 2 ) + { + bounce = 1.f; + } + else + { + bounce = -0.5f; + } + } + else + { + spin += std::max(0.01, (1.0 - spin) / 20); + spin = std::min(1.0, spin); + rollSpin += spin / 3; + } + } + + if ( spinState != 1 ) + { + spin -= std::max(0.01, (spin) / 20); + spin = std::max(0.0, spin); + if ( rollSpin > 2 * PI + 0.001 ) + { + rollSpin = fmod(rollSpin, 2 * PI); + } + rollSpin += std::max(0.01, (2 * PI - rollSpin) / 10); + rollSpin = std::min(2 * PI, rollSpin); + if ( rollSpin >= 2 * PI ) + { + spinState = 0; + } + } + } + + spin2 -= std::max(0.01, (spin2) / 20); + spin2 = std::max(0.0, spin2); + + hudFlail.pitch = -(4 * PI / 8) * spin; + + float basePitch = 0.15; + my->pitch = hudFlail.pitch - (1.0 - spin) * (PI * basePitch); + my->roll = hudFlail.roll + rollSpin; + my->yaw = hudFlail.yaw + sin(spin2 * PI / 2); + + // target focal x/y/z relative to weapon's position + real_t focalx = 1.5 + weaponLimb->focalx; + real_t focaly = 0.0 + weaponLimb->focaly; + real_t focalz = -0.9 + weaponLimb->focalz; + + // magic code to translate focals into pure coords (doesn't include focaly) + real_t xoffset = focalz * sin(weaponLimb->pitch + PI) * cos(weaponLimb->roll) * cos(weaponLimb->yaw); + real_t yoffset = focalz * sin(weaponLimb->pitch + PI) * cos(weaponLimb->roll) * sin(weaponLimb->yaw); + xoffset += focalz * sin(weaponLimb->roll + PI) * cos(weaponLimb->yaw + PI / 2); + yoffset += focalz * sin(weaponLimb->roll + PI) * sin(weaponLimb->yaw + PI / 2); + + xoffset += focalx * cos(weaponLimb->yaw) * sin(weaponLimb->pitch + PI / 2) + focaly * cos(weaponLimb->yaw + PI / 2); + yoffset += focalx * sin(weaponLimb->yaw) * sin(weaponLimb->pitch + PI / 2) + focaly * sin(weaponLimb->yaw + PI / 2); + + real_t zoffset = focalz * cos(weaponLimb->pitch) * cos(weaponLimb->roll); + zoffset += focalx * sin(weaponLimb->pitch); + my->x += xoffset; + my->y += yoffset; + my->z += zoffset; +} + void actHudAdditional(Entity* my) { bool spellbook = false; @@ -4096,6 +5443,24 @@ void actHudAdditional(Entity* my) return; } + my->mistformGLRender = 0.0; + if ( players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender > 0.05 ) + { + real_t modulus = fmod(players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender, 1.0); + if ( modulus >= 0.05 && modulus < 0.15 ) // force shield + { + my->mistformGLRender = 0.5; + } + else if ( modulus >= 0.15 && modulus < 0.25 ) // reflector shield + { + my->mistformGLRender = 0.6; + } + else + { + my->mistformGLRender = players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender; + } + } + if ( !spellbook ) { my->flags[INVISIBLE] = true; @@ -4161,7 +5526,7 @@ void actHudAdditional(Entity* my) return; } - if ( cast_animation[HUDSHIELD_PLAYERNUM].active ) + if ( cast_animation[HUDSHIELD_PLAYERNUM].hideShieldFromBasicCast() ) { my->flags[INVISIBLE] = true; my->flags[INVISIBLE_DITHER] = false; @@ -4299,6 +5664,38 @@ void actHudAdditional(Entity* my) { my->z -= -2 * .5; } + else if ( stats[HUDSHIELD_PLAYERNUM]->type == GREMLIN ) + { + my->z -= -1 * .5; + } + else if ( stats[HUDSHIELD_PLAYERNUM]->type == GNOME ) + { + my->z -= -2 * .5; + } + else if ( stats[HUDSHIELD_PLAYERNUM]->type == DRYAD ) + { + if ( players[HUDSHIELD_PLAYERNUM]->entity->z >= 1.5 ) + { + my->z -= -3.0 * .5; + } + else + { + my->z -= -2.0 * .5; + } + } + else if ( stats[HUDSHIELD_PLAYERNUM]->type == MYCONID ) + { + my->z -= -1.0 * .5; + } + + if ( stats[HUDSHIELD_PLAYERNUM]->getEffectActive(EFF_MINIMISE) ) + { + my->z += -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HUDSHIELD_PLAYERNUM]->getEffectActive(EFF_MINIMISE) & 0xF) * .5; + } + if ( stats[HUDSHIELD_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) ) + { + my->z -= -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HUDSHIELD_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) & 0xF) * .5; + } } void actHudArrowModel(Entity* my) @@ -4307,7 +5704,10 @@ void actHudArrowModel(Entity* my) bool crossbow = false; if ( stats[HUDSHIELD_PLAYERNUM]->weapon && (stats[HUDSHIELD_PLAYERNUM]->weapon->type == SHORTBOW + || stats[HUDSHIELD_PLAYERNUM]->weapon->type == BONE_SHORTBOW || stats[HUDSHIELD_PLAYERNUM]->weapon->type == LONGBOW + || stats[HUDSHIELD_PLAYERNUM]->weapon->type == BRANCH_BOW + || stats[HUDSHIELD_PLAYERNUM]->weapon->type == BRANCH_BOW_INFECTED || stats[HUDSHIELD_PLAYERNUM]->weapon->type == ARTIFACT_BOW || stats[HUDSHIELD_PLAYERNUM]->weapon->type == COMPOUND_BOW ) ) @@ -4315,7 +5715,9 @@ void actHudArrowModel(Entity* my) bow = true; } else if ( stats[HUDSHIELD_PLAYERNUM]->weapon - && (stats[HUDSHIELD_PLAYERNUM]->weapon->type == CROSSBOW || stats[HUDSHIELD_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW) ) + && (stats[HUDSHIELD_PLAYERNUM]->weapon->type == CROSSBOW + || stats[HUDSHIELD_PLAYERNUM]->weapon->type == HEAVY_CROSSBOW + || stats[HUDSHIELD_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW) ) { crossbow = true; } @@ -4349,7 +5751,27 @@ void actHudArrowModel(Entity* my) return; } - if ( (!crossbow && !bow) || cast_animation[HUDSHIELD_PLAYERNUM].active || cast_animation[HUDSHIELD_PLAYERNUM].active_spellbook ) + my->mistformGLRender = 0.0; + if ( players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender > 0.05 ) + { + real_t modulus = fmod(players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender, 1.0); + if ( modulus >= 0.05 && modulus < 0.15 ) // force shield + { + my->mistformGLRender = 0.5; + } + else if ( modulus >= 0.15 && modulus < 0.25 ) // reflector shield + { + my->mistformGLRender = 0.6; + } + else + { + my->mistformGLRender = players[HUDSHIELD_PLAYERNUM]->entity->mistformGLRender; + } + } + + if ( (!crossbow && !bow) + || cast_animation[HUDSHIELD_PLAYERNUM].hideShieldFromBasicCast() + || cast_animation[HUDSHIELD_PLAYERNUM].active_spellbook ) { my->flags[INVISIBLE] = true; return; @@ -4383,6 +5805,14 @@ void actHudArrowModel(Entity* my) return; } } + if ( stats[HUDSHIELD_PLAYERNUM]->weapon->type == BLACKIRON_CROSSBOW ) + { + if ( hudweapon->sprite != items[BLACKIRON_CROSSBOW].fpindex ) + { + my->flags[INVISIBLE] = true; + return; + } + } } else if ( hudweapon->skill[6] != 0 || hudweapon->skill[7] != RANGED_ANIM_FIRED ) // skill[6] is hiding weapon, skill[7] is shooting something @@ -4437,6 +5867,12 @@ void actHudArrowModel(Entity* my) case QUIVER_HUNTING: my->sprite = 941; break; + case QUIVER_BONE: + my->sprite = 2302; + break; + case QUIVER_BLACKIRON: + my->sprite = 2303; + break; default: break; } @@ -4467,6 +5903,12 @@ void actHudArrowModel(Entity* my) my->focaly += 0.25; my->focalz += -0.25; } + else if ( hudweapon->sprite == items[BLACKIRON_CROSSBOW].fpindex ) + { + my->focalx += 3.5; + my->focaly += 0.25; + my->focalz += -0.25; + } else if ( hudweapon->sprite == items[HEAVY_CROSSBOW].fpindex || hudweapon->sprite == items[HEAVY_CROSSBOW].fpindex - 1 ) { my->focalx += 4.5; diff --git a/src/actitem.cpp b/src/actitem.cpp index d2da9f33e..f77b3fe19 100644 --- a/src/actitem.cpp +++ b/src/actitem.cpp @@ -47,6 +47,321 @@ #define ITEM_SPLOOSHED my->skill[27] #define ITEM_WATERBOB my->fskill[2] +bool itemProcessReturnItemEffect(Entity* my, bool fallingIntoVoid) +{ + if ( Entity* returnToParent = uidToEntity(my->itemReturnUID) ) + { + int returnTime = std::max(10, std::max(getSpellDamageSecondaryFromID(SPELL_RETURN_ITEMS, returnToParent, nullptr, returnToParent, 0.0, false), + getSpellDamageFromID(SPELL_RETURN_ITEMS, returnToParent, nullptr, returnToParent, 0.0, false))); + if ( fallingIntoVoid || (my->ticks >= returnTime && returnToParent->behavior == &actPlayer) ) + { + int cost = std::max(1, getSpellEffectDurationSecondaryFromID(SPELL_RETURN_ITEMS, returnToParent, nullptr, returnToParent)); + if ( cost > 0 && !returnToParent->safeConsumeMP(cost) ) + { + Stat* returnStats = returnToParent->getStats(); + if ( returnStats && returnStats->MP > 0 ) + { + returnToParent->modMP(-returnStats->MP); + } + if ( spell_t* sustainSpell = returnToParent->getActiveMagicEffect(SPELL_RETURN_ITEMS) ) + { + sustainSpell->sustain = false; + } + playSoundEntity(returnToParent, 163, 128); + if ( Item* item = newItemFromEntity(my, true) ) + { + messagePlayerColor(returnToParent->skill[2], MESSAGE_COMBAT, makeColorRGB(255, 0, 0), Language::get(6813), item->getName()); + free(item); + } + my->itemReturnUID = 0; + } + else + { + int i = returnToParent->skill[2]; + Item* item2 = newItemFromEntity(my); + if ( item2 ) + { + int pickedUpCount = item2->count; + Item* item = itemPickup(i, item2); + if ( item ) + { + if ( players[i]->isLocalPlayer() ) + { + // item is the new inventory stack for server, free the picked up items + free(item2); + int oldcount = item->count; + item->count = pickedUpCount; + messagePlayer(i, MESSAGE_INTERACTION | MESSAGE_INVENTORY, Language::get(3746), item->getName()); + item->count = oldcount; + } + else + { + messagePlayer(i, MESSAGE_INTERACTION | MESSAGE_INVENTORY, Language::get(3746), item->getName()); + free(item); // item is the picked up items (item == item2) + } + + if ( returnToParent->behavior == &actPlayer ) + { + if ( auto spell = getSpellFromID(SPELL_RETURN_ITEMS) ) + { + players[returnToParent->skill[2]]->mechanics.sustainedSpellIncrementMP(cost, spell->skillID); + } + players[returnToParent->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_RETURN_ITEMS, 10.0, 1.0, nullptr); + } + + spawnMagicEffectParticles(my->x, my->y, my->z, 170); + my->removeLightField(); + list_RemoveNode(my->mynode); + return true; + } + } + } + } + } + return false; +} + +void onItemPickedUp(Entity& who, Uint32 itemUid) +{ + for ( int player = 0; player < MAXPLAYERS; ++player ) + { + if ( players[player]->mechanics.donationRevealedOnFloor == itemUid ) + { + if ( who.behavior == &actPlayer ) + { + messagePlayerColor(who.skill[2], MESSAGE_HINT, makeColorRGB(255, 255, 0), Language::get(6943)); // you discovered a gift + + for ( int player2 = 0; player2 < MAXPLAYERS; ++player2 ) // relay to other players + { + if ( player2 != who.skill[2] && !client_disconnected[player2] ) + { + messagePlayerColor(player2, MESSAGE_HINT, makeColorRGB(255, 255, 0), Language::get(6944), stats[who.skill[2]]->name); // an ally discovered a gift + } + } + } + else if ( who.behavior == &actMonster && who.monsterAllyIndex >= 0 && who.monsterAllyIndex < MAXPLAYERS && who.getStats() ) + { + std::string allyName = who.getStats()->name; + if ( allyName == "" ) + { + allyName = getMonsterLocalizedName(who.getStats()->type); + } + + messagePlayerColor(who.monsterAllyIndex, MESSAGE_HINT, makeColorRGB(255, 255, 0), Language::get(6944), allyName.c_str()); // your ally %s discovered a gift + + for ( int player2 = 0; player2 < MAXPLAYERS; ++player2 ) // relay to other players + { + if ( player2 != who.monsterAllyIndex && !client_disconnected[player2] ) + { + messagePlayerColor(player2, MESSAGE_HINT, makeColorRGB(255, 255, 0), Language::get(6945), stats[who.monsterAllyIndex]->name, allyName.c_str()); // %s's ally %s discovered a gift + } + } + } + + players[player]->mechanics.updateSustainedSpellEvent(SPELL_DONATION, 150.0, 1.0, nullptr); + break; + } + } +} + +bool entityWantsJewel(int tier, Entity& entity, Stat& stats, bool checkTypeOnly) +{ + int req = -1; + switch ( stats.type ) + { + case GNOME: + case GOBLIN: + req = 1; + break; + case HUMAN: + case GREMLIN: + case SUCCUBUS: + case GOATMAN: + req = 2; + break; + case KOBOLD: + case INSECTOID: + case DRYAD: + case MYCONID: + case BUGBEAR: + case INCUBUS: + req = 3; + break; + case VAMPIRE: + case SALAMANDER: + req = 4; + break; + //case TROLL, + //case AUTOMATON, + default: + break; + } + + if ( req < 0 ) + { + return false; + } + + if ( entity.behavior != &actMonster ) { return false; } + if ( !entity.monsterIsTargetable() || !entity.isMobile() ) { return false; } + if ( entity.isBossMonster() ) { return false; } + if ( entity.monsterAllyGetPlayerLeader() ) { return false; } + if ( stats.type == INCUBUS && !strncmp(stats.name, "inner demon", strlen("inner demon")) ) { return false; } + //if ( stats.leader_uid != 0 && uidToEntity(stats.leader_uid) ) { return false; } + + if ( checkTypeOnly ) + { + return req >= 0; + } + else if ( req >= 0 ) + { + if ( (std::max(1, tier) * 5) >= currentlevel || tier == EXCELLENT ) + { + return true; + } + //return tier >= req; + } + + return false; +} + +bool jewelItemRecruit(Entity* parent, Entity* entity, int itemStatus, const char** msg) +{ + if ( !(entity && parent) ) + { + return false; + } + + if ( parent->behavior != &actPlayer ) + { + return false; + } + + Stat* entitystats = entity->getStats(); + if ( !entitystats ) + { + return false; + } + + int allowedFollowers = std::min(8, std::max(4, 2 * (stats[parent->skill[2]]->getModifiedProficiency(PRO_LEADERSHIP) / 20))); + int numFollowers = 0; + for ( node_t* node = stats[parent->skill[2]]->FOLLOWERS.first; node; node = node->next ) + { + Entity* follower = nullptr; + if ( (Uint32*)node->element ) + { + follower = uidToEntity(*((Uint32*)node->element)); + } + if ( follower ) + { + Stat* followerStats = follower->getStats(); + if ( followerStats ) + { + if ( !(followerStats->type == SENTRYBOT || followerStats->type == GYROBOT + || followerStats->type == SPELLBOT || followerStats->type == DUMMYBOT) ) + { + ++numFollowers; + } + } + } + } + + if ( numFollowers >= allowedFollowers ) + { + if ( allowedFollowers >= 8 ) + { + if ( msg ) + { + *msg = Language::get(3482); + } + } + else + { + if ( msg ) + { + *msg = Language::get(3480); + } + } + return false; + } + else if ( forceFollower(*parent, *entity) ) + { + Entity* fx = createRadiusMagic(SPELL_FORGE_JEWEL, entity, entity->x, entity->y, 16, 2 * TICKS_PER_SECOND, entity); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 2410); + playSoundEntity(entity, 167, 64); + + if ( parent->behavior == &actPlayer ) + { + magicOnSpellCastEvent(parent, parent, entity, SPELL_FORGE_JEWEL, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + messagePlayerMonsterEvent(parent->skill[2], makeColorRGB(0, 255, 0), *entitystats, Language::get(6954), Language::get(6955), MSG_COMBAT); + Compendium_t::Events_t::eventUpdateMonster(parent->skill[2], Compendium_t::CPDM_RECRUITED, entity, 1); + + if ( stats[parent->skill[2]]->playerRace == RACE_GNOME && stats[parent->skill[2]]->stat_appearance == 0 ) + { + steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_MERCENARY_ARMY, STEAM_STAT_INT, 1); + } + + if ( itemStatus == DECREPIT ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_JEWEL_RECRUIT_DECREPIT, GEM_JEWEL, 1); + } + else if ( itemStatus == WORN ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_JEWEL_RECRUIT_WORN, GEM_JEWEL, 1); + } + else if ( itemStatus == SERVICABLE ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_JEWEL_RECRUIT_SERVICABLE, GEM_JEWEL, 1); + } + else if ( itemStatus == EXCELLENT ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_JEWEL_RECRUIT_EXCELLENT, GEM_JEWEL, 1); + } + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_RECRUITED, GEM_JEWEL, 1); + + if ( entitystats->type == HUMAN && entitystats->getAttribute("special_npc") == "merlin" ) + { + Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_MERLINS, "magicians guild", 1); + } + entity->monsterAllyIndex = parent->skill[2]; + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(entity, 42); // update monsterAllyIndex for clients. + } + } + + if ( entity->monsterTarget == parent->getUID() ) + { + entity->monsterReleaseAttackTarget(); + } + entity->setEffect(EFF_CONFUSED, false, 0, true); + + // change the color of the hit entity. + entity->flags[USERFLAG2] = true; + serverUpdateEntityFlag(entity, USERFLAG2); + if ( monsterChangesColorWhenAlly(entitystats) ) + { + int bodypart = 0; + for ( node_t* node = (entity)->children.first; node != nullptr; node = node->next ) + { + if ( bodypart >= LIMB_HUMANOID_TORSO ) + { + Entity* tmp = (Entity*)node->element; + if ( tmp ) + { + tmp->flags[USERFLAG2] = true; + //serverUpdateEntityFlag(tmp, USERFLAG2); + } + } + ++bodypart; + } + } + + return true; + } + return false; +} + void actItem(Entity* my) { Item* item; @@ -67,6 +382,25 @@ void actItem(Entity* my) my->new_z -= ITEM_WATERBOB; ITEM_WATERBOB = 0.0; + if ( my->sprite >= items[TOOL_DUCK].index && my->sprite < items[TOOL_DUCK].index + 4 ) + { + if ( multiplayer == CLIENT ) + { + my->flags[INVISIBLE] = true; + } + else + { + Item* item = newItemFromEntity(my, true); + if ( item ) + { + item->applyDuck(my->parent, my->x, my->y, nullptr, false); + } + free(item); + list_RemoveNode(my->mynode); + return; + } + } + if ( multiplayer == CLIENT ) { my->flags[NOUPDATE] = true; @@ -168,6 +502,20 @@ void actItem(Entity* my) my->flags[INVISIBLE] = false; } item = newItemFromEntity(my, true); + /*if ( currentlevel > 0 && my->ticks == 1 ) + { + if ( items[item->type].category != TOME_SPELL && items[item->type].category != SPELLBOOK ) + { + if ( items[item->type].level < 0 ) + { + if ( item->type != KEY_IRON && item->type != KEY_BRONZE && item->type != KEY_GOLD + && item->type != KEY_SILVER ) + { + printlog("%s", items[item->type].getIdentifiedName()); + } + } + } + }*/ my->sprite = itemModel(item); free(item); } @@ -182,6 +530,78 @@ void actItem(Entity* my) // pick up item if (multiplayer != CLIENT) { + if ( my->skill[10] == GEM_JEWEL ) + { + if ( Entity* parent = uidToEntity(my->parent) ) + { + if ( parent && parent->behavior == &actPlayer ) + { + int tier = my->skill[11]; + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + node_t* node; + for ( node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity && entity->behavior == &actMonster ) + { + if ( Stat* entitystats = entity->getStats() ) + { + auto hitProps = getParticleEmitterHitProps(my->getUID(), entity); + if ( hitProps->hits > 0 || (ticks - hitProps->tick) < TICKS_PER_SECOND ) + { + continue; + } + if ( entity->getUID() % 10 == ticks % 10 ) + { + hitProps->tick = ticks; + if ( entityWantsJewel(tier, *entity, *entitystats, true) ) + { + if ( entityDist(my, entity) < 16.0 ) + { + hitProps->hits++; + if ( entityWantsJewel(tier, *entity, *entitystats, false) ) + { + const char* msg = nullptr; + if ( jewelItemRecruit(parent, entity, tier, &msg) ) + { + my->clearMonsterInteract(); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else + { + if ( msg ) + { + auto hitProps = getParticleEmitterHitProps(my->getUID(), parent); + if ( hitProps && hitProps->hits == 0 ) + { + ++hitProps->hits; + messagePlayer(parent->isEntityPlayer(), MESSAGE_HINT, msg); + } + } + } + } + else + { + spawnFloatingSpriteMisc(134, entity->x + (-4 + local_rng.rand() % 9) + cos(entity->yaw) * 2, + entity->y + (-4 + local_rng.rand() % 9) + sin(entity->yaw) * 2, entity->z + local_rng.rand() % 4); + } + } + } + } + } + } + } + } + } + } + } + + Uint32 myUid = my->getUID(); if ( my->isInteractWithMonster() ) { Entity* monsterInteracting = uidToEntity(my->interactedByMonster); @@ -243,6 +663,7 @@ void actItem(Entity* my) copyOfItem = nullptr; if ( pickedUpItem && monsterInteracting->monsterAllyIndex >= 0 ) { + onItemPickedUp(*monsterInteracting, myUid); FollowerMenu[monsterInteracting->monsterAllyIndex].entityToInteractWith = nullptr; // in lieu of my->clearMonsterInteract, my might have been deleted. return; } @@ -252,6 +673,7 @@ void actItem(Entity* my) { if ( monsterInteracting->monsterConsumeFoodEntity(my, monsterInteracting->getStats()) && monsterInteracting->monsterAllyIndex >= 0 ) { + onItemPickedUp(*monsterInteracting, myUid); FollowerMenu[monsterInteracting->monsterAllyIndex].entityToInteractWith = nullptr; // in lieu of my->clearMonsterInteract, my might have been deleted. return; } @@ -260,6 +682,7 @@ void actItem(Entity* my) { if ( monsterInteracting->monsterAddNearbyItemToInventory(monsterInteracting->getStats(), 24, 9, my) && monsterInteracting->monsterAllyIndex >= 0 ) { + onItemPickedUp(*monsterInteracting, myUid); FollowerMenu[monsterInteracting->monsterAllyIndex].entityToInteractWith = nullptr; // in lieu of my->clearMonsterInteract, my might have been deleted. return; } @@ -270,6 +693,15 @@ void actItem(Entity* my) } my->clearMonsterInteract(); } + + if ( my->itemReturnUID != 0 ) + { + if ( itemProcessReturnItemEffect(my, false) ) + { + return; + } + } + for ( i = 0; i < MAXPLAYERS; i++) { if ( selectedEntity[i] == my || client_selected[i] == my ) @@ -324,6 +756,27 @@ void actItem(Entity* my) { steamAchievementClient(i, "BARONY_ACH_REPOSSESSION"); } + if ( my->itemGerminateResult == 1 ) + { + if ( item2->type == FOOD_NUT || item2->type == FOOD_SHROOM ) + { + if ( achievementObserver.checkUidIsFromPlayer(my->itemOriginalOwner) >= 0 ) + { + int owner = achievementObserver.checkUidIsFromPlayer(my->itemOriginalOwner); + achievementObserver.playerAchievements[owner].eatMe++; + } + } + } + if ( GenericGUI[i].isItemRation(item2->type) + && stats[i]->HUNGER <= getEntityHungerInterval(i, nullptr, stats[i], HUNGER_INTERVAL_HUNGRY) ) + { + if ( achievementObserver.checkUidIsFromPlayer(my->itemOriginalOwner) >= 0 + && achievementObserver.checkUidIsFromPlayer(my->itemOriginalOwner) != i ) + { + steamAchievementClient(i, "BARONY_ACH_SECOND_BREAKFAST"); + } + } + //messagePlayer(i, "old owner: %d", item2->ownerUid); if (item2) { @@ -341,6 +794,7 @@ void actItem(Entity* my) if ( salvaged ) { + onItemPickedUp(*players[i]->entity, myUid); free(item2); my->removeLightField(); list_RemoveNode(my->mynode); @@ -396,6 +850,7 @@ void actItem(Entity* my) } free(item); // item is the picked up items (item == item2) } + onItemPickedUp(*players[i]->entity, myUid); my->removeLightField(); list_RemoveNode(my->mynode); return; @@ -464,10 +919,78 @@ void actItem(Entity* my) my->light = addLight(my->x / 16, my->y / 16, "lootbag_white"); } break; + case 2409: // jewel + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, "jewel_yellow"); + } + break; default: break; } + bool levitating = false; + Entity* leader = nullptr; + if ( my->itemFollowUID != 0 ) + { + if ( multiplayer != CLIENT ) + { + if ( leader = uidToEntity(my->itemFollowUID) ) + { + Stat* leaderStats = leader->getStats(); + real_t dist = entityDist(leader, my); + + real_t maxDist = std::max(16, std::min(getSpellDamageSecondaryFromID(SPELL_ATTRACT_ITEMS, leader, nullptr, leader), + getSpellDamageFromID(SPELL_ATTRACT_ITEMS, leader, nullptr, leader))); + if ( dist > maxDist + 4.0 || (leaderStats && !leaderStats->getEffectActive(EFF_ATTRACT_ITEMS)) ) + { + my->itemFollowUID = 0; + serverUpdateEntitySkill(my, 30); + leader = nullptr; + } + + if ( leader ) + { + levitating = true; + const real_t followDist = 12.0; + if ( dist > 12.0 ) + { + real_t tangent = atan2(leader->y - my->y, leader->x - my->x); + my->vel_x = cos(tangent) * ((dist - followDist) / MAGICLIGHTBALL_DIVIDE_CONSTANT); + my->vel_y = sin(tangent) * ((dist - followDist) / MAGICLIGHTBALL_DIVIDE_CONSTANT); + my->vel_x = (my->vel_x < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_x : MAGIC_LIGHTBALL_SPEEDLIMIT; + my->vel_y = (my->vel_y < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_y : MAGIC_LIGHTBALL_SPEEDLIMIT; + } + my->flags[UPDATENEEDED] = true; + my->flags[NOUPDATE] = false; + } + } + } + else + { + levitating = true; + my->itemNotMoving = 0; + my->itemNotMovingClient = 0; + my->flags[UPDATENEEDED] = true; + my->flags[NOUPDATE] = false; + } + } + + if ( my->sprite == items[GEM_JEWEL].index ) + { + if ( my->ticks % 25 == 0 || my->ticks % 40 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 2410, 1.0, 1.0) ) + { + fx->vel_z = -0.2; + fx->yaw = (local_rng.rand() % 360) * PI / 180.0; + fx->x = my->x + 3.0 * cos(fx->yaw); + fx->y = my->y + 3.0 * sin(fx->yaw); + fx->focalz = 0.25; + } + } + } + if ( my->itemNotMoving ) { switch ( my->sprite ) @@ -541,7 +1064,26 @@ void actItem(Entity* my) my->flags[BURNING] = false; - if ( my->z < groundheight ) + if ( levitating ) + { + /*ITEM_VELZ += 0.04; + ITEM_VELZ = std::min(0.0, ITEM_VELZ); + my->z += ITEM_VELZ;*/ + my->z = my->itemLevitateStartZ * my->itemLevitate; + my->z = std::min(groundheight - 0.1, my->z); + my->z = std::max(my->z, -7.5); + my->vel_z = 0.0; + + real_t diff = std::max(0.025, my->itemLevitate / 10.0); + my->itemLevitate = std::max(0.0, my->itemLevitate - diff); + + my->yaw += PI / (TICKS_PER_SECOND * 10); + my->new_yaw = my->yaw; + ITEM_WATERBOB = sin(((ticks % (TICKS_PER_SECOND * 2)) / ((real_t)TICKS_PER_SECOND * 2.0)) * (2.0 * PI)) * 0.5; + my->z += ITEM_WATERBOB; + my->new_z = my->z; + } + else if ( my->z < groundheight ) { // fall // chakram and shuriken lie flat, needs to use sprites for client @@ -596,7 +1138,7 @@ void actItem(Entity* my) if ( my->x >= 0 && my->y >= 0 && my->x < map.width << 4 && my->y < map.height << 4 ) { const int tile = map.tiles[(int)(my->y / 16) * MAPLAYERS + (int)(my->x / 16) * MAPLAYERS * map.height]; - if ( tile || (my->sprite >= 610 && my->sprite <= 613) || (my->sprite >= 1206 && my->sprite <= 1209) ) + if ( tile || (my->sprite >= 610 && my->sprite <= 613) || (my->sprite >= 1206 && my->sprite <= 1210) ) { onground = true; if (!isArtifact && tile >= 64 && tile < 72) { // landing on lava @@ -617,6 +1159,7 @@ void actItem(Entity* my) if ( splash ) { playSoundEntity(my, 136, 64); + createWaterSplash(my->x, my->y, 30); } } } @@ -722,21 +1265,29 @@ void actItem(Entity* my) Compendium_t::Events_t::eventUpdateWorld(playerOwner, Compendium_t::CPDM_PITS_ITEMS_LOST, "pits", 1); if ( ITEM_TYPE >= 0 && ITEM_TYPE < NUMITEMS ) { - Compendium_t::Events_t::eventUpdateWorld(playerOwner, Compendium_t::CPDM_PITS_ITEMS_VALUE_LOST, "pits", items[ITEM_TYPE].value); + Compendium_t::Events_t::eventUpdateWorld(playerOwner, Compendium_t::CPDM_PITS_ITEMS_VALUE_LOST, "pits", items[ITEM_TYPE].gold_value); } } } + if ( my->itemReturnUID != 0 ) + { + if ( itemProcessReturnItemEffect(my, true) ) + { + return; + } + } } if ( ITEM_TYPE == ARTIFACT_MACE && my->parent != 0 ) { steamAchievementEntity(uidToEntity(my->parent), "BARONY_ACH_STFU"); } + my->removeLightField(); list_RemoveNode(my->mynode); return; } // don't perform unneeded computations on items that have basically no velocity - if (!overWater && onground && + if (!overWater && onground && !levitating && my->z > groundheight - .0001 && my->z < groundheight + .0001 && fabs(ITEM_VELX) < 0.02 && fabs(ITEM_VELY) < 0.02) { @@ -769,6 +1320,15 @@ void actItem(Entity* my) { double result = clipMove(&my->x, &my->y, ITEM_VELX, ITEM_VELY, my); my->yaw += result * .05; + + if ( multiplayer != CLIENT ) + { + if ( levitating && leader && leader->behavior == &actPlayer ) + { + players[leader->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_ATTRACT_ITEMS, result, 0.025, nullptr); + } + } + if ( result != sqrt( ITEM_VELX * ITEM_VELX + ITEM_VELY * ITEM_VELY ) ) { if ( !hit.side ) @@ -786,6 +1346,54 @@ void actItem(Entity* my) } } } + ITEM_VELX = ITEM_VELX * .925; ITEM_VELY = ITEM_VELY * .925; } + +static Uint32 lastAttractTick = 0; +void Entity::attractItem(Entity& itemEntity) +{ + if ( itemEntity.itemFollowUID != getUID() && itemEntity.z < 16.0 && itemEntity.ticks > TICKS_PER_SECOND ) + { + if ( lastAttractTick != ::ticks ) + { + spawnMagicEffectParticles(itemEntity.x, itemEntity.y, itemEntity.z, 170); + lastAttractTick = ::ticks; + } + itemEntity.itemFollowUID = getUID(); + + itemEntity.flags[USERFLAG1] = false; + itemEntity.itemNotMoving = 0; + itemEntity.itemNotMovingClient = 0; + itemEntity.z = std::max(itemEntity.z - 0.1, 0.0); + itemEntity.vel_z = -0.75; + itemEntity.itemLevitate = 1.0; + itemEntity.itemLevitateStartZ = itemEntity.z; + itemEntity.flags[UPDATENEEDED] = true; + itemEntity.flags[NOUPDATE] = false; + if ( multiplayer == SERVER ) + { + for ( int c = 1; c < MAXPLAYERS; c++ ) + { + if ( client_disconnected[c] || players[c]->isLocalPlayer() ) + { + continue; + } + strcpy((char*)net_packet->data, "ATTI"); + SDLNet_Write32(itemEntity.getUID(), &net_packet->data[4]); + SDLNet_Write16((Sint16)(itemEntity.x * 32), &net_packet->data[8]); + SDLNet_Write16((Sint16)(itemEntity.y * 32), &net_packet->data[10]); + SDLNet_Write16((Sint16)(itemEntity.z * 32), &net_packet->data[12]); + SDLNet_Write16((Sint16)(itemEntity.vel_x * 32), &net_packet->data[14]); + SDLNet_Write16((Sint16)(itemEntity.vel_y * 32), &net_packet->data[16]); + SDLNet_Write16((Sint16)(itemEntity.vel_z * 32), &net_packet->data[18]); + SDLNet_Write32(getUID(), & net_packet->data[20]); + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 24; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } + } + } +} \ No newline at end of file diff --git a/src/actladder.cpp b/src/actladder.cpp index 39d5bcfd4..72006c51c 100644 --- a/src/actladder.cpp +++ b/src/actladder.cpp @@ -286,7 +286,7 @@ void actPortal(Entity* my) && !entity->monsterAllyGetPlayerLeader() ) { Stat* stats = entity->getStats(); - if ( stats && MonsterData_t::nameMatchesSpecialNPCName(*stats, "bram kindly") ) + if ( stats && (stats->getAttribute("special_npc") == "bram kindly") ) { bossAlive = true; } @@ -616,6 +616,10 @@ void actWinningPortal(Entity* my) switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::ClassicEndingHuman); break; case RACE_AUTOMATON: @@ -630,6 +634,7 @@ void actWinningPortal(Entity* my) case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::ClassicEndingEvil); break; } @@ -644,6 +649,10 @@ void actWinningPortal(Entity* my) switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::ClassicBaphometEndingHuman); break; case RACE_AUTOMATON: @@ -658,6 +667,7 @@ void actWinningPortal(Entity* my) case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::ClassicBaphometEndingEvil); break; } @@ -845,6 +855,10 @@ void Entity::actExpansionEndGamePortal() switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::EndingHuman); break; case RACE_AUTOMATON: @@ -859,6 +873,7 @@ void Entity::actExpansionEndGamePortal() case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::EndingEvil); break; } @@ -1055,6 +1070,7 @@ void Entity::actMidGamePortal() switch ( race ) { // herx midpoint default: case RACE_HUMAN: + case RACE_GNOME: MainMenu::beginFade(MainMenu::FadeDestination::HerxMidpointHuman); break; case RACE_AUTOMATON: @@ -1063,12 +1079,16 @@ void Entity::actMidGamePortal() case RACE_GOATMAN: case RACE_GOBLIN: case RACE_INSECTOID: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::HerxMidpointBeast); break; case RACE_SKELETON: case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::HerxMidpointEvil); break; } @@ -1077,6 +1097,7 @@ void Entity::actMidGamePortal() switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: MainMenu::beginFade(MainMenu::FadeDestination::BaphometMidpointHuman); break; case RACE_AUTOMATON: @@ -1085,12 +1106,16 @@ void Entity::actMidGamePortal() case RACE_GOATMAN: case RACE_GOBLIN: case RACE_INSECTOID: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::BaphometMidpointBeast); break; case RACE_SKELETON: case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::BaphometMidpointEvil); break; } diff --git a/src/actmagictrap.cpp b/src/actmagictrap.cpp index 7c89057f7..a35907e20 100644 --- a/src/actmagictrap.cpp +++ b/src/actmagictrap.cpp @@ -44,33 +44,44 @@ void actMagicTrapCeiling(Entity* my) void Entity::actMagicTrapCeiling() { -#ifdef USE_FMOD - if ( spellTrapAmbience == 0 ) + if ( actTrapSabotaged == 0 ) { +#ifdef USE_FMOD + if ( spellTrapAmbience == 0 ) + { + spellTrapAmbience--; + stopEntitySound(); + entity_sound = playSoundEntityLocal(this, 149, 16); + } + if ( entity_sound ) + { + bool playing = false; + entity_sound->isPlaying(&playing); + if ( !playing ) + { + entity_sound = nullptr; + } + } +#else spellTrapAmbience--; - stopEntitySound(); - entity_sound = playSoundEntityLocal(this, 149, 16); - } - if ( entity_sound ) - { - bool playing = false; - entity_sound->isPlaying(&playing); - if ( !playing ) + if ( spellTrapAmbience <= 0 ) { - entity_sound = nullptr; + spellTrapAmbience = TICKS_PER_SECOND * 30; + playSoundEntityLocal(this, 149, 16); } +#endif } -#else - spellTrapAmbience--; - if ( spellTrapAmbience <= 0 ) + else { - spellTrapAmbience = TICKS_PER_SECOND * 30; - playSoundEntityLocal(this, 149, 16); - } +#ifdef USE_FMOD + stopEntitySound(); #endif + return; + } if ( multiplayer == CLIENT ) { + flags[NOUPDATE] = true; return; } if ( circuit_status != CIRCUIT_ON ) @@ -166,7 +177,7 @@ void Entity::actMagicTrapCeiling() entity->x = x; entity->y = y; entity->z = ceilingModel->z - 2; - double missile_speed = 4 * ((double)(((spellElement_t*)(getSpellFromID(spellTrapType)->elements.first->element))->mana) / ((spellElement_t*)(getSpellFromID(spellTrapType)->elements.first->element))->overload_multiplier); + double missile_speed = 4.0; entity->vel_x = 0.0; entity->vel_y = 0.0; entity->vel_z = 0.5 * (missile_speed); @@ -239,8 +250,15 @@ void actMagicTrap(Entity* my) return; } + if ( my->actTrapSabotaged > 0 ) + { + my->removeLightField(); + return; + } + if ( multiplayer == CLIENT ) { + my->flags[NOUPDATE] = true; return; } @@ -284,7 +302,7 @@ void actMagicTrap(Entity* my) entity->y = my->y + y; entity->z = my->z; entity->yaw = oldir * (PI / 2.f); - double missile_speed = 4 * ((double)(((spellElement_t*)(getSpellFromID(MAGICTRAP_SPELL)->elements.first->element))->mana) / ((spellElement_t*)(getSpellFromID(MAGICTRAP_SPELL)->elements.first->element))->overload_multiplier); + double missile_speed = 4.0; entity->vel_x = cos(entity->yaw) * (missile_speed); entity->vel_y = sin(entity->yaw) * (missile_speed); } @@ -605,7 +623,7 @@ void daedalusShrineInteract(Entity* my, Entity* touched) if ( touched && touched->getStats() ) { Stat* myStats = touched->getStats(); - if ( myStats->EFFECTS[EFF_SLOW] ) + if ( myStats->getEffectActive(EFF_SLOW) ) { touched->setEffect(EFF_SLOW, false, 0, true); } @@ -742,7 +760,7 @@ void Entity::actDaedalusShrine() if ( touched && touched->getStats() ) { Stat* myStats = touched->getStats(); - if ( myStats->EFFECTS[EFF_SLOW] ) + if ( myStats->getEffectActive(EFF_SLOW) ) { touched->setEffect(EFF_SLOW, false, 0, true); } @@ -1072,7 +1090,10 @@ void Entity::actAssistShrine() } else if ( multiplayer == SINGLE || playernum == 0 ) { - GenericGUI[playernum].assistShrineGUI.closeAssistShrine(); + if ( playernum >= 0 && playernum < MAXPLAYERS ) + { + GenericGUI[playernum].assistShrineGUI.closeAssistShrine(); + } } } } diff --git a/src/actmonster.cpp b/src/actmonster.cpp index 83cb5c78b..02c039090 100644 --- a/src/actmonster.cpp +++ b/src/actmonster.cpp @@ -30,104 +30,134 @@ #include "ui/MainMenu.hpp" #include "menu.hpp" -float limbs[NUMMONSTERS][20][3]; +float limbs[NUMMONSTERS][30][3]; // determines which monsters fight which bool swornenemies[NUMMONSTERS][NUMMONSTERS] = { -// N H R G S T B S G S S I C G D S M L M D S K S G I V S C I G A L L S S G D B -// O U A O L R A P H K C M R N E U I I I E H O C O N A H O N O U I I N P Y U U -// T M T B I O T I O E O P A O M C M C N V P B A L C M A C S A T F I T L R M G - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // NOTHING - { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1 }, // HUMAN - { 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0 }, // RAT - { 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0 }, // GOBLIN - { 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1 }, // SLIME - { 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0 }, // TROLL - { 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1 }, // BAT_SMALL - { 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0 }, // SPIDER - { 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // GHOUL - { 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // SKELETON - { 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0 }, // SCORPION - { 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // IMP - { 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0 }, // CRAB - { 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0 }, // GNOME - { 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1 }, // DEMON - { 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // SUCCUBUS - { 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0 }, // MIMIC - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // LICH - { 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0 }, // MINOTAUR - { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // DEVIL - { 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SHOPKEEPER - { 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0 }, // KOBOLD - { 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0 }, // SCARAB - { 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0 }, // CRYSTALGOLEM - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // INCUBUS - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // VAMPIRE - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // SHADOW - { 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // COCKATRICE - { 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // INSECTOID - { 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // GOATMAN - { 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // AUTOMATON - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // LICH_ICE - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 }, // LICH_FIRE - { 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // SENTRYBOT - { 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // SPELLBOT - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GYROBOT - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DUMMYBOT - { 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0 } // BUGBEAR -// N H R G S T B S G S S I C G D S M L M D S K S G I V S C I G A L L S S G D B -// O U A O L R A P H K C M R N E U I I I E H O C O N A H O N O U I I N P Y U U -// T M T B I O T I O E O P A O M C M C N V P B A L C M A C S A T F I T L R M G +// N H R G S T B S G S S I C G D S M L M D S K S G I V S C I G A L L S S G D B D M S G R M A U U U U U U U U +// O U A O L R A P H K C M R N E U I I I E H O C O N A H O N O U I I N P Y U U D M S G E I D N N N N N N N N +// T M T B I O T I O E O P A O M C M C N V P B A L C M A C S A T F I T L R M G D M S G V N O 1 2 3 4 5 6 7 8 + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // NOTHING + { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // HUMAN + { 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // RAT + { 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GOBLIN + { 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SLIME + { 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // TROLL + { 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // BAT_SMALL + { 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SPIDER + { 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GHOUL + { 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SKELETON + { 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SCORPION + { 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // IMP + { 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // CRAB + { 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GNOME + { 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DEMON + { 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SUCCUBUS + { 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MIMIC + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // LICH + { 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MINOTAUR + { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DEVIL + { 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SHOPKEEPER + { 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // KOBOLD + { 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SCARAB + { 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // CRYSTALGOLEM + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // INCUBUS + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // VAMPIRE + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SHADOW + { 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // COCKATRICE + { 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // INSECTOID + { 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GOATMAN + { 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // AUTOMATON + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // LICH_ICE + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // LICH_FIRE + { 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SENTRYBOT + { 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SPELLBOT + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GYROBOT + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DUMMYBOT + { 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // BUGBEAR + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DRYAD + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MYCONID + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SALAMANDER + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GREMLIN + { 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // REVENANT_SKULL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MINIMIMIC + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MONSTER_ADORCISED_WEAPON + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // FLAME_ELEMENTAL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // HOLOGRAM + { 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MOTH_SMALL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // EARTH_ELEMENTAL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DUCK_SMALL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MONSTER_UNUSED_6 + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MONSTER_UNUSED_7 + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // MONSTER_UNUSED_8 +// N H R G S T B S G S S I C G D S M L M D S K S G I V S C I G A L L S S G D B D M S G R M A F H M E D U U U +// O U A O L R A P H K C M R N E U I I I E H O C O N A H O N O U I I N P Y U U D M S G E I D L O T R U N N N +// T M T B I O T I O E O P A O M C M C N V P B A L C M A C S A T F I T L R M G D M S G V N O M L H F C 6 7 8 }; // determines which monsters come to the aid of other monsters bool monsterally[NUMMONSTERS][NUMMONSTERS] = { -// N H R G S T B S G S S I C G D S M L M D S K S G I V S C I G A L L S S G D B -// O U A O L R A P H K C M R N E U I I I E H O C O N A H O N O U I I N P Y U U -// T M T B I O T I O E O P A O M C M C N V P B A L C M A C S A T F I T L R M G - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // NOTHING - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // HUMAN - { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // RAT - { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, // GOBLIN - { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, // SLIME - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // TROLL - { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // BAT_SMALL - { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SPIDER - { 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0 }, // GHOUL - { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // SKELETON - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SCORPION - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // IMP - { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // CRAB - { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GNOME - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // DEMON - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // SUCCUBUS - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MIMIC - { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0 }, // LICH - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MINOTAUR - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0 }, // DEVIL - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SHOPKEEPER - { 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // KOBOLD - { 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SCARAB - { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // CRYSTALGOLEM - { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // INCUBUS - { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 }, // VAMPIRE - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 }, // SHADOW - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // COCKATRICE - { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // INSECTOID - { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0 }, // GOATMAN - { 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, // AUTOMATON - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 }, // LICH_ICE - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 }, // LICH_FIRE - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // SENTRYBOT - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // SPELLBOT - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // GYROBOT - { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0 }, // DUMMYBOT - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // BUGBEAR -// N H R G S T B S G S S I C G D S M L M D S K S G I V S C I G A L L S S G D B -// O U A O L R A P H K C M R N E U I I I E H O C O N A H O N O U I I N P Y U U -// T M T B I O T I O E O P A O M C M C N V P B A L C M A C S A T F I T L R M G +// N H R G S T B S G S S I C G D S M L M D S K S G I V S C I G A L L S S G D B D M S G R M A U U U U U U U U +// O U A O L R A P H K C M R N E U I I I E H O C O N A H O N O U I I N P Y U U D M S G E I D N N N N N N N N +// T M T B I O T I O E O P A O M C M C N V P B A L C M A C S A T F I T L R M G D M S G V N O 1 2 3 4 5 6 7 8 + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // NOTHING + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // HUMAN + { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // RAT + { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GOBLIN + { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SLIME + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // TROLL + { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // BAT_SMALL + { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SPIDER + { 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GHOUL + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SKELETON + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SCORPION + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // IMP + { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // CRAB + { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GNOME + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DEMON + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SUCCUBUS + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MIMIC + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // LICH + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MINOTAUR + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DEVIL + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SHOPKEEPER + { 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // KOBOLD + { 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SCARAB + { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // CRYSTALGOLEM + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // INCUBUS + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // VAMPIRE + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SHADOW + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // COCKATRICE + { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // INSECTOID + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GOATMAN + { 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // AUTOMATON + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // LICH_ICE + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // LICH_FIRE + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SENTRYBOT + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SPELLBOT + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GYROBOT + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DUMMYBOT + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // BUGBEAR + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DRYAD + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MYCONID + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // SALAMANDER + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // GREMLIN + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // REVENANT_SKULL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MINIMIMIC + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MONSTER_ADORCISED_WEAPON + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, // FLAME_ELEMENTAL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // HOLOGRAM + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 }, // MOTH_SMALL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 }, // EARTH_ELEMENTAL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // DUCK_SMALL + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MONSTER_UNUSED_6 + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // MONSTER_UNUSED_7 + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // MONSTER_UNUSED_8 +// N H R G S T B S G S S I C G D S M L M D S K S G I V S C I G A L L S S G D B D M S G R M A F H M E D U U U +// O U A O L R A P H K C M R N E U I I I E H O C O N A H O N O U I I N P Y U U D M S G E I D L O T R U N N N +// T M T B I O T I O E O P A O M C M C N V P B A L C M A C S A T F I T L R M G D M S G V N O M L H F C 6 7 8 }; // monster sight ranges @@ -170,18 +200,49 @@ double sightranges[NUMMONSTERS] = 192, // SPELLBOT 256, // GYROBOT 32, // DUMMYBOT - 128 // BUGBEAR + 128, // BUGBEAR + 256, // DRYAD + 256, // MYCONID + 256, // SALAMANDER + 256, // GREMLIN + 192, // REVENANT_SKULL + 256, // MINIMIMIC + 64, // ADORCISED_WEAPON + 128, // FLAME_ELEMENTAL + 256, // HOLOGRAM + 128, // MOTH_SMALL + 128, // EARTH_ELEMENTAL + 256, // DUCK_SMALL + 256, // MONSTER_UNUSED_6 + 256, // MONSTER_UNUSED_7 + 256 // MONSTER_UNUSED_8 }; int monsterGlobalAnimationMultiplier = 10; int monsterGlobalAttackTimeMultiplier = 1; -std::string getMonsterLocalizedName(Monster creature) +std::string getMonsterLocalizedName(Monster creature, Stat* optionalStats) { if ( creature == BUGBEAR ) { return Language::get(6256); } + else if ( creature == MOTH_SMALL && optionalStats && MonsterData_t::nameMatchesSpecialNPCName(*optionalStats, "fire sprite") ) + { + return optionalStats->name; + } + else if ( creature >= DRYAD && creature <= GREMLIN ) + { + return Language::get(6358 + (creature - DRYAD)); + } + else if ( creature >= REVENANT_SKULL && creature <= MINIMIMIC ) + { + return Language::get(6568 + (creature - REVENANT_SKULL)); + } + else if ( creature >= MONSTER_ADORCISED_WEAPON && creature <= MONSTER_UNUSED_8 ) + { + return Language::get(6580 + (creature - MONSTER_ADORCISED_WEAPON)); + } else if ( creature < KOBOLD ) { if (creature == SPIDER && ((!intro && arachnophobia_filter) || (intro && MainMenu::arachnophobia_filter)) ) { @@ -203,6 +264,18 @@ std::string getMonsterLocalizedPlural(Monster creature) { return Language::get(6257); } + else if ( creature >= DRYAD && creature <= GREMLIN ) + { + return Language::get(6362 + (creature - DRYAD)); + } + else if ( creature >= REVENANT_SKULL && creature <= MINIMIMIC ) + { + return Language::get(6570 + (creature - REVENANT_SKULL)); + } + else if ( creature >= MONSTER_ADORCISED_WEAPON && creature <= MONSTER_UNUSED_8 ) + { + return Language::get(6589 + (creature - MONSTER_ADORCISED_WEAPON)); + } if ( creature < KOBOLD ) { if (creature == SPIDER && ((!intro && arachnophobia_filter) || (intro && MainMenu::arachnophobia_filter))) { @@ -223,6 +296,18 @@ std::string getMonsterLocalizedInjury(Monster creature) { return Language::get(6258); } + else if ( creature >= DRYAD && creature <= GREMLIN ) + { + return Language::get(6366 + (creature - DRYAD)); + } + else if ( creature >= REVENANT_SKULL && creature <= MINIMIMIC ) + { + return Language::get(6572 + (creature - REVENANT_SKULL)); + } + else if ( creature >= MONSTER_ADORCISED_WEAPON && creature <= MONSTER_UNUSED_8 ) + { + return Language::get(6598 + (creature - MONSTER_ADORCISED_WEAPON)); + } if ( creature < KOBOLD ) { if (creature == SPIDER && ((!intro && arachnophobia_filter) || (intro && MainMenu::arachnophobia_filter)) ) { @@ -330,11 +415,11 @@ bool ShopkeeperPlayerHostility_t::isPlayerEnemy(const int player) bool ShopkeeperPlayerHostility_t::playerRaceCheckHostility(const int player, const Monster type) const { if ( player < 0 || player >= MAXPLAYERS ) { return false; } - if ( type != HUMAN && type != AUTOMATON ) + if ( type != HUMAN && type != AUTOMATON && type != DRYAD && type != MYCONID && type != SALAMANDER && type != GNOME ) { if ( stats[player] && stats[player]->mask && stats[player]->mask->type == MONOCLE ) { - if ( !stats[player]->EFFECTS[EFF_SHAPESHIFT] && !(players[player]->entity && players[player]->entity->isInvisible()) ) + if ( !stats[player]->getEffectActive(EFF_SHAPESHIFT) && !(players[player]->entity && players[player]->entity->isInvisible()) ) { return true; } @@ -497,6 +582,10 @@ void ShopkeeperPlayerHostility_t::setWantedLevel(ShopkeeperPlayerHostility_t::Pl } real_t monsterVisionRange = sightranges[SHOPKEEPER]; + if ( players[i]->mechanics.ensemblePlaying >= 0 ) + { + monsterVisionRange = std::max(monsterVisionRange, 5 * 16.0); + } int light = players[i]->entity->entityLightAfterReductions(*stats[i], shopkeeper); double targetdist = sqrt(pow(shopkeeper->x - players[i]->entity->x, 2) + pow(shopkeeper->y - players[i]->entity->y, 2)); @@ -629,6 +718,37 @@ void ShopkeeperPlayerHostility_t::serverSendClientUpdate(const bool force) ShopkeeperPlayerHostility_t ShopkeeperPlayerHostility; +bool Entity::monsterAlertBeforeHit(Entity* attacker) +{ + if ( !attacker ) + { + return false; + } + + bool alertTarget = true; + if ( attacker->behavior == &actMonster && attacker->monsterAllyIndex != -1 ) + { + if ( behavior == &actMonster && monsterAllyIndex != -1 ) + { + // if a player ally + hit another ally, don't aggro back + alertTarget = false; + } + } + + if ( Stat* myStats = getStats() ) + { + if ( myStats->getEffectActive(EFF_NUMBING_BOLT) ) + { + alertTarget = false; + spawnFloatingSpriteMisc(134, x + (-4 + local_rng.rand() % 9) + cos(yaw) * 2, + y + (-4 + local_rng.rand() % 9) + sin(yaw) * 2, z + local_rng.rand() % 4); + setEffect(EFF_NUMBING_BOLT, false, 0, false); + } + } + + return alertTarget; +} + void Entity::updateEntityOnHit(Entity* attacker, bool alertTarget) { if ( !attacker ) return; @@ -636,6 +756,38 @@ void Entity::updateEntityOnHit(Entity* attacker, bool alertTarget) if ( Stat* myStats = getStats() ) { + if ( Uint8 effectStrength = myStats->getEffectActive(EFF_PENANCE) ) + { + if ( attacker ) + { + if ( attacker->behavior == &actPlayer || attacker->monsterAllyGetPlayerLeader() ) + { + if ( effectStrength >= 1 && effectStrength < 1 + MAXPLAYERS ) + { + setEffect(EFF_PENANCE, false, 0, true); + } + } + } + } + if ( myStats->getEffectActive(EFF_SEEK_CREATURE) ) + { + setEffect(EFF_SEEK_CREATURE, false, 0, true); + } + if ( Uint8 effectStrength = myStats->getEffectActive(EFF_COMMAND) ) + { + if ( attacker && (attacker->behavior == &actPlayer || attacker->monsterAllyGetPlayerLeader()) ) + { + if ( effectStrength >= 1 && effectStrength < 1 + MAXPLAYERS ) + { + players[effectStrength - 1]->mechanics.targetsRefuseCompel.insert(this->getUID()); + } + setEffect(EFF_COMMAND, false, 0, true); + } + } + /*if ( myStats->getEffectActive(EFF_HEALING_WORD) ) + { + setEffect(EFF_HEALING_WORD, false, 0, false); + }*/ if ( myStats->type == SHOPKEEPER ) { if ( alertTarget ) @@ -959,6 +1111,11 @@ void MonsterAllyFormation_t::updateFormation(Uint32 leaderUid, Uint32 monsterUpd } } + if ( leader->behavior == &actPlayer && players[leader->skill[2]]->ghost.isActive() && players[leader->skill[2]]->ghost.isSpiritGhost() ) + { + leader = players[leader->skill[2]]->ghost.my; + } + size_t formationIndex = 0; for ( auto& unit : leaderUnits.meleeUnits ) { @@ -1004,7 +1161,10 @@ void MonsterAllyFormation_t::updateFormation(Uint32 leaderUid, Uint32 monsterUpd ally->x = x; ally->y = y; double tangent = atan2(leader->y - ally->y, leader->x - ally->x); + bool oldPassable = leader->flags[PASSABLE]; // hack to linetrace ghosts + leader->flags[PASSABLE] = false; lineTraceTarget(ally, ally->x, ally->y, tangent, 128, 0, false, leader); + leader->flags[PASSABLE] = oldPassable; if ( hit.entity == leader ) { found = true; @@ -1062,7 +1222,10 @@ void MonsterAllyFormation_t::updateFormation(Uint32 leaderUid, Uint32 monsterUpd ally->x = x; ally->y = y; double tangent = atan2(leader->y - ally->y, leader->x - ally->x); + bool oldPassable = leader->flags[PASSABLE]; // hack to linetrace ghosts + leader->flags[PASSABLE] = false; lineTraceTarget(ally, ally->x, ally->y, tangent, 128, 0, false, leader); + leader->flags[PASSABLE] = oldPassable; if ( hit.entity == leader ) { found = true; @@ -1109,6 +1272,15 @@ Entity* summonMonster(Monster creature, long x, long y, bool forceLocation) // small poof auto poof = spawnPoof(entity->x, entity->y, 4, 0.5); } + else if ( creature == MOTH_SMALL || creature == FLAME_ELEMENTAL || creature == DUCK_SMALL ) + { + // small poof + auto poof = spawnPoof(entity->x, entity->y, -6, 0.5); + } + else if ( creature == EARTH_ELEMENTAL ) + { + // no poof + } else { (void)spawnPoof(entity->x, entity->y, 0, 1.0); } @@ -1175,6 +1347,10 @@ Entity* summonMonsterNoSmoke(Monster creature, long x, long y, bool forceLocatio { myStats->setAttribute("slime_type", "terrain_spawn_override"); } + if ( myStats->type == EARTH_ELEMENTAL ) + { + myStats->setAttribute("earth_elemental_spawn", "terrain_spawn_override"); + } } // Find a free tile next to the source and then spawn it there. @@ -1245,6 +1421,10 @@ Entity* summonMonsterNoSmoke(Monster creature, long x, long y, bool forceLocatio { myStats->attributes.erase("slime_type"); } + if ( myStats->type == EARTH_ELEMENTAL ) + { + myStats->attributes.erase("earth_elemental_spawn"); + } } nummonsters++; @@ -1378,6 +1558,18 @@ int getMonsterInteractGreeting(Stat& myStats) { return 6259; } + else if ( myStats.type >= REVENANT_SKULL && myStats.type <= MINIMIMIC ) + { + return 6574 + (myStats.type - REVENANT_SKULL); + } + else if ( myStats.type >= MONSTER_ADORCISED_WEAPON && myStats.type <= MONSTER_UNUSED_8 ) + { + return (6607 + (myStats.type - MONSTER_ADORCISED_WEAPON)); + } + else if ( myStats.type >= DRYAD && myStats.type <= GREMLIN ) + { + return 6370 + (myStats.type - DRYAD); + } else if ( myStats.type < BUGBEAR ) { return 4262 + myStats.type; @@ -1467,6 +1659,11 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], return false; } + if ( race == INCUBUS && myStats->getAttribute("special_npc") == "johann" ) + { + return false; + } + bool canAlly = false; bool roseEvent = false; if ( skillCapstoneUnlocked(monsterclicked, PRO_LEADERSHIP) ) @@ -1518,7 +1715,7 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], } else if ( stats[monsterclicked]->type == VAMPIRE ) { - if ( race == VAMPIRE && !MonsterData_t::nameMatchesSpecialNPCName(*myStats, "bram kindly") ) + if ( race == VAMPIRE && !(myStats->getAttribute("special_npc") == "bram kindly") ) { canAlly = true; } @@ -1529,7 +1726,7 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], { canAlly = true; } - else if ( race == HUMAN && (myStats->EFFECTS[EFF_DRUNK] || myStats->EFFECTS[EFF_CONFUSED]) + else if ( race == HUMAN && (myStats->getEffectActive(EFF_DRUNK) || myStats->getEffectActive(EFF_CONFUSED)) && stats[monsterclicked]->type != INCUBUS ) { canAlly = true; @@ -1537,9 +1734,9 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], { steamAchievementClient(monsterclicked, "BARONY_ACH_TEMPTRESS"); } - if ( myStats->EFFECTS[EFF_CONFUSED] ) + if ( myStats->getEffectActive(EFF_CONFUSED) ) { - my->setEffect(EFF_CONFUSED, false, 0, false); + my->setEffect(EFF_CONFUSED, false, 0, true); } } } @@ -1592,6 +1789,34 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], canAlly = true; } } + else if ( stats[monsterclicked]->type == DRYAD ) + { + if ( race == DRYAD ) + { + canAlly = true; + } + } + else if ( stats[monsterclicked]->type == MYCONID ) + { + if ( race == MYCONID ) + { + canAlly = true; + } + } + else if ( stats[monsterclicked]->type == SALAMANDER ) + { + if ( race == SALAMANDER ) + { + canAlly = true; + } + } + else if ( stats[monsterclicked]->type == GREMLIN ) + { + if ( race == GREMLIN ) + { + canAlly = true; + } + } else if ( stats[monsterclicked]->type == CREATURE_IMP ) { if ( race == CREATURE_IMP && !(!strncmp(map.name, "Boss", 4) || !strncmp(map.name, "Hell Boss", 9)) ) @@ -1599,7 +1824,9 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], canAlly = true; // non-boss imps } } - if ( myStats->monsterForceAllegiance == Stat::MONSTER_FORCE_PLAYER_RECRUITABLE ) + if ( myStats->monsterForceAllegiance == Stat::MONSTER_FORCE_PLAYER_RECRUITABLE + || (myStats->getEffectActive(EFF_PENANCE) >= 1 + && myStats->getEffectActive(EFF_PENANCE) < 1 + MAXPLAYERS) ) { canAlly = true; } @@ -1623,7 +1850,7 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], bool tryAlly = my->checkFriend(players[monsterclicked]->entity); if ( stats[monsterclicked]->type == SUCCUBUS ) { - if ( race == HUMAN && (myStats->EFFECTS[EFF_DRUNK] || myStats->EFFECTS[EFF_CONFUSED]) ) + if ( race == HUMAN && (myStats->getEffectActive(EFF_DRUNK) || myStats->getEffectActive(EFF_CONFUSED)) ) { tryAlly = true; } @@ -1690,7 +1917,7 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], } else if ( stats[monsterclicked]->type == VAMPIRE ) { - if ( race == VAMPIRE && !MonsterData_t::nameMatchesSpecialNPCName(*myStats, "bram kindly") ) + if ( race == VAMPIRE && !(myStats->getAttribute("special_npc") == "bram kindly") ) { canAlly = true; } @@ -1701,7 +1928,7 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], { canAlly = true; } - else if ( race == HUMAN && (myStats->EFFECTS[EFF_DRUNK] || myStats->EFFECTS[EFF_CONFUSED]) + else if ( race == HUMAN && (myStats->getEffectActive(EFF_DRUNK) || myStats->getEffectActive(EFF_CONFUSED)) && stats[monsterclicked]->type != INCUBUS ) { canAlly = true; @@ -1709,9 +1936,9 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], { steamAchievementClient(monsterclicked, "BARONY_ACH_TEMPTRESS"); } - if ( myStats->EFFECTS[EFF_CONFUSED] ) + if ( myStats->getEffectActive(EFF_CONFUSED) ) { - my->setEffect(EFF_CONFUSED, false, 0, false); + my->setEffect(EFF_CONFUSED, false, 0, true); } } } @@ -1764,6 +1991,34 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], canAlly = true; } } + else if ( stats[monsterclicked]->type == DRYAD ) + { + if ( race == DRYAD ) + { + canAlly = true; + } + } + else if ( stats[monsterclicked]->type == MYCONID ) + { + if ( race == MYCONID ) + { + canAlly = true; + } + } + else if ( stats[monsterclicked]->type == SALAMANDER ) + { + if ( race == SALAMANDER ) + { + canAlly = true; + } + } + else if ( stats[monsterclicked]->type == GREMLIN ) + { + if ( race == GREMLIN ) + { + canAlly = true; + } + } else if ( stats[monsterclicked]->type == CREATURE_IMP ) { if ( race == CREATURE_IMP && !(!strncmp(map.name, "Boss", 4) || !strncmp(map.name, "Hell Boss", 9)) ) @@ -1775,6 +2030,12 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], { canAlly = true; } + if ( myStats->monsterForceAllegiance == Stat::MONSTER_FORCE_PLAYER_RECRUITABLE + || (myStats->getEffectActive(EFF_PENANCE) >= 1 + && myStats->getEffectActive(EFF_PENANCE) < 1 + MAXPLAYERS) ) + { + canAlly = true; + } if ( stats[monsterclicked]->mask && stats[monsterclicked]->mask->type == MASK_MOUTH_ROSE && players[monsterclicked]->entity->effectShapeshift == NOTHING ) { @@ -1900,6 +2161,10 @@ bool makeFollower(int monsterclicked, bool ringconflict, char namesays[64], my->monsterState = MONSTER_STATE_WAIT; // be ready to follow myStats->leader_uid = players[monsterclicked]->entity->getUID(); my->monsterAllyIndex = monsterclicked; + if ( myStats->monsterForceAllegiance == Stat::MONSTER_FORCE_PLAYER_ENEMY ) + { + myStats->monsterForceAllegiance = Stat::MONSTER_FORCE_ALLEGIANCE_NONE; + } if ( multiplayer == SERVER ) { serverUpdateEntitySkill(my, 42); // update monsterAllyIndex for clients. @@ -2288,11 +2553,13 @@ void sentrybotPickSpotNoise(Entity* my, Stat* myStats) { doSpecialNoise = true; } + break; case 1: if ( my->ticks % 60 >= 20 && my->ticks % 60 < 40 ) { doSpecialNoise = true; } + break; case 2: if ( my->ticks % 60 >= 40 ) { @@ -2354,6 +2621,21 @@ void monsterAnimate(Entity* my, Stat* myStats, double dist) case MIMIC: mimicAnimate(my, myStats, dist); break; case BAT_SMALL: batAnimate(my, myStats, dist); break; case BUGBEAR: bugbearMoveBodyparts(my, myStats, dist); break; + case DRYAD: monsterDMoveBodyparts(my, myStats, dist); break; + case MYCONID: monsterMMoveBodyparts(my, myStats, dist); break; + case SALAMANDER: monsterSMoveBodyparts(my, myStats, dist); break; + case GREMLIN: monsterGMoveBodyparts(my, myStats, dist); break; + case REVENANT_SKULL: revenantSkullAnimate(my, myStats, dist); break; + case MINIMIMIC: mimicAnimate(my, myStats, dist); break; + case MONSTER_ADORCISED_WEAPON: revenantSkullAnimate(my, myStats, dist); break; + case FLAME_ELEMENTAL: revenantSkullAnimate(my, myStats, dist); break; + case HOLOGRAM: hologramAnimate(my, myStats, dist); break; + case MOTH_SMALL: mothAnimate(my, myStats, dist); break; + case EARTH_ELEMENTAL: earthElementalAnimate(my, myStats, dist); break; + case DUCK_SMALL: duckAnimate(my, myStats, dist); break; + case MONSTER_UNUSED_6: break; + case MONSTER_UNUSED_7: break; + case MONSTER_UNUSED_8: break; default: break; } @@ -2371,7 +2653,7 @@ void actMonster(Entity* my) return; } - int x, y, c, i; + int x, y; double dist, dist2; list_t* path; node_t* node, *node2; @@ -2449,6 +2731,21 @@ void actMonster(Entity* my) case MIMIC: initMimic(my, nullptr); break; case BAT_SMALL: initBat(my, nullptr); break; case BUGBEAR: initBugbear(my, nullptr); break; + case DRYAD: initMonsterD(my, nullptr); break; + case MYCONID: initMonsterM(my, nullptr); break; + case SALAMANDER: initMonsterS(my, nullptr); break; + case GREMLIN: initMonsterG(my, nullptr); break; + case REVENANT_SKULL: initRevenantSkull(my, nullptr); break; + case MINIMIMIC: initMiniMimic(my, nullptr); break; + case MONSTER_ADORCISED_WEAPON: initAdorcisedWeapon(my, nullptr); break; + case FLAME_ELEMENTAL: initFlameElemental(my, nullptr); break; + case HOLOGRAM: initHologram(my, nullptr); break; + case MOTH_SMALL: initMoth(my, nullptr); break; + case EARTH_ELEMENTAL: initEarthElemental(my, nullptr); break; + case DUCK_SMALL: initDuck(my, nullptr); break; + case MONSTER_UNUSED_6: break; + case MONSTER_UNUSED_7: break; + case MONSTER_UNUSED_8: break; default: printlog("Unknown monster, can't init!"); break; } } @@ -2544,6 +2841,21 @@ void actMonster(Entity* my) case MIMIC: initMimic(my, myStats); break; case BAT_SMALL: initBat(my, myStats); break; case BUGBEAR: initBugbear(my, myStats); break; + case DRYAD: initMonsterD(my, myStats); break; + case MYCONID: initMonsterM(my, myStats); break; + case SALAMANDER: initMonsterS(my, myStats); break; + case GREMLIN: initMonsterG(my, myStats); break; + case REVENANT_SKULL: initRevenantSkull(my, myStats); break; + case MONSTER_ADORCISED_WEAPON: initAdorcisedWeapon(my, myStats); break; + case FLAME_ELEMENTAL: initFlameElemental(my, myStats); break; + case HOLOGRAM: initHologram(my, myStats); break; + case MOTH_SMALL: initMoth(my, myStats); break; + case EARTH_ELEMENTAL: initEarthElemental(my, myStats); break; + case DUCK_SMALL: initDuck(my, myStats); break; + case MONSTER_UNUSED_6: break; + case MONSTER_UNUSED_7: break; + case MONSTER_UNUSED_8: break; + case MINIMIMIC: initMiniMimic(my, myStats); break; default: break; //This should never be reached. } } @@ -2563,6 +2875,10 @@ void actMonster(Entity* my) } my->monsterLookTime = local_rng.rand() % 120; my->monsterMoveTime = local_rng.rand() % 10; + if ( my->monsterCanTradeWith(-1) ) + { + my->createPathBoundariesNPC(); + } MONSTER_SOUND = NULL; if ( MONSTER_NUMBER == -1 ) { @@ -2929,7 +3245,7 @@ void actMonster(Entity* my) } if ( myStats->type == LICH_FIRE ) { - if ( !myStats->EFFECTS[EFF_VAMPIRICAURA] ) + if ( !myStats->getEffectActive(EFF_VAMPIRICAURA) ) { if ( (lichAlly && lichAlly->monsterState != MONSTER_STATE_LICH_CASTSPELLS) || my->monsterLichAllyStatus == LICH_ALLY_DEAD @@ -3308,6 +3624,63 @@ void actMonster(Entity* my) && my->monsterState != MONSTER_STATE_LICHFIRE_DIE && my->monsterState != MONSTER_STATE_LICHICE_DIE ) { + Uint8 cursedFlesh = myStats->getEffectActive(EFF_CURSE_FLESH); + if ( cursedFlesh & (1 << 7) ) + { + int player = -1; + if ( (cursedFlesh & 0x7F) > 0 && (cursedFlesh & 0x7F) < MAXPLAYERS + 1 ) + { + player = (cursedFlesh & 0x7F) - 1; + } + if ( Entity* monster = spellEffectPolymorph(my, player >= 0 ? players[player]->entity : nullptr, true, 0, SKELETON) ) + { + // old entity was removed + if ( Stat* monsterStats = monster->getStats() ) + { + monster->setEffect(EFF_STUNNED, true, 20, false); + + Entity* commanderEntity = player >= 0 ? players[player]->entity : nullptr; + + int duration = getSpellEffectDurationSecondaryFromID(SPELL_REVENANT_CURSE, commanderEntity, nullptr, commanderEntity); + monsterStats->setAttribute("revenant_skeleton", std::to_string(duration)); + monsterStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; + strcpy(monsterStats->name, Language::get(6807)); + real_t ratio = getSpellDamageFromID(SPELL_REVENANT_CURSE, commanderEntity, nullptr, commanderEntity) / 100.0; + real_t maxratio = getSpellDamageSecondaryFromID(SPELL_REVENANT_CURSE, commanderEntity, nullptr, commanderEntity) / 100.0; + ratio = std::min(ratio, maxratio); + + monsterStats->MP *= ratio; + monsterStats->MAXHP *= ratio; + monsterStats->MAXMP *= ratio; + monsterStats->STR *= ratio; + monsterStats->DEX *= ratio; + monsterStats->CON *= ratio; + monsterStats->INT *= ratio; + monsterStats->PER *= ratio; + monsterStats->LVL *= ratio; + monsterStats->LVL = std::max(1, monsterStats->LVL); + monsterStats->GOLD *= ratio; + + monsterStats->HP = monsterStats->MAXHP * ratio; + monsterStats->OLDHP = monsterStats->HP; + + if ( player >= 0 ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6808), Language::get(6807)); + } + + playSoundEntity(monster, 167, 128); + createParticleDropRising(monster, 2354, 1.f); + serverSpawnMiscParticles(monster, PARTICLE_EFFECT_RISING_DROP, 2354); + } + return; + } + else + { + cursedFlesh &= ~(1 << 7); // revert to revenant skull + } + } + //TODO: Refactor die function. // drop all equipment entity = dropItemMonster(myStats->helmet, my, myStats); @@ -3400,13 +3773,13 @@ void actMonster(Entity* my) { nextnode = node->next; Item* item = (Item*)node->element; - for ( c = item->count; c > 0; c-- ) + for ( int c = item->count; c > 0; c-- ) { bool wasDroppable = item->isDroppable; if ( myStats->type == SHOPKEEPER ) { auto& rng = my->entity_rng ? *my->entity_rng : local_rng; - if ( rng.rand() % 2 ) + if ( rng.rand() % 2 && !items[item->type].hasAttribute("UNVOIDABLE") ) { item->isDroppable = false; // sometimes don't drop inventory } @@ -3442,6 +3815,7 @@ void actMonster(Entity* my) } } } + bool wasQuiver = itemTypeIsQuiver(item->type); entity = dropItemMonster(item, my, myStats); // returns nullptr on "undroppables" if ( entity ) @@ -3461,7 +3835,7 @@ void actMonster(Entity* my) // broadcast my player allies about my death int playerFollower = MAXPLAYERS; - for (c = 0; c < MAXPLAYERS; c++) + for ( int c = 0; c < MAXPLAYERS; c++) { if (players[c] && players[c]->entity) { @@ -3505,7 +3879,19 @@ void actMonster(Entity* my) } bool skipObituary = false; - if ( my->monsterAllySummonRank != 0 && myStats->MP > 0 ) + if ( myStats->getAttribute("skip_obituary") != "" ) + { + skipObituary = true; + } + else if ( my->monsterAllySummonRank != 0 && myStats->MP > 0 ) + { + skipObituary = true; + } + else if ( myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "" ) + { + skipObituary = true; + } + else if ( myStats->type == HOLOGRAM ) { skipObituary = true; } @@ -3551,6 +3937,14 @@ void actMonster(Entity* my) } #endif myStats = my->getStats(); + + real_t deathLocationX = my->x; + real_t deathLocationY = my->y; + if ( cursedFlesh > 0 ) + { + my->flags[PASSABLE] = true; + } + switch ( myStats->type ) { case HUMAN: @@ -3600,9 +3994,9 @@ void actMonster(Entity* my) MONSTER_ATTACKTIME = 0; serverUpdateEntitySkill(my, 8); serverUpdateEntitySkill(my, 9); - for ( c = 0; c < NUMEFFECTS; ++c ) + for ( int c = 0; c < NUMEFFECTS; ++c ) { - myStats->EFFECTS[c] = false; + myStats->clearEffect(c); myStats->EFFECTS_TIMERS[c] = 0; } break; @@ -3622,9 +4016,9 @@ void actMonster(Entity* my) serverUpdateEntitySkill(my, 8); serverUpdateEntitySkill(my, 9); serverUpdateEntitySkill(my, 10); - for ( c = 0; c < NUMEFFECTS; ++c ) + for ( int c = 0; c < NUMEFFECTS; ++c ) { - myStats->EFFECTS[c] = false; + myStats->clearEffect(c); myStats->EFFECTS_TIMERS[c] = 0; } break; @@ -3667,9 +4061,9 @@ void actMonster(Entity* my) serverUpdateEntitySkill(my, 8); serverUpdateEntitySkill(my, 9); serverUpdateEntitySkill(my, 0); - for ( c = 0; c < NUMEFFECTS; ++c ) + for ( int c = 0; c < NUMEFFECTS; ++c ) { - myStats->EFFECTS[c] = false; + myStats->clearEffect(c); myStats->EFFECTS_TIMERS[c] = 0; } break; @@ -3682,9 +4076,9 @@ void actMonster(Entity* my) serverUpdateEntitySkill(my, 8); serverUpdateEntitySkill(my, 9); serverUpdateEntitySkill(my, 0); - for ( c = 0; c < NUMEFFECTS; ++c ) + for ( int c = 0; c < NUMEFFECTS; ++c ) { - myStats->EFFECTS[c] = false; + myStats->clearEffect(c); myStats->EFFECTS_TIMERS[c] = 0; } break; @@ -3707,9 +4101,117 @@ void actMonster(Entity* my) case BUGBEAR: bugbearDie(my); break; + case DRYAD: + monsterDDie(my); + break; + case MYCONID: + monsterMDie(my); + break; + case SALAMANDER: + monsterSDie(my); + break; + case GREMLIN: + monsterGDie(my); + break; + case REVENANT_SKULL: + revenantSkullDie(my); + break; + case MINIMIMIC: + miniMimicDie(my); + break; + case MONSTER_ADORCISED_WEAPON: + adorcisedWeaponDie(my); + break; + case FLAME_ELEMENTAL: + flameElementalDie(my); + break; + case HOLOGRAM: + hologramDie(my); + break; + case MOTH_SMALL: + mothDie(my); + break; + case EARTH_ELEMENTAL: + earthElementalDie(my); + break; + case DUCK_SMALL: + duckDie(my); + break; + case MONSTER_UNUSED_6: + break; + case MONSTER_UNUSED_7: + break; + case MONSTER_UNUSED_8: + break; default: break; //This should never be reached. } + + if ( cursedFlesh > 0 ) + { + real_t x = floor(deathLocationX / 16) * 16 + 8.0; + real_t y = floor(deathLocationY / 16) * 16 + 8.0; + + + Entity* monster = summonMonster(REVENANT_SKULL, x, y, false); + if ( monster ) + { + if ( Stat* monsterStats = monster->getStats() ) + { + monster->setEffect(EFF_STUNNED, true, 20, false); + int player = -1; + if ( cursedFlesh > 0 && cursedFlesh < MAXPLAYERS + 1 ) + { + player = cursedFlesh - 1; + } + + Entity* commanderEntity = player >= 0 ? players[player]->entity : nullptr; + + int duration = getSpellEffectDurationSecondaryFromID(SPELL_CURSE_FLESH, commanderEntity, nullptr, commanderEntity); + monsterStats->setAttribute("revenant_skull", std::to_string(duration)); + monsterStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; + int lvl = getSpellDamageFromID(SPELL_CURSE_FLESH, commanderEntity, nullptr, commanderEntity); + int maxlvl = getSpellDamageSecondaryFromID(SPELL_CURSE_FLESH, commanderEntity, nullptr, commanderEntity); + lvl = std::min(lvl, maxlvl); + monsterStats->LVL = lvl; + if ( player >= 0 && players[player]->entity && forceFollower(*players[player]->entity, *monster) ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6808), getMonsterLocalizedName(monsterStats->type).c_str()); + monster->monsterAllyIndex = player; + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. + } + + // change the color of the hit entity. + Compendium_t::Events_t::eventUpdateMonster(player, Compendium_t::CPDM_RECRUITED, monster, 1); + monster->flags[USERFLAG2] = true; + serverUpdateEntityFlag(monster, USERFLAG2); + if ( monsterChangesColorWhenAlly(monsterStats) ) + { + int bodypart = 0; + for ( node_t* node = monster->children.first; node != nullptr; node = node->next ) + { + if ( bodypart >= LIMB_HUMANOID_TORSO ) + { + Entity* tmp = (Entity*)node->element; + if ( tmp ) + { + tmp->flags[USERFLAG2] = true; + //serverUpdateEntityFlag(tmp, USERFLAG2); + } + } + ++bodypart; + } + } + } + + playSoundEntity(monster, 167, 128); + createParticleDropRising(monster, 2354, 1.f); + serverSpawnMiscParticles(monster, PARTICLE_EFFECT_RISING_DROP, 2354); + } + } + } return; } @@ -3840,51 +4342,7 @@ void actMonster(Entity* my) } // calculate weight - Sint32 weight = 0; - if ( myStats->helmet != NULL ) - { - weight += myStats->helmet->getWeight(); - } - if ( myStats->breastplate != NULL ) - { - weight += myStats->breastplate->getWeight(); - } - if ( myStats->gloves != NULL ) - { - weight += myStats->gloves->getWeight(); - } - if ( myStats->shoes != NULL ) - { - weight += myStats->shoes->getWeight(); - } - if ( myStats->shield != NULL ) - { - weight += myStats->shield->getWeight(); - } - if ( myStats->weapon != NULL ) - { - weight += myStats->weapon->getWeight(); - } - if ( myStats->cloak != NULL ) - { - weight += myStats->cloak->getWeight(); - } - if ( myStats->amulet != NULL ) - { - weight += myStats->amulet->getWeight(); - } - if ( myStats->ring != NULL ) - { - weight += myStats->ring->getWeight(); - } - if ( myStats->mask != NULL ) - { - weight += myStats->mask->getWeight(); - } - weight += myStats->getGoldWeight(); - weight /= 2; // on monsters weight shouldn't matter so much - double weightratio = (1000 + my->getSTR() * 100 - weight) / (double)(1000 + my->getSTR() * 100); - weightratio = fmin(fmax(0, weightratio), 1); + real_t weightratio = my->monsterGetWeightRatio(); // determine if I have a ranged weapon or not hasrangedweapon = my->hasRangedWeapon(); @@ -3943,6 +4401,21 @@ void actMonster(Entity* my) case DUMMYBOT: case MIMIC: case BUGBEAR: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case REVENANT_SKULL: + case MONSTER_ADORCISED_WEAPON: + case MINIMIMIC: + case COCKATRICE: + case HOLOGRAM: + case MOTH_SMALL: + case EARTH_ELEMENTAL: + case DUCK_SMALL: + case MONSTER_UNUSED_6: + case MONSTER_UNUSED_7: + case MONSTER_UNUSED_8: handleinvisible = false; break; default: @@ -3951,7 +4424,7 @@ void actMonster(Entity* my) if ( handleinvisible ) { //TODO: Should this use isInvisible()? - if ( myStats->EFFECTS[EFF_INVISIBLE] ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; for ( node = list_Node(&my->children, 2); node != NULL; node = node->next ) @@ -3989,7 +4462,7 @@ void actMonster(Entity* my) snprintf(namesays, 63, Language::get(1302), myStats->name); } int monsterclicked = -1; - for (i = 0; i < MAXPLAYERS; i++) + for ( int i = 0; i < MAXPLAYERS; i++) { if ( selectedEntity[i] == my || client_selected[i] == my ) { @@ -4013,12 +4486,12 @@ void actMonster(Entity* my) if ( my->isInertMimic() ) { // wake up - if ( myStats->EFFECTS[EFF_MIMIC_LOCKED] ) + if ( myStats->getEffectActive(EFF_MIMIC_LOCKED) ) { messagePlayer(monsterclicked, MESSAGE_INTERACTION, Language::get(462)); playSoundEntity(my, 152, 64); } - else if ( my->disturbMimic(players[monsterclicked]->entity, false, false) ) + else if ( my->disturbMimic(players[monsterclicked]->entity, false, true) ) { messagePlayer(monsterclicked, MESSAGE_INTERACTION, Language::get(6081)); } @@ -4034,7 +4507,7 @@ void actMonster(Entity* my) } else { - if (my->monsterTarget == players[monsterclicked]->entity->getUID() && my->monsterState != 4) + if (my->monsterTarget == players[monsterclicked]->entity->getUID() && my->monsterState != MONSTER_STATE_TALK) { // angry at the player, "En Guarde!" for ( int c = 0; c < MAXPLAYERS; ++c ) @@ -4116,7 +4589,7 @@ void actMonster(Entity* my) else { // handle followers/trading - if ( myStats->type != SHOPKEEPER ) + if ( myStats->type != SHOPKEEPER && !my->monsterCanTradeWith(-1) ) { if ( myStats->MISC_FLAGS[STAT_FLAG_NPC] == 0 ) { @@ -4276,7 +4749,7 @@ void actMonster(Entity* my) // shopkeepers start trading startTradingServer(my, monsterclicked); if ( stats[monsterclicked] && stats[monsterclicked]->type == HUMAN && stats[monsterclicked]->stat_appearance == 0 - && stats[monsterclicked]->playerRace == RACE_AUTOMATON ) + && stats[monsterclicked]->playerRace == RACE_AUTOMATON && myStats->type == SHOPKEEPER ) { achievementObserver.updatePlayerAchievement(monsterclicked, AchievementObserver::Achievement::BARONY_ACH_REAL_BOY, AchievementObserver::AchievementEvent::REAL_BOY_SHOP); @@ -4344,7 +4817,15 @@ void actMonster(Entity* my) } else { - my->modHP(-9999); + my->setHP(0); + } + } + + if ( myStats->getEffectActive(EFF_SEEK_CREATURE) ) + { + if ( !(my->monsterState == MONSTER_STATE_HUNT || my->monsterState == MONSTER_STATE_PATH) ) + { + my->setEffect(EFF_SEEK_CREATURE, false, 0, false); } } @@ -4407,7 +4888,7 @@ void actMonster(Entity* my) } // being bumped by someone friendly - std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) { list_t* currentList = *it; @@ -4418,7 +4899,19 @@ void actMonster(Entity* my) { continue; } - if ( entity->behavior != &actMonster && entity->behavior != &actPlayer && entity->behavior != &actDoorFrame ) + + if ( entity->behavior == &actChest && myStats->type == EARTH_ELEMENTAL ) + { + // check for spawning on chest + } + else if ( entity->behavior != &actMonster && entity->behavior != &actPlayer && entity->behavior != &actDoorFrame + && !(entity->behavior == &actColliderDecoration + && ((entity->flags[PASSABLE] && entity->colliderHasCollision != 0) + || (entity->isColliderPathableMonster(myStats->type))) )) + { + continue; + } + if ( entity->sprite == 1822 ) /* fire sprite */ { continue; } @@ -4435,23 +4928,49 @@ void actMonster(Entity* my) my->sizey = sizey; if ( entityInside && entity->getRace() != GYROBOT ) { - if ( entity->behavior != &actDoorFrame ) + if ( entity->behavior == &actColliderDecoration ) { double tangent = atan2(my->y - entity->y, my->x - entity->x); MONSTER_VELX = cos(tangent) * .1; MONSTER_VELY = sin(tangent) * .1; } - else if ( entity->behavior == &actDoorFrame && - entity->flags[INVISIBLE] ) + else if ( entity->behavior != &actDoorFrame ) { - int mapx = (int)floor(entity->x / 16); - int mapy = (int)floor(entity->y / 16); - if ( entity->yaw >= -0.1 && entity->yaw <= 0.1 ) + bool skip = false; + if ( myStats->type == MONSTER_ADORCISED_WEAPON || entity->getRace() == MONSTER_ADORCISED_WEAPON ) { - // east/west doorway - if ( my->y < floor(entity->y / 16) * 16 + 8 ) + if ( myStats->getEffectActive(EFF_KNOCKBACK) ) { - bool slide = true; + skip = true; + } + else if ( entity->getStats() && entity->getStats()->getEffectActive(EFF_KNOCKBACK) ) + { + skip = true; + } + } + if ( (myStats->type == EARTH_ELEMENTAL && (my->flags[PASSABLE] || myStats->getEffectActive(EFF_KNOCKBACK)) ) + || (entity->flags[PASSABLE] && entity->getRace() == EARTH_ELEMENTAL) ) + { + skip = true; + } + if ( !skip ) + { + double tangent = atan2(my->y - entity->y, my->x - entity->x); + MONSTER_VELX = cos(tangent) * .1; + MONSTER_VELY = sin(tangent) * .1; + } + } + else if ( entity->behavior == &actDoorFrame && + entity->flags[INVISIBLE] ) + { + int mapx = (int)floor(entity->x / 16); + int mapy = (int)floor(entity->y / 16); + if ( entity->yaw >= -0.1 && entity->yaw <= 0.1 ) + { + // east/west doorway + if ( my->y < floor(entity->y / 16) * 16 + 8 ) + { + bool slide = true; if ( mapy - 1 > 0 ) { int index = (mapy - 1) * MAPLAYERS + (mapx) * MAPLAYERS * map.height; @@ -4600,7 +5119,7 @@ void actMonster(Entity* my) --my->monsterAllySpecialCooldown; } - if ( myStats->type == AUTOMATON ) + if ( myStats->type == AUTOMATON && !my->monsterCanTradeWith(-1) ) { my->automatonRecycleItem(); } @@ -4613,23 +5132,33 @@ void actMonster(Entity* my) } } - if ( myStats->EFFECTS[EFF_PACIFY] || myStats->EFFECTS[EFF_FEAR] ) + if ( myStats->getEffectActive(EFF_PACIFY) || myStats->getEffectActive(EFF_FEAR) ) { my->monsterHitTime = HITRATE / 2; // stop this incrementing to HITRATE but leave monster ready to strike shortly after. } if ( my->monsterDefend != MONSTER_DEFEND_NONE ) { - if ( my->monsterState != MONSTER_STATE_ATTACK - || myStats->shield == nullptr ) + if ( myStats->type == EARTH_ELEMENTAL ) { - myStats->defending = false; - my->monsterDefend = 0; - serverUpdateEntitySkill(my, 47); + if ( my->monsterAttack == 0 ) + { + myStats->defending = true; + } } - else if ( my->monsterAttack == 0 ) + else { - myStats->defending = true; + if ( my->monsterState != MONSTER_STATE_ATTACK + || myStats->shield == nullptr ) + { + myStats->defending = false; + my->monsterDefend = 0; + serverUpdateEntitySkill(my, 47); + } + else if ( my->monsterAttack == 0 ) + { + myStats->defending = true; + } } } else @@ -4637,6 +5166,25 @@ void actMonster(Entity* my) myStats->defending = false; } + if ( myStats->getEffectActive(EFF_SPIN) ) + { + my->monsterLookTime = 1; + my->monsterLookDir += PI / 16; + } + if ( myStats->getEffectActive(EFF_DISORIENTED) == 2 ) + { + if ( myStats->EFFECTS_TIMERS[EFF_DISORIENTED] == 1 ) + { + my->monsterLookTime = 1; + my->monsterLookDir += PI; + } + } + + if ( my->monsterState == MONSTER_STATE_WAIT ) + { + myStats->monsterRangedAccuracy.accuracy = 0.0; + } + /*if ( myStats->defending ) { messagePlayer(0, "defending!"); @@ -4673,12 +5221,39 @@ void actMonster(Entity* my) // my->getUID(), ticks, state_string.c_str(), my->monsterAttack, my->monsterHitTime, MONSTER_ATTACKTIME, devilstate, devilacted, my->monsterSpecialTimer); //Debug message. //} + int linetraceTargetEnemyFlags = LINETRACE_ATK_CHECK_FRIENDLYFIRE; + //Begin state machine if ( my->monsterState == MONSTER_STATE_WAIT ) //Begin wait state { //my->monsterTarget = -1; //TODO: Setting it to -1 = Bug? -1 may not work properly for cases such as: if ( !my->monsterTarget ) - my->monsterReleaseAttackTarget(); - if ( !myStats->EFFECTS[EFF_KNOCKBACK] ) + if ( myStats->type == DUCK_SMALL ) + { + Entity* target = uidToEntity(my->monsterTarget); + if ( target && entityDist(target, my) < 64.0 ) + { + // don't release, try path again + if ( myReflex ) + { + my->monsterAcquireAttackTarget(*target, MONSTER_STATE_PATH); + my->lookAtEntity(*target); + if ( previousMonsterState != my->monsterState ) + { + serverUpdateEntitySkill(my, 0); + } + return; + } + } + else + { + my->monsterReleaseAttackTarget(); + } + } + else + { + my->monsterReleaseAttackTarget(); + } + if ( !myStats->getEffectActive(EFF_KNOCKBACK) ) { MONSTER_VELX = 0; MONSTER_VELY = 0; @@ -4686,19 +5261,35 @@ void actMonster(Entity* my) else { // do knockback movement - my->monsterHandleKnockbackVelocity(my->monsterKnockbackTangentDir, weightratio); + my->monsterHandleKnockbackVelocity(my->monsterKnockbackTangentDir + PI, weightratio); if ( abs(MONSTER_VELX) > 0.01 || abs(MONSTER_VELY) > 0.01 ) { dist2 = clipMove(&my->x, &my->y, MONSTER_VELX, MONSTER_VELY, my); my->handleKnockbackDamage(*myStats, hit.entity); } } - if ( myReflex && !myStats->EFFECTS[EFF_DISORIENTED] && !isIllusionTaunt ) + + Entity* scaryEntity = nullptr; + if ( myStats->getEffectActive(EFF_FEAR) && my->monsterFearfulOfUid != 0 + && !myStats->getEffectActive(EFF_DISORIENTED) + && !myStats->getEffectActive(EFF_KNOCKBACK) && !isIllusionTaunt ) + { + scaryEntity = uidToEntity(my->monsterFearfulOfUid); + if ( scaryEntity && scaryEntity->behavior == &actRadiusMagic ) + { + real_t tangent = atan2(scaryEntity->y - my->y, scaryEntity->x - my->x); + MONSTER_VELX = 0.25 * cos(tangent + PI); + MONSTER_VELY = 0.25 * sin(tangent + PI); + my->lookAtEntity(*scaryEntity); + clipMove(&my->x, &my->y, MONSTER_VELX, MONSTER_VELY, my); + } + } + + if ( myReflex && !myStats->getEffectActive(EFF_DISORIENTED) && !isIllusionTaunt ) { - if ( myStats->EFFECTS[EFF_FEAR] && my->monsterFearfulOfUid != 0 ) + if ( myStats->getEffectActive(EFF_FEAR) && my->monsterFearfulOfUid != 0 ) { - Entity* scaryEntity = uidToEntity(my->monsterFearfulOfUid); - if ( scaryEntity ) + if ( scaryEntity && scaryEntity->behavior != &actRadiusMagic ) { my->monsterAcquireAttackTarget(*scaryEntity, MONSTER_STATE_PATH); my->lookAtEntity(*scaryEntity); @@ -4735,7 +5326,8 @@ void actMonster(Entity* my) // skip if light level is too low and distance is too high int light = entity->entityLightAfterReductions(*hitstats, my); - if ( (myStats->type >= LICH && myStats->type < KOBOLD) || myStats->type == LICH_FIRE || myStats->type == LICH_ICE || myStats->type == SHADOW ) + if ( (myStats->type >= LICH && myStats->type < KOBOLD) || myStats->type == LICH_FIRE + || myStats->type == LICH_ICE || myStats->type == SHADOW || myStats->type == MONSTER_ADORCISED_WEAPON ) { //See invisible. light = 1000; @@ -4747,10 +5339,14 @@ void actMonster(Entity* my) double targetdist = sqrt( pow(my->x - entity->x, 2) + pow(my->y - entity->y, 2) ); real_t monsterVisionRange = sightranges[myStats->type]; - if ( hitstats->type == DUMMYBOT ) + if ( hitstats->type == DUMMYBOT || hitstats->type == HOLOGRAM ) { monsterVisionRange = std::max(monsterVisionRange, 96.0); } + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->mechanics.ensemblePlaying >= 0 ) + { + monsterVisionRange = std::max(monsterVisionRange, 5 * 16.0); + } if ( targetdist > monsterVisionRange ) { @@ -4760,7 +5356,7 @@ void actMonster(Entity* my) { if ( !(myStats->leader_uid == entity->getUID()) && !(hitstats->leader_uid == my->getUID()) - && !(my->monsterAllyGetPlayerLeader() && entity->behavior == &actPlayer) ) + && !((my->monsterAllyGetPlayerLeader() || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0) && entity->behavior == &actPlayer) ) { if ( !levitating ) { @@ -4771,15 +5367,33 @@ void actMonster(Entity* my) lineTrace(my, my->x, my->y, tangent, monsterVisionRange, 0, false); } if ( hit.entity == entity ) + { + if ( entity->behavior == &actPlayer && entity->isInvisible() ) + { + real_t dist = sqrt(pow(entity->vel_x, 2) + pow(entity->vel_y, 2)); + if ( dist > 0.05 ) + { + players[entity->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, 1.0, 1.0, my); + } + } if ( local_rng.rand() % 100 == 0 ) { - entity->increaseSkill(PRO_STEALTH); + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->mechanics.allowedRaiseStealthAgainstEntity(*my) ) + { + entity->increaseSkill(PRO_STEALTH); + players[entity->skill[2]]->mechanics.enemyRaisedStealthAgainst[my->getUID()]++; + } } + } } continue; } bool visiontest = false; - if ( hitstats->type == DUMMYBOT || myStats->type == SENTRYBOT || myStats->type == SPELLBOT + if ( (myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "") ) + { + visiontest = true; + } + else if ( hitstats->type == DUMMYBOT || hitstats->type == HOLOGRAM || myStats->type == SENTRYBOT || myStats->type == SPELLBOT || (ringConflictHolder && ringConflictHolder == entity) ) { if ( dir >= -13 * PI / 16 && dir <= 13 * PI / 16 ) @@ -4811,11 +5425,11 @@ void actMonster(Entity* my) } else { - lineTrace(my, my->x, my->y, tangent, monsterVisionRange, LINETRACE_IGNORE_ENTITIES, false); + lineTrace(my, my->x, my->y, tangent, monsterVisionRange, LINETRACE_IGNORE_ENTITIES | linetraceTargetEnemyFlags, false); } if ( !hit.entity ) { - lineTrace(my, my->x, my->y, tangent, TOUCHRANGE, 0, false); + lineTrace(my, my->x, my->y, tangent, TOUCHRANGE, linetraceTargetEnemyFlags, false); } if ( hit.entity == entity ) { @@ -4878,7 +5492,7 @@ void actMonster(Entity* my) if ( !entity->checkFriend(attackTarget) ) { tangent = atan2( entity->y - my->y, entity->x - my->x ); - lineTrace(my, my->x, my->y, tangent, monsterVisionRange, 0, false); + lineTrace(my, my->x, my->y, tangent, monsterVisionRange, linetraceTargetEnemyFlags, false); if ( hit.entity == entity ) { entity->monsterAcquireAttackTarget(*attackTarget, MONSTER_STATE_PATH); @@ -4973,9 +5587,9 @@ void actMonster(Entity* my) // follow the leader :) if ( myStats->leader_uid != 0 && my->monsterAllyState == ALLY_STATE_DEFAULT - && !myStats->EFFECTS[EFF_FEAR] - && !myStats->EFFECTS[EFF_DISORIENTED] - && !myStats->EFFECTS[EFF_ROOTED] + && !myStats->getEffectActive(EFF_FEAR) + && !myStats->getEffectActive(EFF_DISORIENTED) + && !myStats->getEffectActive(EFF_ROOTED) && !isIllusionTaunt && !monsterIsImmobileTurret(my, myStats) && my->getUID() % TICKS_PER_SECOND == ticks % monsterAllyFormations.getFollowerChaseLeaderInterval(*my, *myStats) @@ -4984,10 +5598,25 @@ void actMonster(Entity* my) Entity* leader = uidToEntity(myStats->leader_uid); if ( leader ) { + Uint32 leaderUid = leader->getUID(); + + if ( leader && leader->behavior == &actPlayer && players[leader->skill[2]]->ghost.isActive() && players[leader->skill[2]]->ghost.isSpiritGhost() ) + { + leader = players[leader->skill[2]]->ghost.my; + } + real_t followx = leader->x; real_t followy = leader->y; - if ( myStats->type == GYROBOT ) + + int followDist = WAIT_FOLLOWDIST; + if ( myStats->type == GYROBOT || myStats->type == DUCK_SMALL + || (myStats->type == MOTH_SMALL && myStats->getAttribute("fire_sprite") != "") ) { + if ( myStats->type == MOTH_SMALL || myStats->type == DUCK_SMALL ) + { + followDist /= 2; + } + // follow ahead of the leader. real_t startx = leader->x; real_t starty = leader->y; @@ -5028,7 +5657,7 @@ void actMonster(Entity* my) // hit a wall. break; } - if ( sqrt(pow(leader->x - previousx, 2) + pow(leader->y - previousy, 2)) > WAIT_FOLLOWDIST ) + if ( sqrt(pow(leader->x - previousx, 2) + pow(leader->y - previousy, 2)) > followDist ) { break; } @@ -5039,7 +5668,7 @@ void actMonster(Entity* my) } double dist = sqrt(pow(my->x - followx, 2) + pow(my->y - followy, 2)); - if ( dist > WAIT_FOLLOWDIST ) + if ( dist > followDist ) { bool doFollow = true; if ( my->monsterTarget != 0 ) @@ -5051,7 +5680,8 @@ void actMonster(Entity* my) { my->monsterReleaseAttackTarget(); std::pair followPos; - if ( myStats->type == GYROBOT ) + if ( myStats->type == GYROBOT || myStats->type == DUCK_SMALL + || (myStats->type == MOTH_SMALL && myStats->getAttribute("fire_sprite") != "") ) { if ( my->monsterSetPathToLocation(static_cast(followx) / 16, static_cast(followy) / 16, 0, GeneratePathTypes::GENERATE_PATH_ALLY_FOLLOW) ) @@ -5060,7 +5690,7 @@ void actMonster(Entity* my) my->monsterState = MONSTER_STATE_HUNT; // hunt state } } - else if ( monsterAllyFormations.getFollowLocation(my->getUID(), leader->getUID(), followPos) ) + else if ( monsterAllyFormations.getFollowLocation(my->getUID(), leaderUid, followPos) ) { if ( my->monsterSetPathToLocation(followPos.first, followPos.second, 1, GeneratePathTypes::GENERATE_PATH_ALLY_FOLLOW) ) @@ -5079,10 +5709,14 @@ void actMonster(Entity* my) return; } } - else if ( myStats->type != GYROBOT ) + else if ( !(myStats->type == GYROBOT || myStats->type == DUCK_SMALL + || (myStats->type == MOTH_SMALL && myStats->getAttribute("fire_sprite") != "")) ) { tangent = atan2( leader->y - my->y, leader->x - my->x ); + bool oldPassable = leader->flags[PASSABLE]; // hack for ghosts + leader->flags[PASSABLE] = false; lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], 0, true); + leader->flags[PASSABLE] = oldPassable; if ( hit.entity != leader ) { bool doFollow = true; @@ -5094,7 +5728,7 @@ void actMonster(Entity* my) { my->monsterReleaseAttackTarget(); std::pair followPos; - if ( monsterAllyFormations.getFollowLocation(my->getUID(), leader->getUID(), followPos) ) + if ( monsterAllyFormations.getFollowLocation(my->getUID(), leaderUid, followPos) ) { if ( my->monsterSetPathToLocation(followPos.first, followPos.second, 1, GeneratePathTypes::GENERATE_PATH_ALLY_FOLLOW) ) @@ -5128,7 +5762,7 @@ void actMonster(Entity* my) my->monsterLookTime = 0; my->monsterMoveTime--; if ( myStats->type != GHOUL && (myStats->type != SPIDER || (myStats->type == SPIDER && my->monsterAllyGetPlayerLeader())) - && !myStats->EFFECTS[EFF_FEAR] && !isIllusionTaunt ) + && !myStats->getEffectActive(EFF_FEAR) && !isIllusionTaunt ) { if ( monsterIsImmobileTurret(my, myStats) ) { @@ -5150,7 +5784,12 @@ void actMonster(Entity* my) my->monsterLookDir = (local_rng.rand() % 360) * PI / 180; } } - if ( !myStats->EFFECTS[EFF_FEAR] && my->monsterTarget == 0 && my->monsterState == MONSTER_STATE_WAIT && my->monsterAllyGetPlayerLeader() ) + if ( !myStats->getEffectActive(EFF_FEAR) + && my->monsterTarget == 0 + && my->monsterState == MONSTER_STATE_WAIT + && (my->monsterAllyGetPlayerLeader() + || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0 + || (myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "")) ) { // allies should try intelligently scan for enemies in radius. if ( monsterIsImmobileTurret(my, myStats) && myStats->LVL < 5 ) @@ -5160,6 +5799,10 @@ void actMonster(Entity* my) else { real_t dist = sightranges[myStats->type]; + if ( myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "" ) + { + dist = 40; + } for ( node = map.creatures->first; node != nullptr; node = node->next ) { Entity* target = (Entity*)node->element; @@ -5170,7 +5813,7 @@ void actMonster(Entity* my) if ( dist < sightranges[myStats->type] && dist <= oldDist ) { double tangent = atan2(target->y - my->y, target->x - my->x); - lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], 0, false); + lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], linetraceTargetEnemyFlags, false); if ( hit.entity == target ) { //my->monsterLookTime = 1; @@ -5252,9 +5895,9 @@ void actMonster(Entity* my) } if ( my->monsterMoveTime == 0 && (uidToEntity(myStats->leader_uid) == NULL || my->monsterAllyState == ALLY_STATE_DEFEND) - && !myStats->EFFECTS[EFF_FEAR] - && !myStats->EFFECTS[EFF_DISORIENTED] - && !myStats->EFFECTS[EFF_ROOTED] + && !myStats->getEffectActive(EFF_FEAR) + && !myStats->getEffectActive(EFF_DISORIENTED) + && !myStats->getEffectActive(EFF_ROOTED) && !isIllusionTaunt && !(monsterIsImmobileTurret(my, myStats)) && myStats->type != DEVIL ) @@ -5281,14 +5924,19 @@ void actMonster(Entity* my) searchLimitX = 7; searchLimitY = 7; } + else if ( !strcmp(map.name, "Citadel") ) + { + searchLimitX = 7; + searchLimitY = 7; + } int lowerX = std::max(0, centerX - searchLimitX); // assigned upper/lower x coords from entity start position. int upperX = std::min(centerX + searchLimitX, map.width); int lowerY = std::max(0, centerY - searchLimitY); // assigned upper/lower y coords from entity start position. int upperY = std::min(centerY + searchLimitY, map.height); //messagePlayer(0, "my x: %d, my y: %d, rangex: (%d-%d), rangey: (%d-%d)", centerX, centerY, lowerX, upperX, lowerY, upperY); - - if ( myStats->type != SHOPKEEPER && (myStats->MISC_FLAGS[STAT_FLAG_NPC] == 0 && my->monsterAllyState == ALLY_STATE_DEFAULT) ) + if ( myStats->type != SHOPKEEPER && !my->monsterCanTradeWith(-1) + && (myStats->MISC_FLAGS[STAT_FLAG_NPC] == 0 && my->monsterAllyState == ALLY_STATE_DEFAULT) ) { for ( x = lowerX; x < upperX; x++ ) { @@ -5415,7 +6063,7 @@ void actMonster(Entity* my) if ( myStats->type == SHOPKEEPER && strncmp(map.name, "Mages Guild", 11) ) { // shopkeepers hold a grudge against players - for ( c = 0; c < MAXPLAYERS; ++c ) + for ( int c = 0; c < MAXPLAYERS; ++c ) { if ( players[c] && players[c]->entity ) { @@ -5432,7 +6080,8 @@ void actMonster(Entity* my) { // skip if light level is too low and distance is too high int light = entity->entityLightAfterReductions(*hitstats, my); - if ( (myStats->type >= LICH && myStats->type < KOBOLD) || myStats->type == LICH_FIRE || myStats->type == LICH_ICE || myStats->type == SHADOW ) + if ( (myStats->type >= LICH && myStats->type < KOBOLD) || myStats->type == LICH_FIRE || myStats->type == LICH_ICE + || myStats->type == SHADOW || myStats->type == MONSTER_ADORCISED_WEAPON ) { //See invisible. light = 1000; @@ -5444,11 +6093,15 @@ void actMonster(Entity* my) double targetdist = sqrt( pow(my->x - entity->x, 2) + pow(my->y - entity->y, 2) ); real_t monsterVisionRange = sightranges[myStats->type]; - if ( hitstats && hitstats->type == DUMMYBOT ) + if ( hitstats && (hitstats->type == DUMMYBOT || hitstats->type == HOLOGRAM) ) { monsterVisionRange = std::max(monsterVisionRange, 96.0); } - if ( myStats->EFFECTS[EFF_FEAR] ) + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->mechanics.ensemblePlaying >= 0 ) + { + monsterVisionRange = std::max(monsterVisionRange, 5 * 16.0); + } + if ( myStats->getEffectActive(EFF_FEAR) ) { targetdist = 0.0; // so we can always see our scary target. } @@ -5456,7 +6109,7 @@ void actMonster(Entity* my) if ( targetdist > monsterVisionRange ) { // if target has left my sight, decide whether or not to path or retreat (stay put). - if ( my->shouldRetreat(*myStats) && !myStats->EFFECTS[EFF_FEAR] ) + if ( my->shouldRetreat(*myStats) && !myStats->getEffectActive(EFF_FEAR) ) { my->monsterMoveTime = 0; my->monsterState = MONSTER_STATE_WAIT; // wait state @@ -5472,7 +6125,7 @@ void actMonster(Entity* my) { if ( !(myStats->leader_uid == entity->getUID()) && !(hitstats->leader_uid == my->getUID()) - && !(my->monsterAllyGetPlayerLeader() && entity->behavior == &actPlayer) ) + && !((my->monsterAllyGetPlayerLeader() || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0) && entity->behavior == &actPlayer) ) { tangent = atan2( my->monsterTargetY - my->y, my->monsterTargetX - my->x ); if ( !levitating ) @@ -5485,15 +6138,27 @@ void actMonster(Entity* my) } if ( hit.entity == entity ) { + if ( entity->behavior == &actPlayer && entity->isInvisible() ) + { + real_t dist = sqrt(pow(entity->vel_x, 2) + pow(entity->vel_y, 2)); + if ( dist > 0.05 ) + { + players[entity->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, 1.0, 1.0, my); + } + } if ( local_rng.rand() % 100 == 0 ) { - entity->increaseSkill(PRO_STEALTH); + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->mechanics.allowedRaiseStealthAgainstEntity(*my) ) + { + entity->increaseSkill(PRO_STEALTH); + players[entity->skill[2]]->mechanics.enemyRaisedStealthAgainst[my->getUID()]++; + } } } } // if target is within sight range but light level is too low and out of melee range. // decide whether or not to path or retreat (stay put). - if ( my->shouldRetreat(*myStats) && !myStats->EFFECTS[EFF_FEAR] ) + if ( my->shouldRetreat(*myStats) && !myStats->getEffectActive(EFF_FEAR) ) { my->monsterMoveTime = 0; my->monsterState = MONSTER_STATE_WAIT; // wait state @@ -5505,7 +6170,7 @@ void actMonster(Entity* my) } else { - if ( myStats->EFFECTS[EFF_FEAR] ) + if ( myStats->getEffectActive(EFF_FEAR) ) { myReflex = false; // don't determine if you lost sight of the scary monster. } @@ -5537,7 +6202,7 @@ void actMonster(Entity* my) { if ( hasrangedweapon ) { - dist = lineTrace(my, my->x, my->y, tangent, monsterVisionRange, 0, false); + dist = lineTrace(my, my->x, my->y, tangent, monsterVisionRange, linetraceTargetEnemyFlags, false); if ( hit.entity == entity ) { Entity* ohitentity = hit.entity; @@ -5548,7 +6213,7 @@ void actMonster(Entity* my) real_t my1 = my->y + 2.5 * sin(tangent + PI / 2); real_t mx1 = my->x + 2.5 * cos(tangent + PI / 2); real_t tangent1 = atan2(my->monsterTargetY - my1, my->monsterTargetX - mx1); - dist = lineTraceTarget(my, mx1, my1, tangent1, monsterVisionRange, 0, false, entity); + dist = lineTraceTarget(my, mx1, my1, tangent1, monsterVisionRange, linetraceTargetEnemyFlags, false, entity); hitentity1 = hit.entity; } Entity* hitentity2 = nullptr; @@ -5556,7 +6221,7 @@ void actMonster(Entity* my) real_t my2 = my->y - 2.5 * sin(tangent + PI / 2); real_t mx2 = my->x - 2.5 * cos(tangent + PI / 2); real_t tangent2 = atan2(my->monsterTargetY - my2, my->monsterTargetX - mx2); - dist = lineTraceTarget(my, mx2, my2, tangent2, monsterVisionRange, 0, false, entity); + dist = lineTraceTarget(my, mx2, my2, tangent2, monsterVisionRange, linetraceTargetEnemyFlags, false, entity); hitentity2 = hit.entity; } if ( hitentity1 && hitentity2 ) @@ -5572,12 +6237,12 @@ void actMonster(Entity* my) } else { - dist = lineTrace(my, my->x, my->y, tangent, monsterVisionRange, 0, true); + dist = lineTrace(my, my->x, my->y, tangent, monsterVisionRange, linetraceTargetEnemyFlags, true); } } else { - dist = lineTrace(my, my->x, my->y, tangent, monsterVisionRange, 0, false); + dist = lineTrace(my, my->x, my->y, tangent, monsterVisionRange, linetraceTargetEnemyFlags, false); } } else @@ -5589,7 +6254,7 @@ void actMonster(Entity* my) { // if I currently lost sight of my target in a straight line in front of me // decide whether or not to path or retreat (stay put). - if ( my->shouldRetreat(*myStats) && !myStats->EFFECTS[EFF_FEAR] ) + if ( my->shouldRetreat(*myStats) && !myStats->getEffectActive(EFF_FEAR) ) { my->monsterMoveTime = 0; my->monsterState = MONSTER_STATE_WAIT; // wait state @@ -5635,7 +6300,7 @@ void actMonster(Entity* my) } Entity* tempHitEntity = hit.entity; - if ( lineTrace(my, my->x, my->y, tangent2, TOUCHRANGE, 1, false) < TOUCHRANGE ) + if ( lineTrace(my, my->x, my->y, tangent2, TOUCHRANGE, LINETRACE_IGNORE_ENTITIES | linetraceTargetEnemyFlags, false) < TOUCHRANGE ) { MONSTER_FLIPPEDANGLE = (MONSTER_FLIPPEDANGLE < 5) * 10; goAgain++; @@ -5659,7 +6324,7 @@ void actMonster(Entity* my) int myDex = my->monsterGetDexterityForMovement(); real_t maxVelX = cos(tangent2) * .045 * (myDex + 10) * weightratio; real_t maxVelY = sin(tangent2) * .045 * (myDex + 10) * weightratio; - if ( !myStats->EFFECTS[EFF_KNOCKBACK] ) + if ( !myStats->getEffectActive(EFF_KNOCKBACK) ) { MONSTER_VELX = maxVelX; MONSTER_VELY = maxVelY; @@ -5672,7 +6337,7 @@ void actMonster(Entity* my) if ( effectiveDistance < rangedWeaponDistance ) { // shorter range xbows etc should advance at a little less than the extremity. - rangedWeaponDistance = effectiveDistance - 10; + rangedWeaponDistance = std::max(STRIKERANGE, effectiveDistance - 10); } if ( myStats->weapon && myStats->weapon->type == SPELLBOOK_DASH ) { @@ -5685,14 +6350,37 @@ void actMonster(Entity* my) { chaseRange = 20; } + if ( myStats->type == DUCK_SMALL ) + { + chaseRange = 32; + } + if ( myStats->weapon && (myStats->weapon->type == TOOL_WHIP || myStats->weapon->type == STEEL_FLAIL) ) + { + chaseRange = TOUCHRANGE; + } - if ( monsterIsImmobileTurret(my, myStats) || myStats->EFFECTS[EFF_ROOTED] ) + if ( monsterIsImmobileTurret(my, myStats) || myStats->getEffectActive(EFF_ROOTED) ) { // this is just so that the monster rotates. it doesn't actually move - MONSTER_VELX = maxVelX * 0.01; - MONSTER_VELY = maxVelY * 0.01; + if ( !myStats->getEffectActive(EFF_KNOCKBACK) ) + { + MONSTER_VELX = maxVelX * 0.01; + MONSTER_VELY = maxVelY * 0.01; + } + else + { + my->monsterHandleKnockbackVelocity(my->monsterKnockbackTangentDir + PI, weightratio); + bool flag = my->flags[PASSABLE]; + if ( (myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "") ) + { + my->flags[PASSABLE] = true; // hack to zoom through stuff + } + dist2 = clipMove(&my->x, &my->y, MONSTER_VELX, MONSTER_VELY, my); + my->handleKnockbackDamage(*myStats, hit.entity); + my->flags[PASSABLE] = flag; + } } - else if ( !myStats->EFFECTS[EFF_KNOCKBACK] && + else if ( !myStats->getEffectActive(EFF_KNOCKBACK) && ((dist > chaseRange && !hasrangedweapon && !my->shouldRetreat(*myStats)) || (hasrangedweapon && dist > rangedWeaponDistance)) ) { @@ -5732,7 +6420,7 @@ void actMonster(Entity* my) } if ( hit.entity != NULL ) { - if ( hit.entity->behavior == &actDoor ) + if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) { // opens the door if unlocked and monster can do it if ( !hit.entity->doorLocked && my->getINT() > -2 ) @@ -5748,6 +6436,42 @@ void actMonster(Entity* my) playSoundEntity(hit.entity, 21, 96); } } + else if ( hit.entity->behavior == &actIronDoor ) + { + if ( myStats->type == MINOTAUR ) + { + hit.entity->doorHealth = 0; // minotaurs smash doors instantly + my->monsterAttack = my->getAttackPose(); // random attack motion + my->monsterAttackTime = 0; + my->monsterHitTime = 0; + + playSoundEntity(hit.entity, 28, 64); + if ( hit.entity->doorHealth <= 0 ) + { + // set direction of splinters + if ( !hit.entity->doorDir ) + { + hit.entity->doorSmacked = (my->x > hit.entity->x); + } + else + { + hit.entity->doorSmacked = (my->y < hit.entity->y); + } + } + } + else + { + if ( my->shouldRetreat(*myStats) && !myStats->getEffectActive(EFF_FEAR) ) + { + my->monsterMoveTime = 0; + my->monsterState = MONSTER_STATE_WAIT; // wait state + } + else + { + my->monsterState = MONSTER_STATE_PATH; // path state + } + } + } else { // can't open door, so break it down @@ -5767,8 +6491,8 @@ void actMonster(Entity* my) { hit.entity->doorHealth = 0; // minotaurs smash doors instantly } - updateEnemyBar(my, hit.entity, Language::get(674), hit.entity->skill[4], hit.entity->skill[9], - false, DamageGib::DMG_DEFAULT); + updateEnemyBar(my, hit.entity, hit.entity->behavior == &actIronDoor ? Language::get(6414) : Language::get(674), + hit.entity->doorHealth, hit.entity->doorMaxHealth, false, DamageGib::DMG_DEFAULT); playSoundEntity(hit.entity, 28, 64); if ( hit.entity->doorHealth <= 0 ) { @@ -5853,7 +6577,7 @@ void actMonster(Entity* my) } else { - if ( my->shouldRetreat(*myStats) && !myStats->EFFECTS[EFF_FEAR] ) + if ( my->shouldRetreat(*myStats) && !myStats->getEffectActive(EFF_FEAR) ) { my->monsterMoveTime = 0; my->monsterState = MONSTER_STATE_WAIT; // wait state @@ -5866,7 +6590,7 @@ void actMonster(Entity* my) } else { - if ( my->shouldRetreat(*myStats) && !myStats->EFFECTS[EFF_FEAR] ) + if ( my->shouldRetreat(*myStats) && !myStats->getEffectActive(EFF_FEAR) ) { my->monsterMoveTime = 0; my->monsterState = MONSTER_STATE_WAIT; // wait state @@ -5923,7 +6647,7 @@ void actMonster(Entity* my) int myDex = my->monsterGetDexterityForMovement(); real_t maxVelX = cos(tangent2) * .045 * (myDex + 10) * weightratio * -.5; real_t maxVelY = sin(tangent2) * .045 * (myDex + 10) * weightratio * -.5; - if ( myStats->EFFECTS[EFF_KNOCKBACK] ) + if ( myStats->getEffectActive(EFF_KNOCKBACK) ) { my->monsterHandleKnockbackVelocity(tangent2, weightratio); } @@ -6042,7 +6766,7 @@ void actMonster(Entity* my) // override if we're strafing, keep facing the target dir = my->yaw - atan2(-tempVelY, -tempVelX); } - else if ( myStats->EFFECTS[EFF_KNOCKBACK] ) + else if ( myStats->getEffectActive(EFF_KNOCKBACK) ) { // in knockback, the velocitys change sign from negative/positive or positive/negative. // this makes monsters moonwalk if the direction to rotate is assumed the same. @@ -6081,6 +6805,12 @@ void actMonster(Entity* my) { dir = my->yaw - atan2( MONSTER_VELY, MONSTER_VELX ); } + + if ( myStats->getEffectActive(EFF_SPIN) ) + { + dir += my->monsterLookDir; + } + while ( dir >= PI ) { dir -= PI * 2; @@ -6145,8 +6875,7 @@ void actMonster(Entity* my) { if ( !MONSTER_ATTACK ) { - int c; - for ( c = 0; c < MAXPLAYERS; c++ ) + for ( int c = 0; c < MAXPLAYERS; c++ ) { playSoundPlayer(c, 204, 64); } @@ -6218,7 +6947,7 @@ void actMonster(Entity* my) tangent = atan2(entity->y - my->y, entity->x - my->x); my->yaw = tangent; (void)castSpell(my->getUID(), &spell_fireball, true, false); - for ( c = 0; c < MAXPLAYERS; ++c ) + for ( int c = 0; c < MAXPLAYERS; ++c ) { if ( players[c] && players[c]->entity && entity != players[c]->entity ) { @@ -6284,6 +7013,12 @@ void actMonster(Entity* my) my->monsterMoveTime = 0; return; } + else if ( myStats->type == HOLOGRAM ) + { + my->monsterState = MONSTER_STATE_WAIT; + my->monsterMoveTime = 0; + return; + } else if ( monsterIsImmobileTurret(my, myStats) ) { my->monsterState = MONSTER_STATE_WAIT; @@ -6428,7 +7163,8 @@ void actMonster(Entity* my) // skip if light level is too low and distance is too high int light = entity->entityLightAfterReductions(*hitstats, my); - if ( (myStats->type >= LICH && myStats->type < KOBOLD) || myStats->type == LICH_FIRE || myStats->type == LICH_ICE || myStats->type == SHADOW ) + if ( (myStats->type >= LICH && myStats->type < KOBOLD) || myStats->type == LICH_FIRE || myStats->type == LICH_ICE || myStats->type == SHADOW + || myStats->type == MONSTER_ADORCISED_WEAPON ) { //See invisible. light = 1000; @@ -6436,10 +7172,14 @@ void actMonster(Entity* my) double targetdist = sqrt( pow(my->x - entity->x, 2) + pow(my->y - entity->y, 2) ); real_t monsterVisionRange = sightranges[myStats->type]; - if ( hitstats->type == DUMMYBOT ) + if ( hitstats->type == DUMMYBOT || hitstats->type == HOLOGRAM ) { monsterVisionRange = std::max(monsterVisionRange, 96.0); } + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->mechanics.ensemblePlaying >= 0 ) + { + monsterVisionRange = std::max(monsterVisionRange, 5 * 16.0); + } if ( targetdist > monsterVisionRange ) { @@ -6449,7 +7189,7 @@ void actMonster(Entity* my) { if ( !(myStats->leader_uid == entity->getUID()) && !(hitstats->leader_uid == my->getUID()) - && !(my->monsterAllyGetPlayerLeader() && entity->behavior == &actPlayer) ) + && !((my->monsterAllyGetPlayerLeader() || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0) && entity->behavior == &actPlayer) ) { if ( !levitating ) { @@ -6461,16 +7201,32 @@ void actMonster(Entity* my) } if ( hit.entity == entity ) { + if ( entity->behavior == &actPlayer && entity->isInvisible() ) + { + real_t dist = sqrt(pow(entity->vel_x, 2) + pow(entity->vel_y, 2)); + if ( dist > 0.05 ) + { + players[entity->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, 1.0, 1.0, my); + } + } if ( local_rng.rand() % 100 == 0 ) { - entity->increaseSkill(PRO_STEALTH); + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->mechanics.allowedRaiseStealthAgainstEntity(*my) ) + { + entity->increaseSkill(PRO_STEALTH); + players[entity->skill[2]]->mechanics.enemyRaisedStealthAgainst[my->getUID()]++; + } } } } continue; } bool visiontest = false; - if ( hitstats->type == DUMMYBOT || myStats->type == SENTRYBOT || myStats->type == SPELLBOT ) + if ( (myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "") ) + { + visiontest = true; + } + else if ( hitstats->type == DUMMYBOT || hitstats->type == HOLOGRAM || myStats->type == SENTRYBOT || myStats->type == SPELLBOT ) { if ( dir >= -13 * PI / 16 && dir <= 13 * PI / 16 ) { @@ -6479,7 +7235,7 @@ void actMonster(Entity* my) } else if ( myStats->type != SPIDER ) { - if ( my->monsterAllyGetPlayerLeader() ) + if ( my->monsterAllyGetPlayerLeader() || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0 ) { if ( dir >= -13 * PI / 16 && dir <= 13 * PI / 16 ) { @@ -6503,16 +7259,16 @@ void actMonster(Entity* my) } if ( visiontest ) // vision cone { - lineTrace(my, my->x + 1, my->y, tangent, monsterVisionRange, 0, (levitating == false)); + lineTrace(my, my->x + 1, my->y, tangent, monsterVisionRange, linetraceTargetEnemyFlags, (levitating == false)); if ( hit.entity == entity ) { - lineTrace(my, my->x - 1, my->y, tangent, monsterVisionRange, 0, (levitating == false)); + lineTrace(my, my->x - 1, my->y, tangent, monsterVisionRange, linetraceTargetEnemyFlags, (levitating == false)); if ( hit.entity == entity ) { - lineTrace(my, my->x, my->y + 1, tangent, monsterVisionRange, 0, (levitating == false)); + lineTrace(my, my->x, my->y + 1, tangent, monsterVisionRange, linetraceTargetEnemyFlags, (levitating == false)); if ( hit.entity == entity ) { - lineTrace(my, my->x, my->y - 1, tangent, monsterVisionRange, 0, (levitating == false)); + lineTrace(my, my->x, my->y - 1, tangent, monsterVisionRange, linetraceTargetEnemyFlags, (levitating == false)); if ( hit.entity == entity ) { Entity& attackTarget = *hit.entity; @@ -6551,6 +7307,11 @@ void actMonster(Entity* my) //messagePlayer(0, "hunt -> attack, %d", my->monsterHitTime); my->monsterAcquireAttackTarget(attackTarget, MONSTER_STATE_ATTACK); + if ( myStats->type == DUCK_SMALL ) + { + my->monsterHitTime = 0; + } + if ( MONSTER_SOUND == NULL ) { if ( myStats->type != MINOTAUR ) @@ -6569,8 +7330,7 @@ void actMonster(Entity* my) } else { - int c; - for ( c = 0; c < MAXPLAYERS; c++ ) + for ( int c = 0; c < MAXPLAYERS; c++ ) { if ( c == 0 ) { @@ -6586,7 +7346,7 @@ void actMonster(Entity* my) if ( entity != nullptr ) { - if ( entity->behavior == &actPlayer && myStats->type != DUMMYBOT ) + if ( entity->behavior == &actPlayer && myStats->type != DUMMYBOT && myStats->type != DUCK_SMALL ) { assailant[entity->skill[2]] = true; // as long as this is active, combat music doesn't turn off assailantTimer[entity->skill[2]] = COMBAT_MUSIC_COOLDOWN; @@ -6626,8 +7386,8 @@ void actMonster(Entity* my) if (shouldHuntPlayer) { double distToPlayer = 0; - int c, playerToChase = -1; - for (c = 0; c < MAXPLAYERS; c++) + int playerToChase = -1; + for ( int c = 0; c < MAXPLAYERS; c++) { if (players[c] && players[c]->entity) { @@ -6710,17 +7470,29 @@ void actMonster(Entity* my) && myStats->leader_uid != 0 && my->monsterAllyState == ALLY_STATE_DEFAULT && !monsterIsImmobileTurret(my, myStats) - && !myStats->EFFECTS[EFF_ROOTED] + && !myStats->getEffectActive(EFF_ROOTED) && my->getUID() % TICKS_PER_SECOND == ticks % monsterAllyFormations.getFollowerChaseLeaderInterval(*my, *myStats) ) { Entity* leader = uidToEntity(myStats->leader_uid); if ( leader ) { + Uint32 leaderUid = leader->getUID(); + if ( leader && leader->behavior == &actPlayer && players[leader->skill[2]]->ghost.isActive() && players[leader->skill[2]]->ghost.isSpiritGhost() ) + { + leader = players[leader->skill[2]]->ghost.my; + } + int followDist = HUNT_FOLLOWDIST; real_t followx = leader->x; real_t followy = leader->y; - if ( myStats->type == GYROBOT ) + if ( myStats->type == GYROBOT || myStats->type == DUCK_SMALL + || (myStats->type == MOTH_SMALL && myStats->getAttribute("fire_sprite") != "") ) { + if ( myStats->type == MOTH_SMALL || myStats->type == DUCK_SMALL ) + { + followDist /= 2; + } + // follow ahead of the leader. real_t startx = leader->x; real_t starty = leader->y; @@ -6760,7 +7532,7 @@ void actMonster(Entity* my) { break; } - if ( sqrt(pow(leader->x - previousx, 2) + pow(leader->y - previousy, 2)) > HUNT_FOLLOWDIST ) + if ( sqrt(pow(leader->x - previousx, 2) + pow(leader->y - previousy, 2)) > followDist ) { break; } @@ -6770,7 +7542,7 @@ void actMonster(Entity* my) //createParticleFollowerCommand(previousx, previousy, 0, 175); debug particle } double dist = sqrt(pow(my->x - followx, 2) + pow(my->y - followy, 2)); - if ( dist > HUNT_FOLLOWDIST ) + if ( dist > followDist ) { bool doFollow = true; if ( my->monsterTarget != 0 ) @@ -6780,7 +7552,8 @@ void actMonster(Entity* my) if ( doFollow ) { - if ( myStats->type == GYROBOT ) + if ( myStats->type == GYROBOT || myStats->type == DUCK_SMALL + || (myStats->type == MOTH_SMALL && myStats->getAttribute("fire_sprite") != "") ) { my->monsterSetPathToLocation(static_cast(followx) / 16, static_cast(followy) / 16, 0, GeneratePathTypes::GENERATE_PATH_ALLY_FOLLOW2); @@ -6788,7 +7561,7 @@ void actMonster(Entity* my) else { std::pair followPos; - if ( monsterAllyFormations.getFollowLocation(my->getUID(), leader->getUID(), followPos) ) + if ( monsterAllyFormations.getFollowLocation(my->getUID(), leaderUid, followPos) ) { my->monsterSetPathToLocation(followPos.first, followPos.second, 2, GeneratePathTypes::GENERATE_PATH_ALLY_FOLLOW2); @@ -6802,7 +7575,8 @@ void actMonster(Entity* my) return; } } - else if ( myStats->type != GYROBOT ) + else if ( !(myStats->type == GYROBOT || myStats->type == DUCK_SMALL + || (myStats->type == MOTH_SMALL && myStats->getAttribute("fire_sprite") != "")) ) { bool doFollow = true; if ( my->monsterTarget != 0 ) @@ -6814,12 +7588,15 @@ void actMonster(Entity* my) { double tangent = atan2( leader->y - my->y, leader->x - my->x ); Entity* ohitentity = hit.entity; + bool oldPassable = leader->flags[PASSABLE]; // hack for ghosts + leader->flags[PASSABLE] = false; lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], 0, true); + leader->flags[PASSABLE] = oldPassable; if ( hit.entity != leader ) { my->monsterReleaseAttackTarget(); std::pair followPos; - if ( monsterAllyFormations.getFollowLocation(my->getUID(), leader->getUID(), followPos) ) + if ( monsterAllyFormations.getFollowLocation(my->getUID(), leaderUid, followPos) ) { if ( my->monsterSetPathToLocation(followPos.first, followPos.second, 1, GeneratePathTypes::GENERATE_PATH_ALLY_FOLLOW) ) @@ -6857,7 +7634,7 @@ void actMonster(Entity* my) if ( my->children.first->element != NULL ) { path = (list_t*)my->children.first->element; - if ( path->first != NULL && !myStats->EFFECTS[EFF_ROOTED] ) + if ( path->first != NULL && !myStats->getEffectActive(EFF_ROOTED) ) { auto pathnode = (pathnode_t*)path->first->element; dist = sqrt( pow(pathnode->y * 16 + 8 - my->y, 2) + pow(pathnode->x * 16 + 8 - my->x, 2) ); @@ -6877,8 +7654,7 @@ void actMonster(Entity* my) } else { - int c; - for ( c = 0; c < MAXPLAYERS; c++ ) + for ( int c = 0; c < MAXPLAYERS; c++ ) { if ( c == 0 ) { @@ -6898,13 +7674,23 @@ void actMonster(Entity* my) // move monster tangent = atan2( pathnode->y * 16 + 8 - my->y, pathnode->x * 16 + 8 - my->x ); int myDex = my->getDEX(); - if ( my->monsterAllyGetPlayerLeader() ) + if ( my->monsterAllyGetPlayerLeader() || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0 ) { + if ( myStats->type == DUCK_SMALL ) + { + if ( entity ) + { + if ( entityDist(my, entity) > TOUCHRANGE ) + { + myDex += 2; + } + } + } myDex = std::min(myDex, MONSTER_ALLY_DEXTERITY_SPEED_CAP); } real_t maxVelX = cos(tangent) * .045 * (myDex + 10) * weightratio; real_t maxVelY = sin(tangent) * .045 * (myDex + 10) * weightratio; - if ( myStats->EFFECTS[EFF_KNOCKBACK] ) + if ( myStats->getEffectActive(EFF_KNOCKBACK) ) { my->monsterHandleKnockbackVelocity(tangent, weightratio); } @@ -6917,7 +7703,7 @@ void actMonster(Entity* my) my->handleKnockbackDamage(*myStats, hit.entity); if ( hit.entity != NULL ) { - if ( hit.entity->behavior == &actDoor ) + if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) { // opens the door if unlocked and monster can do it if ( !hit.entity->doorLocked && my->getINT() > -2 ) @@ -6933,6 +7719,40 @@ void actMonster(Entity* my) playSoundEntity(hit.entity, 21, 96); } } + else if ( hit.entity->behavior == &actIronDoor ) + { + if ( myStats->type == MINOTAUR ) + { + hit.entity->doorHealth = 0; // minotaurs smash doors instantly + my->monsterAttack = my->getAttackPose(); // random attack motion + my->monsterAttackTime = 0; + my->monsterHitTime = 0; + + playSoundEntity(hit.entity, 28, 64); + if ( hit.entity->doorHealth <= 0 ) + { + // set direction of splinters + if ( !hit.entity->doorDir ) + { + hit.entity->doorSmacked = (my->x > hit.entity->x); + } + else + { + hit.entity->doorSmacked = (my->y < hit.entity->y); + } + } + } + else + { + ++my->monsterPathCount; + if ( my->monsterPathCount > 50 ) + { + my->monsterPathCount = 0; + //messagePlayer(0, MESSAGE_DEBUG, "remaking path!"); + my->monsterMoveBackwardsAndPath(true); + } + } + } else { // can't open door, so break it down @@ -6952,7 +7772,8 @@ void actMonster(Entity* my) { hit.entity->doorHealth = 0; // minotaurs smash doors instantly } - updateEnemyBar(my, hit.entity, Language::get(674), hit.entity->skill[4], hit.entity->skill[9], + updateEnemyBar(my, hit.entity, hit.entity->behavior == &actIronDoor ? Language::get(6414) : Language::get(674), + hit.entity->doorHealth, hit.entity->doorMaxHealth, false, DamageGib::DMG_DEFAULT); playSoundEntity(hit.entity, 28, 64); if ( hit.entity->doorHealth <= 0 ) @@ -7104,6 +7925,10 @@ void actMonster(Entity* my) // melee 240ms my->monsterHitTime = std::max(HITRATE - 12, my->monsterHitTime); } + /*if ( myStats->type == DUCK_SMALL ) + { + my->monsterHitTime = 0; + }*/ //messagePlayer(0, "bump1 -> attack, %d", my->monsterHitTime); } else if ( yourStats ) @@ -7170,6 +7995,10 @@ void actMonster(Entity* my) } //messagePlayer(0, "bump2 -> attack, %d", my->monsterHitTime); my->monsterAcquireAttackTarget(attackTarget, MONSTER_STATE_ATTACK); + /*if ( myStats->type == DUCK_SMALL ) + { + my->monsterHitTime = 0; + }*/ } else { @@ -7213,7 +8042,7 @@ void actMonster(Entity* my) } // rotate monster - if ( myStats->EFFECTS[EFF_KNOCKBACK] ) + if ( myStats->getEffectActive(EFF_KNOCKBACK) ) { // in knockback, the velocitys change sign from negative/positive or positive/negative. // this makes monsters moonwalk if the direction to rotate is assumed the same. @@ -7247,6 +8076,11 @@ void actMonster(Entity* my) { dir = my->yaw - atan2(MONSTER_VELY, MONSTER_VELX); } + + if ( myStats->getEffectActive(EFF_SPIN) ) + { + dir += my->monsterLookDir; + } while ( dir >= PI ) { dir -= PI * 2; @@ -7277,7 +8111,40 @@ void actMonster(Entity* my) messagePlayer(0, "[SHADOW] No path #1: Resetting to wait state."); }*/ } - my->monsterState = MONSTER_STATE_WAIT; // no path, return to wait state + + if ( path->first != NULL && myStats->getEffectActive(EFF_ROOTED) ) + { + // keep path while rooted + } + else + { + my->monsterState = MONSTER_STATE_WAIT; // no path, return to wait state + + if ( target && ((target->getStats() && target->getStats()->type == DUCK_SMALL) || target->behavior == &actDeathGhost) + && myStats->getEffectActive(EFF_DISORIENTED) ) + { + if ( entityDist(my, target) < STRIKERANGE ) + { + if ( target->getStats() && target->getStats()->type == DUCK_SMALL ) + { + if ( target->monsterAttack == 0 ) + { + if ( local_rng.rand() % 4 == 0 ) + { + // dodge anim + target->attack(local_rng.rand() % 2 ? MONSTER_POSE_MELEE_WINDUP2 : MONSTER_POSE_MELEE_WINDUP3, 0, nullptr); + } + } + } + + my->monsterAttack = my->getAttackPose(); // random attack motion + my->monsterAttackTime = 0; + my->monsterHitTime = std::max(my->monsterHitTime, HITRATE / 4); + serverUpdateEntitySkill(my, 9); + serverUpdateEntitySkill(my, 8); + } + } + } if ( !target && myStats->type == MIMIC ) { mimicResetIdle(my); @@ -7339,7 +8206,7 @@ void actMonster(Entity* my) if ( dist < sightranges[myStats->type] && dist <= oldDist ) { double tangent = atan2(target->y - my->y, target->x - my->x); - lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], 0, false); + lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], linetraceTargetEnemyFlags, false); if ( hit.entity == target ) { my->monsterLookTime = 1; @@ -7366,7 +8233,7 @@ void actMonster(Entity* my) serverUpdateEntitySkill(my, 43); // update monsterAllyState } } - else if ( !target && my->monsterAllyGetPlayerLeader() ) + else if ( !target && (my->monsterAllyGetPlayerLeader() || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0) ) { // scan for enemies after reaching move point. real_t dist = sightranges[myStats->type]; @@ -7380,7 +8247,7 @@ void actMonster(Entity* my) if ( dist < sightranges[myStats->type] && dist <= oldDist ) { double tangent = atan2(target->y - my->y, target->x - my->x); - lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], 0, false); + lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], linetraceTargetEnemyFlags, false); if ( hit.entity == target ) { my->monsterLookTime = 1; @@ -7492,7 +8359,7 @@ void actMonster(Entity* my) if ( dist < sightranges[myStats->type] && dist <= oldDist ) { double tangent = atan2(target->y - my->y, target->x - my->x); - lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], 0, false); + lineTrace(my, my->x, my->y, tangent, sightranges[myStats->type], linetraceTargetEnemyFlags, false); if ( hit.entity == target ) { my->monsterLookTime = 1; @@ -7550,6 +8417,11 @@ void actMonster(Entity* my) { dir += PI * 2; } + + if ( myStats->getEffectActive(EFF_SPIN) ) + { + dir += my->monsterLookDir; + } my->yaw -= dir / 2; while ( my->yaw < 0 ) { @@ -7720,8 +8592,7 @@ void actMonster(Entity* my) { serverUpdateEntitySkill(my, 8); serverUpdateEntitySkill(my, 9); - int c; - for ( c = 0; c < MAXPLAYERS; c++ ) + for ( int c = 0; c < MAXPLAYERS; c++ ) { playSoundPlayer(c, 186, 128); } @@ -8584,6 +9455,11 @@ void actMonster(Entity* my) { dir += PI * 2; } + + if ( myStats->getEffectActive(EFF_SPIN) ) + { + dir += my->monsterLookDir; + } my->yaw -= dir / 64; while ( my->yaw < 0 ) { @@ -8731,7 +9607,7 @@ void actMonster(Entity* my) { real_t distToPlayer = 0.f; int playerToChase = -1; - for ( c = 0; c < MAXPLAYERS; c++ ) + for ( int c = 0; c < MAXPLAYERS; c++ ) { if ( players[c] && players[c]->entity ) { @@ -8862,7 +9738,7 @@ void actMonster(Entity* my) { real_t distToPlayer = 0.f; int playerToChase = -1; - for ( c = 0; c < MAXPLAYERS; c++ ) + for ( int c = 0; c < MAXPLAYERS; c++ ) { if ( players[c] && players[c]->entity ) { @@ -9013,6 +9889,10 @@ void actMonster(Entity* my) bool visiontest = false; real_t monsterVisionRange = 40.0; //sightranges[myStats->type]; + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->mechanics.ensemblePlaying >= 0 ) + { + monsterVisionRange = std::max(monsterVisionRange, 5 * 16.0); + } int sizex = my->sizex; int sizey = my->sizey; my->sizex = std::max(my->sizex, 4); // override size temporarily @@ -9087,7 +9967,7 @@ void actMonster(Entity* my) } } } - else if ( myStats && myStats->type == MIMIC && myStats->EFFECTS[EFF_MIMIC_LOCKED] && !my->isInertMimic() ) + else if ( myStats && myStats->type == MIMIC && myStats->getEffectActive(EFF_MIMIC_LOCKED) && !my->isInertMimic() ) { my->monsterHitTime++; if ( my->monsterHitTime >= HITRATE ) @@ -9106,7 +9986,7 @@ void actMonster(Entity* my) } else if ( myStats && myStats->type == MIMIC && !my->isInertMimic() ) { - if ( myStats->EFFECTS[EFF_ASLEEP] || myStats->EFFECTS[EFF_PARALYZED] ) + if ( myStats->getEffectActive(EFF_ASLEEP) || myStats->getEffectActive(EFF_PARALYZED) ) { if ( my->monsterSpecialState != MIMIC_STATUS_IMMOBILE ) { @@ -9153,7 +10033,11 @@ void actMonster(Entity* my) double targetdist = sqrt(pow(my->x - entity->x, 2) + pow(my->y - entity->y, 2)); real_t monsterVisionRange = 24.0; //sightranges[myStats->type]; - + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->mechanics.ensemblePlaying >= 0 ) + { + monsterVisionRange = std::max(monsterVisionRange, 3 * 16.0); + } + if ( targetdist > monsterVisionRange ) { continue; @@ -9179,7 +10063,7 @@ void actMonster(Entity* my) { // charge state Entity* attackTarget = hit.entity; - if ( my->disturbMimic(attackTarget, false, true) ) + if ( my->disturbMimic(attackTarget, false, monsterVisionRange <= 25.0 ) ) { my->monsterAcquireAttackTarget(*attackTarget, MONSTER_STATE_ATTACK); @@ -9205,9 +10089,124 @@ void actMonster(Entity* my) } } } - if ( myStats->EFFECTS[EFF_KNOCKBACK] ) + + if ( myStats && myStats->type == DUCK_SMALL ) + { + if ( my->monsterSpecialState == DUCK_DIVE ) + { + my->monsterReleaseAttackTarget(); + real_t centerx = (int)floor(my->x / 16) * 16.0 + 8.0; + real_t centery = (int)floor(my->y / 16) * 16.0 + 8.0; + + // center on tile + real_t tangent = atan2(centery - my->y, centerx - my->x); + MONSTER_VELX = 0.1 * cos(tangent); + MONSTER_VELY = 0.1 * sin(tangent); + clipMove(&my->x, &my->y, MONSTER_VELX, MONSTER_VELY, my); + MONSTER_VELX = 0.0; + MONSTER_VELY = 0.0; + } + else if ( my->monsterSpecialState == DUCK_RETURN ) + { + my->monsterReleaseAttackTarget(); + Entity* leader = uidToEntity(myStats->leader_uid); + if ( !leader || myStats->getAttribute("duck_run") == "1" ) + { + int dir = my->getUID() % 4; + int mapx = 1; + int mapy = 1; + switch ( dir ) + { + case 0: + mapx = 1; + mapy = 1; + break; + case 1: + mapx = 1; + mapy = map.height - 2; + break; + case 2: + mapx = map.width - 2; + mapy = 1; + break; + case 3: + mapx = map.width - 2; + mapy = map.height - 2; + break; + default: + break; + } + + real_t tangent = atan2((mapy * 16.0 + 8.0) - my->y, (mapx * 16.0 + 8.0) - my->x); + my->monsterLookDir = tangent; + MONSTER_VELX = cos(tangent); + MONSTER_VELY = sin(tangent); + clipMove(&my->x, &my->y, MONSTER_VELX, MONSTER_VELY, my); + if ( static_cast(my->x / 16) == mapx && static_cast(my->y / 16) == mapy ) + { + playSoundEntity(my, 794 + local_rng.rand() % 2, 128); + playSoundEntity(my, 789 + local_rng.rand() % 5, 128); + my->setHP(0); + return; + } + } + else if ( leader ) + { + real_t tangent = atan2(leader->y - my->y, leader->x - my->x); + my->monsterLookDir = tangent; + MONSTER_VELX = cos(tangent); + MONSTER_VELY = sin(tangent); + clipMove(&my->x, &my->y, MONSTER_VELX, MONSTER_VELY, my); + if ( entityInsideEntity(my, leader) ) + { + if ( Stat* leaderStats = leader->getStats() ) + { + if ( leader->behavior == &actPlayer ) + { + int player = leader->skill[2]; + int appearance = 0; + if ( myStats->getAttribute("duck_type") != "" ) + { + appearance = std::stoi(myStats->getAttribute("duck_type")); + } + int bless = 0; + if ( myStats->getAttribute("duck_bless") != "" ) + { + bless = std::stoi(myStats->getAttribute("duck_bless")); + } + Item* item2 = newItem(TOOL_DUCK, EXCELLENT, bless, 1, appearance, true, nullptr); + Item* item = itemPickup(player, item2); + if ( item ) + { + if ( players[player]->isLocalPlayer() ) + { + // item is the new inventory stack for server, free the picked up items + free(item2); + int oldcount = item->count; + item->count = 1; + messagePlayer(player, MESSAGE_INTERACTION | MESSAGE_INVENTORY, Language::get(504), item->description()); + item->count = oldcount; + } + else + { + messagePlayer(player, MESSAGE_INTERACTION | MESSAGE_INVENTORY, Language::get(504), item->description()); + free(item); // item is the picked up items (item == item2) + } + } + } + } + playSoundEntity(my, 794 + local_rng.rand() % 2, 128); + playSoundEntity(my, 789 + local_rng.rand() % 5, 128); + my->setHP(0); + return; + } + } + } + my->monsterRotate(); + } + else if ( myStats->getEffectActive(EFF_KNOCKBACK) ) { - my->monsterHandleKnockbackVelocity(my->monsterKnockbackTangentDir, weightratio); + my->monsterHandleKnockbackVelocity(my->monsterKnockbackTangentDir + PI, weightratio); if ( abs(MONSTER_VELX) > 0.01 || abs(MONSTER_VELY) > 0.01 ) { dist2 = clipMove(&my->x, &my->y, MONSTER_VELX, MONSTER_VELY, my); @@ -9221,6 +10220,8 @@ void actMonster(Entity* my) } } + my->processEntityWind(); + if ( previousMonsterState != my->monsterState ) { serverUpdateEntitySkill(my, 0); @@ -9229,7 +10230,7 @@ void actMonster(Entity* my) serverUpdateEntitySkill(my, 1); // update monsterTarget for player leaders. } - if ( myStats->type == SHOPKEEPER && previousMonsterState == MONSTER_STATE_TALK ) + if ( previousMonsterState == MONSTER_STATE_TALK && (myStats->type == SHOPKEEPER || my->monsterCanTradeWith(-1)) ) { for ( int i = 0; i < MAXPLAYERS; ++i ) { @@ -9281,9 +10282,46 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) } } + if ( myStats->type == DUCK_SMALL ) + { + if ( dist < 48 ) + { + ++monsterHitTime; + if ( monsterHitTime >= HITRATE ) + { + bool anyTarget = duckAreaQuck(this); + if ( anyTarget ) + { + monsterHitTime = 0; + + if ( Entity* target = uidToEntity(monsterTarget) ) + { + if ( monsterSetPathToLocation(target->x / 16, target->y / 16, 1, + GeneratePathTypes::GENERATE_PATH_TO_HUNT_MONSTER_TARGET, true, false) ) + { + monsterState = MONSTER_STATE_HUNT; // hunt state + this->setEffect(EFF_DISORIENTED, true, TICKS_PER_SECOND / 2, false); + } + } + else + { + if ( monsterSetPathToLocation(this->x / 16, this->y / 16, 1, + GeneratePathTypes::GENERATE_PATH_TO_HUNT_MONSTER_TARGET, true, false) ) + { + monsterState = MONSTER_STATE_HUNT; // hunt state + this->setEffect(EFF_DISORIENTED, true, TICKS_PER_SECOND / 2, false); + } + } + } + } + } + return; + } + //TODO: I don't like this function getting called every frame. Find a better place to put it. chooseWeapon(target, dist); - bool hasrangedweapon = this->hasRangedWeapon(); + bool hasrangedweapon = this->hasRangedWeapon(true); + bool lichRangeCheckOverride = false; if ( myStats->type == SLIME ) { @@ -9292,7 +10330,52 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) lichRangeCheckOverride = true; } } - if ( myStats->type == LICH_FIRE) + if ( myStats->type == REVENANT_SKULL + || myStats->type == MONSTER_ADORCISED_WEAPON + || myStats->type == FLAME_ELEMENTAL + || myStats->type == EARTH_ELEMENTAL ) + { + if ( monsterSpecialState == SKULL_CAST ) + { + lichRangeCheckOverride = true; + } + + if ( (myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "") ) + { + lichRangeCheckOverride = true; + } + } + else if ( myStats->type == MOTH_SMALL ) + { + if ( monsterSpecialState == MOTH_CAST ) + { + lichRangeCheckOverride = true; + } + } + else if ( myStats->type == DRYAD ) + { + if ( (monsterSpecialState >= MONSTER_D_SPECIAL_CAST1 + && monsterSpecialState <= MONSTER_D_SPECIAL_CAST3) ) + { + lichRangeCheckOverride = true; + } + } + else if ( myStats->type == MYCONID ) + { + if ( (monsterSpecialState >= MONSTER_M_SPECIAL_CAST1 + && monsterSpecialState <= MONSTER_M_SPECIAL_CAST3) ) + { + lichRangeCheckOverride = true; + } + } + else if ( myStats->type == GREMLIN ) + { + if ( monsterSpecialState == MONSTER_G_SPECIAL_CAST1 ) + { + lichRangeCheckOverride = true; + } + } + else if ( myStats->type == LICH_FIRE) { if ( monsterLichFireMeleeSeq == LICH_ATK_BASICSPELL_SINGLE ) { @@ -9315,7 +10398,7 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) monsterStrafeDirection = -1 + ((local_rng.rand() % 2 == 0) ? 2 : 0); } } - else if ( myStats->type == DUMMYBOT ) + else if ( myStats->type == DUMMYBOT || myStats->type == HOLOGRAM ) { return; } @@ -9345,10 +10428,25 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) } int meleeDist = STRIKERANGE; + int strikeRange = STRIKERANGE; + if ( myStats->getEffectActive(EFF_ROOTED) || (shouldRetreat(*myStats) && myStats->getEffectActive(EFF_COWARDICE)) ) + { + meleeDist = TOUCHRANGE - 1; // so you can't melee range-cheese + } if ( myStats->type == BUGBEAR && monsterSpecialState == BUGBEAR_DEFENSE ) { meleeDist = TOUCHRANGE - 1; } + if ( myStats->weapon && myStats->weapon->type == TOOL_WHIP ) + { + meleeDist = std::max(meleeDist, (int)(STRIKERANGE * 1.5)); + strikeRange = (int)(STRIKERANGE * 1.5); + } + else if ( myStats->weapon && myStats->weapon->type == STEEL_FLAIL ) + { + meleeDist = std::max(meleeDist, (int)(STRIKERANGE * 1.5)); + strikeRange = (int)(STRIKERANGE * 1.5); + } // check the range to the target, depending on ranged weapon or melee. if ( (dist < meleeDist && !hasrangedweapon) || (hasrangedweapon && dist < getMonsterEffectiveDistanceOfRangedWeapon(myStats->weapon)) || lichRangeCheckOverride ) @@ -9360,8 +10458,11 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) { if ( (myStats->weapon->type == SLING || myStats->weapon->type == SHORTBOW + || myStats->weapon->type == BONE_SHORTBOW || myStats->weapon->type == ARTIFACT_BOW || myStats->weapon->type == LONGBOW + || myStats->weapon->type == BRANCH_BOW + || myStats->weapon->type == BRANCH_BOW_INFECTED || myStats->weapon->type == COMPOUND_BOW) ) { bow = 2; @@ -9377,7 +10478,7 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) } } } - else if ( myStats->weapon->type == CROSSBOW ) + else if ( myStats->weapon->type == CROSSBOW || myStats->weapon->type == BLACKIRON_CROSSBOW ) { if ( myStats->shield && itemTypeIsQuiver(myStats->shield->type) ) { @@ -9396,6 +10497,22 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) { bow = 1.5; } + else if ( myStats->type == REVENANT_SKULL || myStats->type == MONSTER_ADORCISED_WEAPON + || myStats->type == FLAME_ELEMENTAL || myStats->type == EARTH_ELEMENTAL ) + { + bow = 1.5; + } + else if ( myStats->type == MOTH_SMALL ) + { + if ( myStats->getAttribute("fire_sprite") != "" ) + { + bow = 3.0; + } + else + { + bow = 0.5; + } + } else if ( monsterIsImmobileTurret(this, myStats) ) { bow = 2; @@ -9419,6 +10536,18 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) } } } + else if ( myStats->weapon && myStats->weapon->type == STEEL_FLAIL ) + { + if ( this->monsterAttack == MONSTER_POSE_FLAIL_SWING ) + { + bow = 6; + } + else if ( this->monsterAttack == MONSTER_POSE_FLAIL_SWING_WINDUP ) + { + bow = 1.5; + } + } + // check if ready to attack if ( (this->monsterHitTime >= static_cast(HITRATE * monsterGlobalAttackTimeMultiplier * bow) && (myStats->type != LICH && myStats->type != LICH_ICE)) @@ -9464,7 +10593,7 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) } else { - tracedist = STRIKERANGE; + tracedist = strikeRange; } // check again for the target in attack range. return the result into hit.entity. @@ -9475,7 +10604,7 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) } else { - lineTrace(this, this->x, this->y, newTangent, tracedist, 0, false); + lineTrace(this, this->x, this->y, newTangent, tracedist, LINETRACE_ATK_CHECK_FRIENDLYFIRE, false); } if ( hit.entity != nullptr && hit.entity == target ) { @@ -9531,7 +10660,11 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) } } - if ( myStats->type == BUGBEAR && monsterSpecialState == BUGBEAR_DEFENSE ) + if ( myStats->type == MOTH_SMALL && pose == 0 ) + { + // couldn't find a body to execute attack + } + else if ( myStats->type == BUGBEAR && monsterSpecialState == BUGBEAR_DEFENSE ) { if ( myStats->shield && myStats->shield->type == STEEL_SHIELD && local_rng.rand() % 3 == 0 && dist <= meleeDist ) { @@ -9550,7 +10683,14 @@ void Entity::handleMonsterAttack(Stat* myStats, Entity* target, double dist) else if ( monsterDefend == MONSTER_DEFEND_HOLD ) { // skip attack, continue defending. offset the hit time to allow for timing variation. - monsterHitTime = HITRATE / 4; + if ( myStats->type == EARTH_ELEMENTAL ) + { + monsterHitTime = HITRATE / 2; + } + else + { + monsterHitTime = HITRATE / 4; + } } else { @@ -9937,21 +11077,22 @@ int limbAnimateToLimit(Entity* limb, int axis, double rate, double setpoint, boo } else if ( axis == ANIMATE_WEAPON_YAW ) { - while ( limb->fskill[5] < 0 ) + real_t& limbSkill = limb->behavior == &actPlayer ? limb->fskill[2] : limb->monsterWeaponYaw; + while ( limbSkill < 0 ) { - limb->fskill[5] += 2 * PI; + limbSkill += 2 * PI; } - while ( limb->fskill[5] >= 2 * PI ) + while ( limbSkill >= 2 * PI ) { - limb->fskill[5] -= 2 * PI; + limbSkill -= 2 * PI; } - if ( limbAngleWithinRange(limb->fskill[5], rate, setpoint) ) + if ( limbAngleWithinRange(limbSkill, rate, setpoint) ) { - limb->fskill[5] = setpoint; + limbSkill = setpoint; return 1; //reached setpoint } - limb->fskill[5] += rate; + limbSkill += rate; } return 0; @@ -10125,6 +11266,10 @@ bool forceFollower(Entity& leader, Entity& follower) { FollowerMenu[player].recentEntity = &follower; } + if ( followerStats->monsterForceAllegiance == Stat::MONSTER_FORCE_PLAYER_ENEMY ) + { + followerStats->monsterForceAllegiance = Stat::MONSTER_FORCE_ALLEGIANCE_NONE; + } } if ( player >= 0 ) @@ -10238,7 +11383,7 @@ bool Entity::handleMonsterSpecialAttack(Stat* myStats, Entity* target, double di break; case CRYSTALGOLEM: specialRoll = local_rng.rand() % 20; - enemiesNearby = numTargetsAroundEntity(this, STRIKERANGE, PI, MONSTER_TARGET_ENEMY); + enemiesNearby = numTargetsAroundEntity(this, TOUCHRANGE, PI, MONSTER_TARGET_ENEMY); if ( enemiesNearby > 1 ) { enemiesNearby = std::min(enemiesNearby, 4); @@ -10351,7 +11496,76 @@ bool Entity::handleMonsterSpecialAttack(Stat* myStats, Entity* target, double di { // special handled in slimeChooseWeapon() monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_SLIME_SPRAY; - break; + } + break; + case REVENANT_SKULL: + // magic + specialRoll = local_rng.rand() % 10; + if ( specialRoll == 0 ) + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_SKULL_CAST; + } + break; + case FLAME_ELEMENTAL: + // magic + specialRoll = local_rng.rand() % 10; + if ( specialRoll == 0 ) + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_SKULL_CAST; + } + break; + case MOTH_SMALL: + // magic + if ( monsterSpecialState == MOTH_CAST ) + { + // special handled in mothChooseWeapon() + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_MOTH_CAST; + } + break; + case DRYAD: + // magic + if ( (monsterSpecialState >= MONSTER_D_SPECIAL_CAST1 + && monsterSpecialState <= MONSTER_D_SPECIAL_CAST3) ) + { + // special handled in monsterDChooseWeapon() + if ( monsterSpecialState == MONSTER_D_SPECIAL_CAST2 ) + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_MONSTER_D_PUSH; + } + else + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_MONSTER_D; + } + } + break; + case MYCONID: + // special handled in monsterMChooseWeapon() + if ( monsterSpecialState == MONSTER_M_SPECIAL_THROW ) + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_MONSTER_M_THROW; + } + else if ( (monsterSpecialState >= MONSTER_M_SPECIAL_CAST1 + && monsterSpecialState <= MONSTER_M_SPECIAL_CAST3) ) + { + if ( monsterSpecialState == MONSTER_M_SPECIAL_CAST1 ) + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_SHORT; + } + else + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_LONG; + } + } + break; + case GREMLIN: + // special handled in monsterGChooseWeapon() + if ( monsterSpecialState == MONSTER_G_SPECIAL_THROW ) + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_MONSTER_G_THROW; + } + else if ( monsterSpecialState == MONSTER_G_SPECIAL_CAST1 ) + { + monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_MONSTER_G_CAST; } break; case SPIDER: @@ -10496,6 +11710,7 @@ bool Entity::handleMonsterSpecialAttack(Stat* myStats, Entity* target, double di this->monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_SHADOW_TELEPORT; break; } + break; case GOATMAN: if ( monsterSpecialState == GOATMAN_POTION ) { @@ -10596,6 +11811,33 @@ bool Entity::handleMonsterSpecialAttack(Stat* myStats, Entity* target, double di deinitSuccess = true; } break; + case REVENANT_SKULL: + case FLAME_ELEMENTAL: + if ( monsterSpecialState == SKULL_CAST || forceDeinit ) + { + monsterSpecialState = 0; + shouldAttack = false; + deinitSuccess = true; + } + break; + case MOTH_SMALL: + if ( monsterSpecialState == MOTH_CAST || forceDeinit ) + { + monsterSpecialState = 0; + shouldAttack = false; + deinitSuccess = true; + } + break; + case DRYAD: + if ( (monsterSpecialState >= MONSTER_D_SPECIAL_CAST1 + && monsterSpecialState <= MONSTER_D_SPECIAL_CAST3) + || forceDeinit ) + { + monsterSpecialState = 0; + shouldAttack = false; + deinitSuccess = true; + } + break; case SPIDER: if ( monsterSpecialState == SPIDER_CAST || forceDeinit ) { @@ -10794,6 +12036,56 @@ bool Entity::handleMonsterSpecialAttack(Stat* myStats, Entity* target, double di deinitSuccess = true; } break; + case MYCONID: + if ( (monsterSpecialState >= MONSTER_M_SPECIAL_CAST1 + && monsterSpecialState <= MONSTER_M_SPECIAL_CAST3) ) + { + monsterSpecialState = 0; + shouldAttack = false; + deinitSuccess = true; + } + else if ( monsterSpecialState == MONSTER_M_SPECIAL_THROW || forceDeinit ) + { + node = itemNodeInInventory(myStats, -1, WEAPON); // find weapon to re-equip + if ( node != nullptr ) + { + swapMonsterWeaponWithInventoryItem(this, myStats, node, false, true); + } + else + { + monsterUnequipSlotFromCategory(myStats, &myStats->weapon, THROWN); + } + shouldAttack = false; + monsterSpecialState = 0; + serverUpdateEntitySkill(this, 33); // for clients to keep track of animation + deinitSuccess = true; + } + break; + case GREMLIN: + if ( monsterSpecialState == MONSTER_G_SPECIAL_CAST1 ) + { + monsterSpecialState = 0; + shouldAttack = false; + deinitSuccess = true; + } + else if ( monsterSpecialState == MONSTER_G_SPECIAL_THROW || forceDeinit ) + { + node = itemNodeInInventory(myStats, -1, WEAPON); // find weapon to re-equip + if ( node != nullptr ) + { + swapMonsterWeaponWithInventoryItem(this, myStats, node, false, true); + } + else + { + monsterUnequipSlotFromCategory(myStats, &myStats->weapon, THROWN); + monsterUnequipSlotFromCategory(myStats, &myStats->weapon, POTION); + } + shouldAttack = false; + monsterSpecialState = 0; + serverUpdateEntitySkill(this, 33); // for clients to keep track of animation + deinitSuccess = true; + } + break; default: break; } @@ -11138,73 +12430,91 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui { return; } - if ( command == -1 || command == ALLY_CMD_CANCEL || monsterAllyIndex == -1 || monsterAllyIndex > MAXPLAYERS ) + Stat* myStats = getStats(); + if ( !myStats ) { return; } - if ( !players[monsterAllyIndex] ) + + if ( command == -1 || command == ALLY_CMD_CANCEL ) { return; } - if ( !players[monsterAllyIndex]->entity ) + + int playerLeader = monsterAllyIndex; + bool compelSpell = false; + if ( myStats->getEffectActive(EFF_COMMAND) >= 1 && myStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) { - return; + playerLeader = myStats->getEffectActive(EFF_COMMAND) - 1; + compelSpell = true; } - if ( monsterTarget == players[monsterAllyIndex]->entity->getUID() ) + + if ( playerLeader <= -1 || monsterAllyIndex >= MAXPLAYERS ) { - // angry at owner. return; } - Stat* myStats = getStats(); - if ( !myStats ) + if ( !players[playerLeader] ) + { + return; + } + if ( !players[playerLeader]->entity ) + { + return; + } + if ( monsterTarget == players[playerLeader]->entity->getUID() ) { + // angry at owner. return; } if ( !isMobile() ) { // doesn't respond. - if ( monsterAllySpecial == ALLY_SPECIAL_CMD_REST && myStats->EFFECTS[EFF_ASLEEP] + if ( monsterAllySpecial == ALLY_SPECIAL_CMD_REST && myStats->getEffectActive(EFF_ASLEEP) && (command == ALLY_CMD_MOVETO_CONFIRM || command == ALLY_CMD_ATTACK_CONFIRM || command == ALLY_CMD_MOVEASIDE) ) { - myStats->EFFECTS[EFF_ASLEEP] = false; // wake up + myStats->clearEffect(EFF_ASLEEP); // wake up myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 0; - myStats->EFFECTS[EFF_HP_REGEN] = false; // stop regen + myStats->clearEffect(EFF_HP_REGEN); // stop regen myStats->EFFECTS_TIMERS[EFF_HP_REGEN] = 0; monsterAllySpecial = ALLY_SPECIAL_CMD_NONE; } else { - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, *myStats, Language::get(514), Language::get(515), MSG_COMBAT); + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, *myStats, Language::get(514), Language::get(515), MSG_COMBAT); } return; } - bool isTinkeringFollower = FollowerMenu[monsterAllyIndex].isTinkeringFollower(myStats->type); + bool isTinkeringFollower = FollowerMenu[playerLeader].isTinkeringFollower(myStats->type); int tinkeringLVL = 0; int skillLVL = 0; - if ( stats[monsterAllyIndex] ) + if ( stats[playerLeader] ) { - tinkeringLVL = stats[monsterAllyIndex]->getModifiedProficiency(PRO_LOCKPICKING) + statGetPER(stats[monsterAllyIndex], players[monsterAllyIndex]->entity); - skillLVL = stats[monsterAllyIndex]->getModifiedProficiency(PRO_LEADERSHIP) + statGetCHR(stats[monsterAllyIndex], players[monsterAllyIndex]->entity); + tinkeringLVL = stats[playerLeader]->getModifiedProficiency(PRO_LOCKPICKING) + statGetPER(stats[playerLeader], players[playerLeader]->entity); + skillLVL = stats[playerLeader]->getModifiedProficiency(PRO_LEADERSHIP) + statGetCHR(stats[playerLeader], players[playerLeader]->entity); if ( isTinkeringFollower ) { skillLVL = tinkeringLVL; } + if ( compelSpell ) + { + skillLVL = std::max(skillLVL, stats[playerLeader]->getModifiedProficiency(PRO_MYSTICISM) + statGetCHR(stats[playerLeader], players[playerLeader]->entity)); + } } if ( myStats->type != GYROBOT ) { - if ( FollowerMenu[monsterAllyIndex].monsterGyroBotOnlyCommand(command) ) + if ( FollowerMenu[playerLeader].monsterGyroBotOnlyCommand(command) ) { return; } } else if ( myStats->type == GYROBOT ) { - if ( FollowerMenu[monsterAllyIndex].monsterGyroBotDisallowedCommands(command) ) + if ( FollowerMenu[playerLeader].monsterGyroBotDisallowedCommands(command) ) { return; } @@ -11213,7 +12523,7 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui // do a final check if player can use this command. /*if ( FollowerMenu.optionDisabledForCreature(skillLVL, myStats->type, command) != 0 ) { - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, *myStats, Language::get(3638), Language::get(3639), MSG_COMBAT); return; }*/ @@ -11221,33 +12531,49 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui switch ( command ) { case ALLY_CMD_RETURN_SOUL: - if ( monsterAllySummonRank != 0 ) + if ( monsterAllySummonRank != 0 && myStats->type == SKELETON ) { float manaToRefund = myStats->MAXMP * (myStats->HP / static_cast(myStats->MAXHP)); setMP(static_cast(manaToRefund)); setHP(0); - if ( stats[monsterAllyIndex] && stats[monsterAllyIndex]->MP == 0 ) + if ( stats[playerLeader] && stats[playerLeader]->MP == 0 ) { - steamAchievementClient(monsterAllyIndex, "BARONY_ACH_EXTERNAL_BATTERY"); + steamAchievementClient(playerLeader, "BARONY_ACH_EXTERNAL_BATTERY"); } } - break; - case ALLY_CMD_ATTACK_CONFIRM: - if ( uid != 0 && uid != getUID() ) + else if ( monsterAllySummonRank != 0 && myStats->type == EARTH_ELEMENTAL ) + { + setHP(0); + setObituary(Language::get(6803)); + } + else if ( myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("adorcised_weapon") != "" ) + { + setHP(0); + setObituary(Language::get(6619)); + } + else if ( myStats->type == FLAME_ELEMENTAL ) + { + setHP(0); + setObituary(Language::get(6673)); + } + break; + case ALLY_CMD_ATTACK_CONFIRM: + if ( uid != 0 && uid != getUID() ) { Entity* target = uidToEntity(uid); if ( target ) { if ( target->behavior == &actMonster || target->behavior == &actPlayer ) { - if ( stats[monsterAllyIndex] ) // check owner's proficiency. + Uint32 oldTarget = this->monsterTarget; + if ( stats[playerLeader] ) // check owner's proficiency. { if ( skillLVL >= SKILL_LEVEL_MASTER || myStats->type != HUMAN ) { // attack anything except if FF is off + friend. if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { - if ( !checkFriend(target) ) + if ( !(checkFriend(target) && players[playerLeader]->entity->friendlyFireProtection(target)) ) { monsterAcquireAttackTarget(*target, MONSTER_STATE_ATTACK); handleNPCInteractDialogue(*myStats, ALLY_EVENT_ATTACK); @@ -11278,6 +12604,10 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui } } monsterAllyInteractTarget = 0; + //if ( oldTarget != this->monsterTarget ) + { + players[playerLeader]->mechanics.targetsCompelled[getUID()][target->getUID()] = ::ticks; + } } else { @@ -11288,7 +12618,7 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui } break; case ALLY_CMD_MOVEASIDE: - monsterMoveAside(this, players[monsterAllyIndex]->entity); + monsterMoveAside(this, players[playerLeader]->entity); handleNPCInteractDialogue(*myStats, ALLY_EVENT_MOVEASIDE); break; case ALLY_CMD_DEFEND: @@ -11405,11 +12735,11 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui { if ( skillLVL < SKILL_LEVEL_BASIC ) { - messagePlayerColor(monsterAllyIndex, MESSAGE_STATUS, 0xFFFFFFFF, Language::get(3680)); + messagePlayerColor(playerLeader, MESSAGE_STATUS, 0xFFFFFFFF, Language::get(3680)); } else { - messagePlayerColor(monsterAllyIndex, MESSAGE_STATUS, 0xFFFFFFFF, Language::get(3679)); + messagePlayerColor(playerLeader, MESSAGE_STATUS, 0xFFFFFFFF, Language::get(3679)); } monsterAllyPickupItems = ALLY_GYRO_DETECT_NONE; } @@ -11464,16 +12794,16 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui if ( !droppedSomething ) { - messagePlayer(monsterAllyIndex, MESSAGE_HINT, Language::get(3868)); + messagePlayer(playerLeader, MESSAGE_HINT, Language::get(3868)); } } - else if ( stats[monsterAllyIndex] ) + else if ( stats[playerLeader] ) { Entity* dropped = nullptr; bool confirmDropped = false; bool dropWeaponOnly = false; bool unableToDrop = false; - Uint32 owner = players[monsterAllyIndex]->entity->getUID(); + Uint32 owner = players[playerLeader]->entity->getUID(); if ( skillLVL >= SKILL_LEVEL_MASTER ) { if ( myStats->helmet ) @@ -11630,10 +12960,10 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui handleNPCInteractDialogue(*myStats, ALLY_EVENT_DROP_WEAPON); } } - /*if ( unableToDrop ) + if ( unableToDrop ) { handleNPCInteractDialogue(*myStats, ALLY_EVENT_DROP_FAILED); - }*/ + } } break; case ALLY_CMD_MOVETO_CONFIRM: @@ -11670,10 +13000,10 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui } case ALLY_CMD_GYRO_RETURN: { - if ( players[monsterAllyIndex]->entity ) + if ( players[playerLeader]->entity ) { - destX = static_cast(players[monsterAllyIndex]->entity->x) >> 4; - destY = static_cast(players[monsterAllyIndex]->entity->y) >> 4; + destX = static_cast(players[playerLeader]->entity->x) >> 4; + destY = static_cast(players[playerLeader]->entity->y) >> 4; bool noground = false; const int monsterX = static_cast(x) >> 4; @@ -11687,7 +13017,7 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui } } - if ( entityDist(this, players[monsterAllyIndex]->entity) < STRIKERANGE + if ( entityDist(this, players[playerLeader]->entity) < STRIKERANGE && !noground ) { monsterSpecialState = GYRO_RETURN_LANDING; @@ -11715,7 +13045,7 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui else { //messagePlayer(0, "no path to destination"); - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFF, *myStats, Language::get(4317), Language::get(4317), MSG_GENERIC); + messagePlayerMonsterEvent(playerLeader, 0xFFFFFF, *myStats, Language::get(4317), Language::get(4317), MSG_GENERIC); } } break; @@ -11724,23 +13054,30 @@ void Entity::monsterAllySendCommand(int command, int destX, int destY, Uint32 ui { if ( monsterAllySpecialCooldown == 0 ) { - int duration = TICKS_PER_SECOND * (60); - if ( myStats->HP < myStats->MAXHP && setEffect(EFF_ASLEEP, true, duration, false) ) // 60 seconds of sleep. + if ( myStats->getEffectActive(EFF_COMMAND) ) { - setEffect(EFF_HP_REGEN, true, duration, false); - monsterAllySpecial = ALLY_SPECIAL_CMD_REST; - monsterAllySpecialCooldown = -1; // locked out until next floor. - serverUpdateEntitySkill(this, 49); - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFF, *myStats, Language::get(398), Language::get(397), MSG_COMBAT); - if ( players[monsterAllyIndex] && players[monsterAllyIndex]->entity - && myStats->HP < myStats->MAXHP && local_rng.rand() % 3 == 0 ) - { - players[monsterAllyIndex]->entity->increaseSkill(PRO_LEADERSHIP); - } + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, *myStats, Language::get(514), Language::get(515), MSG_COMBAT); } else { - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFF, *myStats, Language::get(3880), Language::get(3880), MSG_GENERIC); + int duration = TICKS_PER_SECOND * (60); + if ( myStats->HP < myStats->MAXHP && setEffect(EFF_ASLEEP, true, duration, false) ) // 60 seconds of sleep. + { + setEffect(EFF_HP_REGEN, true, duration, false); + monsterAllySpecial = ALLY_SPECIAL_CMD_REST; + monsterAllySpecialCooldown = -1; // locked out until next floor. + serverUpdateEntitySkill(this, 49); + messagePlayerMonsterEvent(playerLeader, 0xFFFFFF, *myStats, Language::get(398), Language::get(397), MSG_COMBAT); + if ( players[playerLeader] && players[playerLeader]->entity + && myStats->HP < myStats->MAXHP && local_rng.rand() % 3 == 0 ) + { + players[playerLeader]->entity->increaseSkill(PRO_LEADERSHIP); + } + } + else + { + messagePlayerMonsterEvent(playerLeader, 0xFFFFFF, *myStats, Language::get(3880), Language::get(3880), MSG_GENERIC); + } } } break; @@ -11849,7 +13186,7 @@ void Entity::clearMonsterInteract() interactedByMonster = 0; } -bool Entity::monsterSetPathToLocation(int destX, int destY, int adjacentTilesToCheck, int pathingType, bool tryRandomSpot) +bool Entity::monsterSetPathToLocation(int destX, int destY, int adjacentTilesToCheck, int pathingType, bool tryRandomSpot, bool shortByShortest) { int u, v; bool foundplace = false; @@ -11858,8 +13195,15 @@ bool Entity::monsterSetPathToLocation(int destX, int destY, int adjacentTilesToC if ( static_cast(x / 16) == destX && static_cast(y / 16) == destY ) { - monsterAllyFormations.updateOnPathSucceed(getUID(), this); - return true; // we're trying to move to the spot we're already at! + if ( getStats() && getStats()->type == DUCK_SMALL && tryRandomSpot ) + { + // allow small movements + } + else + { + monsterAllyFormations.updateOnPathSucceed(getUID(), this); + return true; // we're trying to move to the spot we're already at! + } } else if ( !checkObstacle((destX << 4) + 8, (destY << 4) + 8, this, nullptr) ) { @@ -11893,9 +13237,31 @@ bool Entity::monsterSetPathToLocation(int destX, int destY, int adjacentTilesToC if ( !possibleDestinations.empty() ) { // sort by distance from monster, first result is shortest path. - std::sort(possibleDestinations.begin(), possibleDestinations.end()); - pathToX = possibleDestinations.at(0).second.first; - pathToY = possibleDestinations.at(0).second.second; + if ( shortByShortest ) + { + std::sort(possibleDestinations.begin(), possibleDestinations.end()); + pathToX = possibleDestinations.at(0).second.first; + pathToY = possibleDestinations.at(0).second.second; + } + else + { + int pick = local_rng.rand() % possibleDestinations.size(); + pathToX = possibleDestinations.at(pick).second.first; + pathToY = possibleDestinations.at(pick).second.second; + if ( getStats() && getStats()->type == DUCK_SMALL ) + { + if ( pathToX == destX && pathToY == destY ) + { + // re-roll + if ( local_rng.rand() % 4 > 0 ) + { + pick = local_rng.rand() % possibleDestinations.size(); + pathToX = possibleDestinations.at(pick).second.first; + pathToY = possibleDestinations.at(pick).second.second; + } + } + } + } foundplace = true; } @@ -11998,11 +13364,20 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) { return; } - if ( monsterAllyIndex < 0 || monsterAllyIndex >= MAXPLAYERS ) + + int playerLeader = monsterAllyIndex; + bool compelSpell = false; + if ( myStats.getEffectActive(EFF_COMMAND) >= 1 && myStats.getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + playerLeader = myStats.getEffectActive(EFF_COMMAND) - 1; + compelSpell = true; + } + + if ( playerLeader < 0 || playerLeader >= MAXPLAYERS ) { return; } - if ( !stats[monsterAllyIndex] ) + if ( !stats[playerLeader] ) { return; } @@ -12026,9 +13401,9 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) message = Language::get(3077 + local_rng.rand() % 2); break; case ALLY_EVENT_INTERACT_ITEM_CURSED: - if ( FollowerMenu[monsterAllyIndex].entityToInteractWith && FollowerMenu[monsterAllyIndex].entityToInteractWith->behavior == &actItem ) + if ( FollowerMenu[playerLeader].entityToInteractWith && FollowerMenu[playerLeader].entityToInteractWith->behavior == &actItem ) { - Item* item = newItemFromEntity(FollowerMenu[monsterAllyIndex].entityToInteractWith); + Item* item = newItemFromEntity(FollowerMenu[playerLeader].entityToInteractWith); if ( item ) { char fullmsg[256]; @@ -12172,13 +13547,13 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) for ( int c = 0; c < MAXPLAYERS; ++c ) { players[c]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), - Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_ATTACK, message.c_str(), stats[monsterAllyIndex]->name); + Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_ATTACK, message.c_str(), stats[playerLeader]->name); } } else { - players[monsterAllyIndex]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), - Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_FOLLOWER_CMD, message.c_str(), stats[monsterAllyIndex]->name); + players[playerLeader]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), + Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_FOLLOWER_CMD, message.c_str(), stats[playerLeader]->name); } } } @@ -12192,9 +13567,9 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) case ALLY_EVENT_MOVEASIDE: if ( local_rng.getU8() % 8 == 0 ) { - players[monsterAllyIndex]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), + players[playerLeader]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_FOLLOWER_CMD, Language::get(getMonsterInteractGreeting(myStats)), stats[monsterAllyIndex]->name); - //messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + //messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, // myStats, Language::get(3129), Language::get(3130), MSG_COMBAT); } break; @@ -12207,19 +13582,19 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) case ALLY_EVENT_MOVETO_FAIL: if ( local_rng.rand() % 2 == 0 ) { - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, Language::get(3131), Language::get(3132), MSG_COMBAT); } else { - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, Language::get(3133), Language::get(3134), MSG_COMBAT); } break; case ALLY_EVENT_INTERACT_ITEM_CURSED: - if ( FollowerMenu[monsterAllyIndex].entityToInteractWith && FollowerMenu[monsterAllyIndex].entityToInteractWith->behavior == &actItem ) + if ( FollowerMenu[playerLeader].entityToInteractWith && FollowerMenu[playerLeader].entityToInteractWith->behavior == &actItem ) { - Item* item = newItemFromEntity(FollowerMenu[monsterAllyIndex].entityToInteractWith); + Item* item = newItemFromEntity(FollowerMenu[playerLeader].entityToInteractWith); if ( item ) { char fullmsg[256] = ""; @@ -12267,7 +13642,7 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) char namedStr[128]; strcpy(namedStr, Language::get(3120)); strcat(namedStr, fullmsg); - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, genericStr, namedStr, MSG_COMBAT); } } @@ -12280,7 +13655,7 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) case ALLY_EVENT_INTERACT_ITEM_NOUSE: case ALLY_EVENT_INTERACT_ITEM_FOOD_FULL: { - Item* item = newItemFromEntity(FollowerMenu[monsterAllyIndex].entityToInteractWith); + Item* item = newItemFromEntity(FollowerMenu[playerLeader].entityToInteractWith); if ( item ) { char itemString[64] = ""; @@ -12289,7 +13664,7 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) strcat(genericStr, itemString); strcpy(namedStr, Language::get(3122)); strcat(namedStr, itemString); - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, genericStr, namedStr, MSG_COMBAT); free(item); } @@ -12299,7 +13674,7 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) case ALLY_EVENT_INTERACT_ITEM_FOOD_GOOD: case ALLY_EVENT_INTERACT_ITEM_FOOD_ROTTEN: { - Item* item = newItemFromEntity(FollowerMenu[monsterAllyIndex].entityToInteractWith); + Item* item = newItemFromEntity(FollowerMenu[playerLeader].entityToInteractWith); if ( item ) { char itemString[64] = ""; @@ -12308,7 +13683,7 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) strcat(genericStr, itemString); strcpy(namedStr, Language::get(3122)); strcat(namedStr, itemString); - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, genericStr, namedStr, MSG_COMBAT); free(item); } @@ -12326,7 +13701,7 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) strcat(genericStr, Language::get(3125)); strcpy(namedStr, Language::get(3122)); strcat(namedStr, Language::get(3125)); - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, genericStr, namedStr, MSG_COMBAT); break; case ALLY_EVENT_DROP_EQUIP: @@ -12334,7 +13709,7 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) strcat(genericStr, Language::get(3126)); strcpy(namedStr, Language::get(3122)); strcat(namedStr, Language::get(3126)); - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, genericStr, namedStr, MSG_COMBAT); break; case ALLY_EVENT_DROP_ALL: @@ -12342,30 +13717,38 @@ void Entity::handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event) strcat(genericStr, Language::get(3127)); strcpy(namedStr, Language::get(3122)); strcat(namedStr, Language::get(3127)); - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, + myStats, genericStr, namedStr, MSG_COMBAT); + break; + case ALLY_EVENT_DROP_FAILED: + strcpy(genericStr, Language::get(3121)); + strcat(genericStr, Language::get(6970)); + strcpy(namedStr, Language::get(3122)); + strcat(namedStr, Language::get(6970)); + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, genericStr, namedStr, MSG_COMBAT); break; case ALLY_EVENT_WAIT: if ( myStats.type == SENTRYBOT || myStats.type == SPELLBOT ) { - messagePlayerColor(monsterAllyIndex, MESSAGE_STATUS, 0xFFFFFFFF, + messagePlayerColor(playerLeader, MESSAGE_STATUS, 0xFFFFFFFF, Language::get(3676), monstertypename[myStats.type]); } else { - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, Language::get(3105), Language::get(3106), MSG_COMBAT); } break; case ALLY_EVENT_FOLLOW: if ( myStats.type == SENTRYBOT || myStats.type == SPELLBOT ) { - messagePlayerColor(monsterAllyIndex, MESSAGE_STATUS, 0xFFFFFFFF, + messagePlayerColor(playerLeader, MESSAGE_STATUS, 0xFFFFFFFF, Language::get(3677), monstertypename[myStats.type]); } else { - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, + messagePlayerMonsterEvent(playerLeader, 0xFFFFFFFF, myStats, Language::get(529), Language::get(3128), MSG_COMBAT); } break; @@ -12388,6 +13771,14 @@ int Entity::shouldMonsterDefend(Stat& myStats, const Entity& target, const Stat& return MONSTER_DEFEND_NONE; } + if ( myStats.type == EARTH_ELEMENTAL ) + { + if ( local_rng.rand() % 20 < 8 ) + { + return MONSTER_DEFEND_HOLD; + } + } + if ( !myStats.shield ) { return MONSTER_DEFEND_NONE; @@ -12415,7 +13806,7 @@ int Entity::shouldMonsterDefend(Stat& myStats, const Entity& target, const Stat& bool isPlayerAlly = (monsterAllyIndex >= 0 && monsterAllyIndex < MAXPLAYERS); - if ( !(isPlayerAlly || myStats.type == HUMAN || myStats.type == BUGBEAR) ) + if ( !(isPlayerAlly || myStats.type == HUMAN || myStats.type == BUGBEAR || myStats.type == GREMLIN || myStats.type == GOATMAN) ) { return MONSTER_DEFEND_NONE; } @@ -12619,6 +14010,15 @@ bool Entity::monsterConsumeFoodEntity(Entity* food, Stat* myStats) switch ( item->type ) { case FOOD_APPLE: + case FOOD_NUT: + case FOOD_SHROOM: + case FOOD_RATION: + case FOOD_RATION_SPICY: + case FOOD_RATION_SOUR: + case FOOD_RATION_BITTER: + case FOOD_RATION_HEARTY: + case FOOD_RATION_HERBAL: + case FOOD_RATION_SWEET: heal = 5 + item->beatitude; buffDuration = std::min(buffDuration, 2 * TICKS_PER_SECOND); myStats->HUNGER += 200; @@ -12682,7 +14082,7 @@ bool Entity::monsterConsumeFoodEntity(Entity* food, Stat* myStats) if ( buffDuration > 0 ) { - myStats->EFFECTS[EFF_HP_REGEN] = true; + myStats->setEffectActive(EFF_HP_REGEN, 1); myStats->EFFECTS_TIMERS[EFF_HP_REGEN] = buffDuration; } @@ -12711,7 +14111,7 @@ bool Entity::monsterConsumeFoodEntity(Entity* food, Stat* myStats) return foodEntityConsumed; } -Entity* Entity::monsterAllyGetPlayerLeader() +Entity* Entity::monsterAllyGetPlayerLeader() const { if ( behavior != &actMonster ) { @@ -12847,6 +14247,15 @@ bool Entity::monsterAllyEquipmentInClass(const Item& item) const case IRON_BREASTPIECE: case IRON_HELM: case IRON_SHIELD: + case BONE_BREASTPIECE: + case BLACKIRON_BREASTPIECE: + case SILVER_BREASTPIECE: + case SCUTUM: + case BLACKIRON_SHIELD: + case SILVER_SHIELD: + case BONE_HELM: + case BLACKIRON_HELM: + case SILVER_HELM: return false; break; default: @@ -12949,8 +14358,8 @@ void Entity::monsterHandleKnockbackVelocity(real_t monsterFacingTangent, real_t { // this function makes the monster accelerate to running forwards or 0 movement speed after being knocked back. // vel_x, vel_y are set on knockback impact and this slowly accumulates speed from the knocked back movement by a factor of monsterKnockbackVelocity. - real_t maxVelX = cos(monsterFacingTangent) * .045 * (monsterGetDexterityForMovement() + 10) * weightratio; - real_t maxVelY = sin(monsterFacingTangent) * .045 * (monsterGetDexterityForMovement() + 10) * weightratio; + real_t maxVelX = cos(monsterFacingTangent) * .045 * (std::max(0, monsterGetDexterityForMovement()) + 10) * weightratio; + real_t maxVelY = sin(monsterFacingTangent) * .045 * (std::max(0, monsterGetDexterityForMovement()) + 10) * weightratio; bool mobile = ((monsterState == MONSTER_STATE_WAIT) || isMobile()); // if immobile, the intended max speed is 0 (stopped). if ( maxVelX > 0 ) @@ -12969,7 +14378,14 @@ void Entity::monsterHandleKnockbackVelocity(real_t monsterFacingTangent, real_t { this->vel_y = std::max(this->vel_y + (this->monsterKnockbackVelocity * maxVelY), mobile ? maxVelY : 0.0); } - this->monsterKnockbackVelocity *= 1.1; + if ( getStats() && getStats()->getEffectActive(EFF_MAGIC_GREASE) ) + { + //this->monsterKnockbackVelocity *= 1.005; + } + else + { + this->monsterKnockbackVelocity *= 1.1; + } } int Entity::monsterGetDexterityForMovement() @@ -12982,7 +14398,7 @@ int Entity::monsterGetDexterityForMovement() Stat* myStats = getStats(); if ( myStats ) { - if ( myStats->EFFECTS[EFF_DASH] ) + if ( myStats->getEffectActive(EFF_DASH) ) { myDex += 30; } @@ -13239,9 +14655,26 @@ void Entity::monsterGenerateQuiverItem(Stat* myStats, bool lesserMonster) int Entity::getMonsterEffectiveDistanceOfRangedWeapon(Item* weapon) { + int distance = 160; + if ( Stat* myStats = getStats() ) + { + if ( myStats->type == DRYAD ) + { + if ( myStats->getAttribute("monster_d_type") == "watcher" ) + { + distance = 64; + if ( monsterStrafeDirection > 0 && monsterSpecialState == 0 ) + { + distance = STRIKERANGE; + } + return distance; + } + } + } + if ( !weapon ) { - return 160; + return distance; } if ( getMonsterTypeFromSprite() == SENTRYBOT ) @@ -13253,15 +14686,17 @@ int Entity::getMonsterEffectiveDistanceOfRangedWeapon(Item* weapon) return sightranges[SPELLBOT]; } - int distance = 160; switch ( weapon->type ) { case SLING: case CROSSBOW: case HEAVY_CROSSBOW: + case BLACKIRON_CROSSBOW: distance = 100; break; case LONGBOW: + case BRANCH_BOW: + case BRANCH_BOW_INFECTED: distance = 200; break; default: @@ -13321,6 +14756,43 @@ bool Entity::isUntargetableBat(real_t* outDist) const return false; } +bool Entity::monsterIsTargetable(bool targetInertMimics) const +{ + if ( flags[PASSABLE] ) + { + return false; + } + if ( behavior == &actPlayer ) + { + return true; + } + else if ( behavior == &actMonster ) + { + Monster type = getMonsterTypeFromSprite(); + if ( type == GYROBOT ) + { + return false; + } + else if ( type == BAT_SMALL && isUntargetableBat() ) + { + return false; + } + else if ( type == MOTH_SMALL && getStats() && getStats()->getAttribute("fire_sprite") != "" ) + { + return false; + } + else if ( type == DUCK_SMALL ) + { + return false; + } + else if ( type == MIMIC && !targetInertMimics && isInertMimic() ) + { + return false; + } + } + return true; +} + void batResetIdle(Entity* my) { if ( !my ) { return; } @@ -13439,3 +14911,512 @@ void mimicResetIdle(Entity* my) my->monsterLookDir = (PI / 2) * (local_rng.rand() % 4); } } + +static ConsoleVariable cvar_monster_debug_models("/monster_debug_models", false); +bool monsterDebugModels(Entity* my, real_t* dist) +{ +#ifdef NDEBUG + return false; +#endif + if ( !*cvar_monster_debug_models ) + { + return false; + } + static Uint32 thisTick = 0; + if ( thisTick == ticks ) + { + return false; + } + thisTick = ticks; + if ( !my ) + { + return false; + } + Stat* myStats = my->getStats(); + if ( !myStats ) + { + return false; + } +#ifndef NDEBUG + static bool forceWalk = false; + if ( keystatus[SDLK_KP_5] ) + { + keystatus[SDLK_KP_5] = 0; + forceWalk = !forceWalk; + } + if ( keystatus[SDLK_KP_6] ) + { + keystatus[SDLK_KP_6] = 0; + myStats->setEffectValueUnsafe(EFF_STUNNED, myStats->getEffectActive(EFF_STUNNED) ? 0 : 1); + } + if ( keystatus[SDLK_KP_PLUS] ) + { + my->fskill[0] += 0.05; + } + if ( keystatus[SDLK_KP_MINUS] ) + { + my->fskill[0] -= 0.05; + } + if ( forceWalk ) + { + if ( dist ) + { + *dist = 0.15; + } + } + + if ( keystatus[SDLK_9] ) + { + keystatus[SDLK_9] = 0; + if ( myStats->helmet ) + { + myStats->helmet->appearance++; + } + } + if ( keystatus[SDLK_0] ) + { + keystatus[SDLK_0] = 0; + if ( myStats->mask ) + { + myStats->mask->appearance++; + } + } + + if ( keystatus[SDLK_KP_7] ) + { + keystatus[SDLK_KP_7] = 0; + my->attack(my->getAttackPose(), 0, nullptr); + } + + if ( keystatus[SDLK_6] ) + { + keystatus[SDLK_6] = 0; + if ( myStats->weapon ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + while ( true ) + { + int type = myStats->weapon->type; + type--; + if ( type < 0 ) + { + type = NUMITEMS - 1; + } + myStats->weapon->type = (ItemType)type; + if ( items[myStats->weapon->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_WEAPON ) + { + break; + } + } + } + else + { + while ( true ) + { + int type = myStats->weapon->type; + type++; + if ( type >= NUMITEMS ) + { + if ( myStats->weapon->node ) + { + list_RemoveNode(myStats->weapon->node); + } + else + { + free(myStats->weapon); + } + myStats->weapon = nullptr; + break; + } + myStats->weapon->type = (ItemType)type; + if ( items[myStats->weapon->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_WEAPON ) + { + break; + } + } + } + } + else + { + myStats->weapon = newItem(QUARTERSTAFF, EXCELLENT, 0, 1, 0, true, nullptr); + } + } + if ( keystatus[SDLK_5] ) + { + keystatus[SDLK_5] = 0; + if ( myStats->shield ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + while ( true ) + { + int type = myStats->shield->type; + type--; + if ( type < 0 ) + { + type = NUMITEMS - 1; + } + myStats->shield->type = (ItemType)type; + if ( items[myStats->shield->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_SHIELD ) + { + break; + } + } + } + else + { + while ( true ) + { + int type = myStats->shield->type; + type++; + if ( type >= NUMITEMS ) + { + if ( myStats->shield->node ) + { + list_RemoveNode(myStats->shield->node); + } + else + { + free(myStats->shield); + } + myStats->shield = nullptr; + break; + } + myStats->shield->type = (ItemType)type; + if ( items[myStats->shield->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_SHIELD ) + { + break; + } + } + } + } + else + { + myStats->shield = newItem(WOODEN_SHIELD, EXCELLENT, 0, 1, 0, true, nullptr); + } + } + if ( keystatus[SDLK_7] ) + { + keystatus[SDLK_7] = 0; + if ( myStats->shoes ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + while ( true ) + { + int type = myStats->shoes->type; + type--; + if ( type < 0 ) + { + type = NUMITEMS - 1; + } + myStats->shoes->type = (ItemType)type; + if ( items[myStats->shoes->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BOOTS ) + { + break; + } + } + } + else + { + while ( true ) + { + int type = myStats->shoes->type; + type++; + if ( type >= NUMITEMS ) + { + if ( myStats->shoes->node ) + { + list_RemoveNode(myStats->shoes->node); + } + else + { + free(myStats->shoes); + } + myStats->shoes = nullptr; + break; + } + myStats->shoes->type = (ItemType)type; + if ( items[myStats->shoes->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BOOTS ) + { + break; + } + } + } + } + else + { + myStats->shoes = newItem(LEATHER_BOOTS, EXCELLENT, 0, 1, 0, true, nullptr); + } + } + if ( keystatus[SDLK_8] ) + { + keystatus[SDLK_8] = 0; + if ( myStats->gloves ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + while ( true ) + { + int type = myStats->gloves->type; + type--; + if ( type < 0 ) + { + type = NUMITEMS - 1; + } + myStats->gloves->type = (ItemType)type; + if ( items[myStats->gloves->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_GLOVES ) + { + break; + } + } + } + else + { + while ( true ) + { + int type = myStats->gloves->type; + type++; + if ( type >= NUMITEMS ) + { + if ( myStats->gloves->node ) + { + list_RemoveNode(myStats->gloves->node); + } + else + { + free(myStats->gloves); + } + myStats->gloves = nullptr; + break; + } + myStats->gloves->type = (ItemType)type; + if ( items[myStats->gloves->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_GLOVES ) + { + break; + } + } + } + } + else + { + myStats->gloves = newItem(GLOVES, EXCELLENT, 0, 1, 0, true, nullptr); + } + } + if ( keystatus[SDLK_2] ) + { + keystatus[SDLK_2] = 0; + if ( myStats->helmet ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + while ( true ) + { + int type = myStats->helmet->type; + type--; + if ( type < 0 ) + { + type = NUMITEMS - 1; + } + myStats->helmet->type = (ItemType)type; + if ( items[myStats->helmet->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_HELM ) + { + break; + } + } + } + else + { + while ( true ) + { + int type = myStats->helmet->type; + type++; + if ( type >= NUMITEMS ) + { + type = 0; + } + myStats->helmet->type = (ItemType)type; + if ( items[myStats->helmet->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_HELM ) + { + break; + } + } + } + } + else + { + myStats->helmet = newItem(LEATHER_HELM, EXCELLENT, 0, 1, 0, true, nullptr); + } + } + if ( keystatus[SDLK_4] ) + { + keystatus[SDLK_4] = 0; + if ( myStats->breastplate ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + while ( true ) + { + int type = myStats->breastplate->type; + type--; + if ( type < 0 ) + { + type = NUMITEMS - 1; + } + myStats->breastplate->type = (ItemType)type; + if ( items[myStats->breastplate->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BREASTPLATE ) + { + break; + } + } + } + else + { + while ( true ) + { + int type = myStats->breastplate->type; + type++; + if ( type >= NUMITEMS ) + { + if ( myStats->breastplate->node ) + { + list_RemoveNode(myStats->breastplate->node); + } + else + { + free(myStats->breastplate); + } + myStats->breastplate = nullptr; + break; + } + myStats->breastplate->type = (ItemType)type; + if ( items[myStats->breastplate->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BREASTPLATE ) + { + break; + } + } + } + } + else + { + myStats->breastplate = newItem(LEATHER_BREASTPIECE, EXCELLENT, 0, 1, 0, true, nullptr); + } + } + if ( keystatus[SDLK_1] ) + { + keystatus[SDLK_1] = 0; + if ( myStats->mask ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + while ( true ) + { + int type = myStats->mask->type; + type--; + if ( type < 0 ) + { + type = NUMITEMS - 1; + } + myStats->mask->type = (ItemType)type; + if ( items[myStats->mask->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_MASK ) + { + break; + } + } + } + else + { + while ( true ) + { + int type = myStats->mask->type; + type++; + if ( type >= NUMITEMS ) + { + type = 0; + } + myStats->mask->type = (ItemType)type; + if ( items[myStats->mask->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_MASK ) + { + break; + } + } + } + } + else + { + myStats->mask = newItem(GEM_GLASS, EXCELLENT, 0, 1, 0, true, nullptr); + } + } + + if ( keystatus[SDLK_MINUS] ) + { + keystatus[SDLK_MINUS] = 0; + if ( myStats->cloak ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + while ( true ) + { + int type = myStats->cloak->type; + type--; + if ( type < 0 ) + { + type = NUMITEMS - 1; + } + myStats->cloak->type = (ItemType)type; + if ( items[myStats->cloak->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_CLOAK ) + { + break; + } + } + } + else + { + while ( true ) + { + int type = myStats->cloak->type; + type++; + if ( type >= NUMITEMS ) + { + type = 0; + } + myStats->cloak->type = (ItemType)type; + if ( items[myStats->cloak->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_CLOAK ) + { + break; + } + } + } + } + else + { + myStats->cloak = newItem(CLOAK, EXCELLENT, 0, 1, 0, true, nullptr); + } + } + +#endif + return true; +} + +bool Entity::monsterCanTradeWith(int player) const +{ + if ( behavior == &actMonster ) + { + Monster type = NOTHING; + if ( Stat* myStats = getStats() ) + { + type = myStats->type; + } + else + { + type = getMonsterTypeFromSprite(); + } + + /*if ( type == AUTOMATON || type == KOBOLD ) + { + return true; + }*/ + } + return false; +} \ No newline at end of file diff --git a/src/actpedestal.cpp b/src/actpedestal.cpp index 2fde2db46..60573b915 100644 --- a/src/actpedestal.cpp +++ b/src/actpedestal.cpp @@ -230,14 +230,14 @@ void Entity::actPedestalBase() switch ( pedestalOrbType ) { case 1: // blue - if ( stats[i] && !stats[i]->EFFECTS[EFF_SHRINE_BLUE_BUFF] ) + if ( stats[i] && !stats[i]->getEffectActive(EFF_SHRINE_BLUE_BUFF) ) { messagePlayer(i, MESSAGE_INTERACTION, Language::get(2910)); } players[i]->entity->setEffect(EFF_SHRINE_BLUE_BUFF, true, 1000, false); break; case 2: // red - if ( stats[i] && !stats[i]->EFFECTS[EFF_SHRINE_RED_BUFF] ) + if ( stats[i] && !stats[i]->getEffectActive(EFF_SHRINE_RED_BUFF) ) { messagePlayer(i, MESSAGE_INTERACTION, Language::get(2904)); } @@ -246,7 +246,7 @@ void Entity::actPedestalBase() case 3: break; case 4: // green - if ( stats[i] && !stats[i]->EFFECTS[EFF_SHRINE_GREEN_BUFF] ) + if ( stats[i] && !stats[i]->getEffectActive(EFF_SHRINE_GREEN_BUFF) ) { messagePlayer(i, MESSAGE_INTERACTION, Language::get(2909)); } diff --git a/src/actplayer.cpp b/src/actplayer.cpp index 82b2bf4ea..8ed94c926 100644 --- a/src/actplayer.cpp +++ b/src/actplayer.cpp @@ -43,6 +43,9 @@ bool partymode = false; static ConsoleVariable cvar_calloutStartZ("/callout_start_z", -2.5); static ConsoleVariable cvar_calloutMoveTo("/callout_moveto_z", 0.1); static ConsoleVariable cvar_calloutStartZLimit("/callout_start_z_limit", 7.5); +static ConsoleVariable cvar_followerStartZ("/follower_start_z", -2.5); +static ConsoleVariable cvar_followerMoveTo("/follower_moveto_z", 0.1); +static ConsoleVariable cvar_followerStartZLimit("/follower_start_z_limit", 7.5); /*------------------------------------------------------------------------------- @@ -62,6 +65,8 @@ static ConsoleVariable cvar_calloutStartZLimit("/callout_start_z_limit", #define GHOSTCAM_DEACTIVATED my->skill[7] #define GHOSTCAM_SPAWN_ANIM my->skill[8] #define GHOSTCAM_SPAWN_ANIM_COMPLETE my->skill[9] +#define GHOSTCAM_COSMETIC_SPRITE my->skill[10] +#define GHOSTCAM_SPIRIT_HIGH_PROFILE my->skill[11] #define GHOSTCAM_BOB my->fskill[0] #define GHOSTCAM_BOBMOVE my->fskill[1] #define GHOSTCAM_DX my->fskill[3] @@ -73,6 +78,7 @@ static ConsoleVariable cvar_calloutStartZLimit("/callout_start_z_limit", #define GHOSTCAM_SQUISH_ANGLE my->fskill[9] #define GHOSTCAM_WEAVE my->fskill[10] #define GHOSTCAM_HOVER my->fskill[11] +#define GHOSTCAM_THIRD_PERSON_CUSTOM my->fskill[12] static ConsoleVariable cvar_player_can_move_in_gui("/player_can_move_in_gui", true); bool playerAllowedMovement(const int playernum) @@ -115,20 +121,34 @@ void Player::Ghost_t::handleGhostCameraBobbing(bool useRefreshRateDelta) && ((input.binary("Move Forward") || input.binary("Move Backward")) || (input.binary("Move Left") - input.binary("Move Right")))) || (inputs.hasController(playernum) - && (inputs.getController(playernum)->getLeftXPercentForPlayerMovement() - || inputs.getController(playernum)->getLeftYPercentForPlayerMovement()))) ) + && (inputs.getController(playernum)->getLeftXPercentForPlayerMovement(playernum) + || inputs.getController(playernum)->getLeftYPercentForPlayerMovement(playernum)))) ) { if ( !player.usingCommand() && player.bControlEnabled ) { - if ( !GHOSTCAM_SNEAKING ) + if ( player.ghost.isSpiritGhost() ) { - GHOSTCAM_BOBMOVE += 0.0125 * *cvar_ghostBobSpeed; + if ( GHOSTCAM_SPIRIT_HIGH_PROFILE ) + { + reset = true; + } + else + { + GHOSTCAM_BOBMOVE += 0.0125 * *cvar_ghostBobSpeed; + } } else { - //GHOSTCAM_BOBMOVE += 0.025; - reset = true; + if ( !GHOSTCAM_SNEAKING ) + { + GHOSTCAM_BOBMOVE += 0.0125 * *cvar_ghostBobSpeed; + } + else + { + //GHOSTCAM_BOBMOVE += 0.025; + reset = true; + } } } else @@ -180,7 +200,7 @@ void Player::Ghost_t::handleGhostMovement(const bool useRefreshRateDelta) bool allowMovement = isControllable() && playerAllowedMovement(player.playernum); static ConsoleVariable cvar_ghostSpeed("/ghost_speed", 1.5); static ConsoleVariable cvar_ghostDrag("/ghost_drag", 0.95); - + real_t drag = *cvar_ghostDrag; if ( ((!player.usingCommand() && player.bControlEnabled && !gamePaused)) && allowMovement ) { @@ -199,12 +219,12 @@ void Player::Ghost_t::handleGhostMovement(const bool useRefreshRateDelta) if ( inputs.hasController(player.playernum) /*&& !input.binary("Move Left") && !input.binary("Move Right")*/ ) { - x_force = inputs.getController(player.playernum)->getLeftXPercentForPlayerMovement(); + x_force = inputs.getController(player.playernum)->getLeftXPercentForPlayerMovement(player.playernum); } if ( inputs.hasController(player.playernum) /*&& !input.binary("Move Forward") && !input.binary("Move Backward")*/ ) { - y_force = inputs.getController(player.playernum)->getLeftYPercentForPlayerMovement(); + y_force = inputs.getController(player.playernum)->getLeftYPercentForPlayerMovement(player.playernum); if ( y_force < 0 ) { y_force *= backpedalMultiplier; //Move backwards more slowly. @@ -212,16 +232,43 @@ void Player::Ghost_t::handleGhostMovement(const bool useRefreshRateDelta) } } - real_t speedFactor = *cvar_ghostSpeed;// getSpeedFactor(weightratio, statGetDEX(stats[PLAYER_NUM], players[PLAYER_NUM]->entity)); + real_t speedFactor = *cvar_ghostSpeed; + + int speedMult = 1; + bool isSpirit = isSpiritGhost(); + if ( !isSpirit ) + { + speedMult += GHOSTCAM_SNEAKING; + } + else + { + if ( node_t* node = list_Node(&my->children, 2) ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( Entity::getMonsterTypeFromSprite(entity->sprite) == DUCK_SMALL ) + { + if ( my->monsterSpecialState == DUCK_INERT ) + { + speedMult += 1; + drag = 0.9; + speedFactor = 3.0; + } + } + } + } + } + speedFactor *= refreshRateDelta; - my->vel_x += y_force * cos(my->yaw) * .045 * speedFactor / (1 + GHOSTCAM_SNEAKING); - my->vel_y += y_force * sin(my->yaw) * .045 * speedFactor / (1 + GHOSTCAM_SNEAKING); - my->vel_x += x_force * cos(my->yaw + PI / 2) * .0225 * speedFactor / (1 + GHOSTCAM_SNEAKING); - my->vel_y += x_force * sin(my->yaw + PI / 2) * .0225 * speedFactor / (1 + GHOSTCAM_SNEAKING); + + my->vel_x += y_force * cos(my->yaw) * .045 * speedFactor / (speedMult); + my->vel_y += y_force * sin(my->yaw) * .045 * speedFactor / (speedMult); + my->vel_x += x_force * cos(my->yaw + PI / 2) * .0225 * speedFactor / (speedMult); + my->vel_y += x_force * sin(my->yaw + PI / 2) * .0225 * speedFactor / (speedMult); } - my->vel_x *= pow(*cvar_ghostDrag, refreshRateDelta); - my->vel_y *= pow(*cvar_ghostDrag, refreshRateDelta); + my->vel_x *= pow(drag, refreshRateDelta); + my->vel_y *= pow(drag, refreshRateDelta); /*for ( int i = 0; i < MAXPLAYERS; ++i ) { @@ -371,13 +418,15 @@ bool Player::Ghost_t::allowedInteractEntity(Entity& entity) { if ( entity.behavior == &actItem || entity.behavior == &actDoor + || entity.behavior == &actIronDoor || entity.behavior == &actSwitch || entity.behavior == &actSwitchWithTimer || entity.behavior == &actPowerCrystal || entity.behavior == &actPowerCrystalBase || entity.behavior == &actTeleportShrine || entity.behavior == &::actDaedalusShrine - || entity.behavior == &actTeleporter ) + || entity.behavior == &actTeleporter + || entity.behavior == &actWallButton ) { return true; } @@ -396,6 +445,15 @@ bool Player::Ghost_t::isActive() return false; } +bool Player::Ghost_t::isSpiritGhost() +{ + if ( my ) + { + return GHOSTCAM_COSMETIC_SPRITE != 0 && players[GHOSTCAM_PLAYERNUM]->entity && stats[GHOSTCAM_PLAYERNUM]->getEffectActive(EFF_PROJECT_SPIRIT); + } + return false; +} + void Player::Ghost_t::handleAttack() { if ( !isActive() ) @@ -442,10 +500,13 @@ void Player::Ghost_t::handleAttack() --errorFlashTeleportTicks; } + bool spiritGhost = isSpiritGhost(); + Input& input = Input::inputs[player.playernum]; bool attack = false; bool useSpell = false; bool defending = false; + bool highProfile = spiritGhost ? GHOSTCAM_SPIRIT_HIGH_PROFILE : false; if ( !player.usingCommand() && player.bControlEnabled && !gamePaused @@ -488,19 +549,35 @@ void Player::Ghost_t::handleAttack() } } } - if ( input.binary("Defend") ) + + if ( spiritGhost ) + { + if ( player.shootmode && !CalloutMenu[player.playernum].calloutMenuIsOpen() && !FollowerMenu[player.playernum].followerMenuIsOpen() ) + { + if ( input.consumeBinaryToggle("Defend") ) + { + highProfile = !highProfile; + playSoundEntityLocal(my, highProfile ? 794 : 795, 64); + } + } + } + else { - defending = true; + if ( input.binary("Defend") ) + { + defending = true; + } } } - if ( GHOSTCAM_SNEAKING != defending || ticks % 120 == 0 ) + if ( GHOSTCAM_SNEAKING != defending || GHOSTCAM_SPIRIT_HIGH_PROFILE != highProfile || ticks % 120 == 0 ) { if ( multiplayer == CLIENT ) { strcpy((char*)net_packet->data, "GHOD"); net_packet->data[4] = player.playernum; - net_packet->data[5] = defending; + net_packet->data[5] = defending ? (1 << 0) : 0; + net_packet->data[5] |= highProfile ? (1 << 1) : 0; net_packet->address.host = net_server.host; net_packet->address.port = net_server.port; net_packet->len = 6; @@ -510,7 +587,8 @@ void Player::Ghost_t::handleAttack() { strcpy((char*)net_packet->data, "GHOD"); net_packet->data[4] = player.playernum; - net_packet->data[5] = defending; + net_packet->data[5] = defending ? (1 << 0) : 0; + net_packet->data[5] |= highProfile ? (1 << 1) : 0; net_packet->len = 6; for ( int c = 1; c < MAXPLAYERS; ++c ) { @@ -526,6 +604,7 @@ void Player::Ghost_t::handleAttack() } } GHOSTCAM_SNEAKING = defending; + GHOSTCAM_SPIRIT_HIGH_PROFILE = highProfile; const int castLoopDuration = 4 * TICKS_PER_SECOND / 10; bool finishSpell = false; @@ -543,6 +622,13 @@ void Player::Ghost_t::handleAttack() finishSpell = true; } } + else if ( castingSpellAnimation == GHOST_SPELL_QUACK ) + { + if ( castingHeldDuration >= 1 ) + { + finishSpell = true; + } + } if ( finishSpell ) { @@ -556,12 +642,17 @@ void Player::Ghost_t::handleAttack() castSpell(uid, &spell_ghost_bolt, false, true); castingSpellAnimation = GHOST_SPELL_POST_CASTING; } + else if ( castingSpellAnimation == GHOST_SPELL_QUACK ) + { + castSpell(uid, getSpellFromID(SPELL_PROJECT_SPIRIT), false, true); + castingSpellAnimation = GHOST_SPELL_POST_CASTING; + } castingHeldDuration = 0; for ( int i = 0; i < 5; ++i ) { - Entity* entity = spawnGib(my); + Entity* entity = spawnGib(my, 16); entity->flags[INVISIBLE] = false; entity->flags[SPRITE] = true; entity->flags[NOUPDATE] = true; @@ -604,10 +695,20 @@ void Player::Ghost_t::handleAttack() { if ( attack ) { - castingSpellAnimation = GHOST_SPELL_BOLT; - cooldownChill = cooldownChillDelay; - errorFlashChillTicks = 0; - playSoundEntity(my, 170, 128); + if ( spiritGhost ) + { + castingSpellAnimation = GHOST_SPELL_QUACK; + cooldownChill = cooldownChillDelay; + errorFlashChillTicks = 0; + //playSoundEntity(my, 170, 128); + } + else + { + castingSpellAnimation = GHOST_SPELL_BOLT; + cooldownChill = cooldownChillDelay; + errorFlashChillTicks = 0; + playSoundEntity(my, 170, 128); + } } else if ( useSpell ) { @@ -617,7 +718,8 @@ void Player::Ghost_t::handleAttack() playSoundEntity(my, 170, 128); } } - else if ( castingSpellAnimation == GHOST_SPELL_TELEPORT || castingSpellAnimation == GHOST_SPELL_BOLT ) + else if ( castingSpellAnimation == GHOST_SPELL_TELEPORT || castingSpellAnimation == GHOST_SPELL_BOLT + || castingSpellAnimation == GHOST_SPELL_QUACK ) { ++castingHeldDuration; @@ -665,7 +767,7 @@ void Player::Ghost_t::handleAttack() if ( ticks % 5 == 0 ) { - Entity* entity = spawnGib(my); + Entity* entity = spawnGib(my, 16); entity->flags[INVISIBLE] = false; entity->flags[SPRITE] = true; entity->flags[NOUPDATE] = true; @@ -694,6 +796,13 @@ void Player::Ghost_t::handleAttack() entity->skill[11] = player.playernum; } } + else if ( castingSpellAnimation == GHOST_SPELL_QUACK ) + { + if ( castingHeldDuration == 1 ) + { + createEnsembleHUDParticleCircling(my); + } + } else if ( castingSpellAnimation == GHOST_SPELL_BOLT ) { float x = 6; @@ -763,11 +872,14 @@ void Player::Ghost_t::handleActions() Input& input = Input::inputs[player.playernum]; CalloutRadialMenu& calloutMenu = CalloutMenu[player.playernum]; + FollowerRadialMenu& followerMenu = FollowerMenu[player.playernum]; auto& b = (multiplayer != SINGLE && player.playernum != 0) ? Input::inputs[0].getBindings() : input.getBindings(); clickDescription(player.playernum, NULL); // inspecting objects - if ( !calloutMenu.calloutMenuIsOpen() ) + bool enableFollowerMenu = my && isSpiritGhost(); + + if ( !followerMenu.followerMenuIsOpen() && !calloutMenu.calloutMenuIsOpen() ) { bool clickedOnGUI = false; @@ -838,6 +950,124 @@ void Player::Ghost_t::handleActions() selectedEntity[player.playernum] = nullptr; } } + else if ( enableFollowerMenu && followerMenu.followerMenuIsOpen() ) + { + selectedEntity[player.playernum] = NULL; + + if ( !players[player.playernum]->usingCommand() && players[player.playernum]->bControlEnabled && !gamePaused && input.binaryToggle("Use") ) + { + if ( !followerMenu.menuToggleClick && followerMenu.selectMoveTo ) + { + if ( followerMenu.optionSelected == ALLY_CMD_MOVETO_SELECT ) + { + // we're selecting a point for the ally to move to. + input.consumeBinaryToggle("Use"); + //input.consumeBindingsSharedWithBinding("Use"); + + // we're selecting a point for the ally to move to. + if ( my ) + { + real_t startx = cameras[player.playernum].x * 16.0; + real_t starty = cameras[player.playernum].y * 16.0; + real_t startz = cameras[player.playernum].z + (4.5 - cameras[player.playernum].z) / 2.0 + *cvar_followerStartZ; + real_t pitch = cameras[player.playernum].vang; + if ( pitch < 0 || pitch > PI ) + { + pitch = 0; + } + + // draw line from the players height and direction until we hit the ground. + real_t previousx = startx; + real_t previousy = starty; + int index = 0; + const real_t yaw = cameras[player.playernum].ang; + for ( ; startz < *cvar_followerStartZLimit; startz += abs((*cvar_followerMoveTo) * tan(pitch)) ) + { + startx += 0.1 * cos(yaw); + starty += 0.1 * sin(yaw); + const int index_x = static_cast(startx) >> 4; + const int index_y = static_cast(starty) >> 4; + index = (index_y)*MAPLAYERS + (index_x)*MAPLAYERS * map.height; + if ( map.tiles[index] && !map.tiles[OBSTACLELAYER + index] ) + { + // store the last known good coordinate + previousx = startx;// + 16 * cos(yaw); + previousy = starty;// + 16 * sin(yaw); + } + if ( map.tiles[OBSTACLELAYER + index] ) + { + break; + } + } + + createParticleFollowerCommand(previousx, previousy, 0, FOLLOWER_TARGET_PARTICLE, 0); + followerMenu.optionSelected = ALLY_CMD_MOVETO_CONFIRM; + followerMenu.selectMoveTo = false; + followerMenu.moveToX = static_cast(previousx) / 16; + followerMenu.moveToY = static_cast(previousy) / 16; + //messagePlayer(PLAYER_NUM, "%d, %d, pitch: %f", followerMoveToX, followerMoveToY, players[PLAYER_NUM]->entity->pitch); + } + } + else if ( followerMenu.optionSelected == ALLY_CMD_ATTACK_SELECT ) + { + // we're selecting a target for the ally. + Entity* target = entityClicked(nullptr, false, player.playernum, EntityClickType::ENTITY_CLICK_FOLLOWER_INTERACT); + input.consumeBinaryToggle("Use"); + //input.consumeBindingsSharedWithBinding("Use"); + if ( target && followerMenu.followerToCommand ) + { + Entity* parent = uidToEntity(target->skill[2]); + if ( target->behavior == &actMonster || (parent && parent->behavior == &actMonster) ) + { + // see if we selected a limb + if ( parent ) + { + target = parent; + } + } + else if ( target->sprite == 184 || target->sprite == 585 ) // switch base. + { + parent = uidToEntity(target->parent); + if ( parent ) + { + target = parent; + } + } + if ( followerMenu.allowedInteractEntity(*target) ) + { + createParticleFollowerCommand(target->x, target->y, 0, FOLLOWER_TARGET_PARTICLE, + target->getUID()); + followerMenu.optionSelected = ALLY_CMD_ATTACK_CONFIRM; + followerMenu.followerToCommand->monsterAllyInteractTarget = target->getUID(); + } + else + { + messagePlayer(player.playernum, MESSAGE_HINT, Language::get(3094)); + followerMenu.optionSelected = ALLY_CMD_CANCEL; + followerMenu.optionPrevious = ALLY_CMD_ATTACK_CONFIRM; + followerMenu.followerToCommand->monsterAllyInteractTarget = 0; + } + } + else + { + followerMenu.optionSelected = ALLY_CMD_CANCEL; + followerMenu.optionPrevious = ALLY_CMD_ATTACK_CONFIRM; + followerMenu.followerToCommand->monsterAllyInteractTarget = 0; + } + + if ( players[player.playernum]->worldUI.isEnabled() ) + { + players[player.playernum]->worldUI.reset(); + players[player.playernum]->worldUI.tooltipView = Player::WorldUI_t::TooltipView::TOOLTIP_VIEW_RESCAN; + players[player.playernum]->worldUI.gimpDisplayTimer = 0; + } + + followerMenu.selectMoveTo = false; + strcpy(followerMenu.interactText, ""); + } + } + } + } else if ( calloutMenu.calloutMenuIsOpen() ) { selectedEntity[player.playernum] = NULL; @@ -952,6 +1182,7 @@ void Player::Ghost_t::handleActions() } } + bool skipFollowerMenu = false; if ( !player.usingCommand() && player.bControlEnabled && !gamePaused && CalloutRadialMenu::calloutMenuEnabledForGamemode() ) { @@ -991,6 +1222,7 @@ void Player::Ghost_t::handleActions() calloutMenu.optionSelected = CalloutRadialMenu::CALLOUT_CMD_SELECT; calloutMenu.lockOnEntityUid = 0; Player::soundActivate(); + skipFollowerMenu = true; if ( player.worldUI.isEnabled() ) { @@ -1029,6 +1261,7 @@ void Player::Ghost_t::handleActions() { calloutMenu.holdWheel = false; } + skipFollowerMenu = true; calloutMenu.selectMoveTo = false; calloutMenu.bOpen = true; calloutMenu.initCalloutMenuGUICursor(true); @@ -1078,6 +1311,7 @@ void Player::Ghost_t::handleActions() { calloutMenu.holdWheel = false; } + skipFollowerMenu = true; calloutMenu.selectMoveTo = false; calloutMenu.bOpen = true; calloutMenu.lockOnEntityUid = 0; @@ -1095,82 +1329,207 @@ void Player::Ghost_t::handleActions() } } - if ( selectedEntity[player.playernum] != nullptr ) + if ( !players[player.playernum]->usingCommand() && players[player.playernum]->bControlEnabled + && !gamePaused + && !skipFollowerMenu + && enableFollowerMenu + && !followerMenu.followerToCommand && followerMenu.recentEntity ) { - Entity* parent = uidToEntity(selectedEntity[player.playernum]->skill[2]); - if ( selectedEntity[player.playernum]->behavior == &actItem && pushPoints == 0 ) + auto& b = (multiplayer != SINGLE && player.playernum != 0) ? Input::inputs[0].getBindings() : input.getBindings(); + bool showNPCCommandsOnGamepad = false; + auto showNPCCommandsFind = b.find("Show NPC Commands"); + std::string showNPCCommandsInputStr = ""; + std::string lastNPCCommandInputStr = ""; + if ( showNPCCommandsFind != b.end() ) { - selectedEntity[player.playernum] = nullptr; - input.consumeBinaryToggle("Use"); - if ( cooldownPush > 0 ) + showNPCCommandsOnGamepad = (*showNPCCommandsFind).second.isBindingUsingGamepad(); + showNPCCommandsInputStr = (*showNPCCommandsFind).second.input; + } + bool lastNPCCommandOnGamepad = false; + auto lastNPCCommandFind = b.find("Command NPC"); + if ( lastNPCCommandFind != b.end() ) + { + lastNPCCommandOnGamepad = (*lastNPCCommandFind).second.isBindingUsingGamepad(); + lastNPCCommandInputStr = (*lastNPCCommandFind).second.input; + } + + if ( players[player.playernum]->worldUI.bTooltipInView && players[player.playernum]->worldUI.tooltipsInRange.size() > 1 ) + { + if ( showNPCCommandsOnGamepad && + (showNPCCommandsInputStr == input.binding("Interact Tooltip Next") + || showNPCCommandsInputStr == input.binding("Interact Tooltip Prev")) ) { - // no action - if ( errorFlashPushTicks == 0 && cooldownPush > (TICKS_PER_SECOND / 2) ) - { - errorFlashPushTicks = errorFlashTicks; - playSound(563, 64); - } + input.consumeBinaryToggle("Show NPC Commands"); + players[player.playernum]->hud.followerDisplay.bOpenFollowerMenuDisabled = true; + } + if ( lastNPCCommandOnGamepad && + (lastNPCCommandInputStr == input.binding("Interact Tooltip Next") + || lastNPCCommandInputStr == input.binding("Interact Tooltip Prev")) ) + { + input.consumeBinaryToggle("Command NPC"); + players[player.playernum]->hud.followerDisplay.bCommandNPCDisabled = true; } } - if ( selectedEntity[player.playernum] ) + + if ( !calloutMenu.calloutMenuIsOpen() ) { - input.consumeBinaryToggle("Use"); - //input.consumeBindingsSharedWithBinding("Use"); - if ( entityDist(my, selectedEntity[player.playernum]) <= TOUCHRANGE ) + if ( (input.binaryToggle("Show NPC Commands") && !showNPCCommandsOnGamepad) + || (input.binaryToggle("Show NPC Commands") && showNPCCommandsOnGamepad + && players[player.playernum]->shootmode /*&& !players[player.playernum]->worldUI.bTooltipInView*/) ) { - inrange[player.playernum] = true; - if ( selectedEntity[player.playernum]->behavior == &actItem ) + if ( players[player.playernum] && players[player.playernum]->entity + && followerMenu.recentEntity->monsterTarget == players[player.playernum]->entity->getUID() ) + { + // your ally is angry at you! + } + else { - --pushPoints; - pushPoints = std::max(0, pushPoints); - if ( pushPoints == 0 ) + selectedEntity[player.playernum] = followerMenu.recentEntity; + followerMenu.holdWheel = true; + if ( showNPCCommandsOnGamepad ) { - cooldownPush = cooldownPushDelay; + followerMenu.holdWheel = false; } } } - else + else if ( (input.binaryToggle("Command NPC") && !lastNPCCommandOnGamepad) + || (input.binaryToggle("Command NPC") && lastNPCCommandOnGamepad && players[player.playernum]->shootmode/*&& !players[PLAYER_NUM]->worldUI.bTooltipInView*/) ) { - inrange[player.playernum] = false; + if ( players[player.playernum] && players[player.playernum]->entity + && followerMenu.recentEntity->monsterTarget == players[player.playernum]->entity->getUID() ) + { + // your ally is angry at you! + input.consumeBinaryToggle("Command NPC"); + } + else if ( followerMenu.optionPrevious != -1 ) + { + followerMenu.followerToCommand = followerMenu.recentEntity; + } + else + { + input.consumeBinaryToggle("Command NPC"); + } } - if ( multiplayer == CLIENT ) + } + } + + if ( selectedEntity[player.playernum] != nullptr ) + { + if ( enableFollowerMenu ) + { + followerMenu.followerToCommand = nullptr; + Entity* parent = uidToEntity(selectedEntity[player.playernum]->skill[2]); + if ( selectedEntity[player.playernum]->behavior == &actMonster || (parent && parent->behavior == &actMonster) ) { - if ( inrange[player.playernum] ) + // see if we selected a follower to process right click menu. + if ( parent && parent->monsterAllyIndex == player.playernum ) { - strcpy((char*)net_packet->data, "CKIR"); + followerMenu.followerToCommand = parent; + //messagePlayer(0, "limb"); } - else + else if ( selectedEntity[player.playernum]->monsterAllyIndex == player.playernum ) + { + followerMenu.followerToCommand = selectedEntity[player.playernum]; + //messagePlayer(0, "head"); + } + + if ( followerMenu.followerToCommand ) + { + if ( players[player.playernum] && players[player.playernum]->entity + && followerMenu.followerToCommand->monsterTarget == players[player.playernum]->entity->getUID() ) + { + // your ally is angry at you! + followerMenu.followerToCommand = nullptr; + followerMenu.optionPrevious = -1; + } + else + { + followerMenu.recentEntity = followerMenu.followerToCommand; + followerMenu.initfollowerMenuGUICursor(true); + followerMenu.updateScrollPartySheet(); + selectedEntity[player.playernum] = NULL; + Player::soundActivate(); + } + } + } + } + + if ( selectedEntity[player.playernum] ) + { + if ( selectedEntity[player.playernum]->behavior == &actItem && pushPoints == 0 ) + { + selectedEntity[player.playernum] = nullptr; + input.consumeBinaryToggle("Use"); + if ( cooldownPush > 0 ) { - strcpy((char*)net_packet->data, "CKOR"); + // no action + if ( errorFlashPushTicks == 0 && cooldownPush > (TICKS_PER_SECOND / 2) ) + { + errorFlashPushTicks = errorFlashTicks; + playSound(563, 64); + } } - net_packet->data[4] = player.playernum; - if ( selectedEntity[player.playernum]->behavior == &actPlayerLimb ) + } + if ( selectedEntity[player.playernum] ) + { + input.consumeBinaryToggle("Use"); + //input.consumeBindingsSharedWithBinding("Use"); + if ( entityDist(my, selectedEntity[player.playernum]) <= TOUCHRANGE ) { - SDLNet_Write32((Uint32)players[selectedEntity[player.playernum]->skill[2]]->entity->getUID(), &net_packet->data[5]); + inrange[player.playernum] = true; + if ( selectedEntity[player.playernum]->behavior == &actItem ) + { + --pushPoints; + pushPoints = std::max(0, pushPoints); + if ( pushPoints == 0 ) + { + cooldownPush = cooldownPushDelay; + } + } } else { - Entity* tempEntity = uidToEntity(selectedEntity[player.playernum]->skill[2]); - if ( tempEntity ) + inrange[player.playernum] = false; + } + if ( multiplayer == CLIENT ) + { + if ( inrange[player.playernum] ) + { + strcpy((char*)net_packet->data, "CKIR"); + } + else + { + strcpy((char*)net_packet->data, "CKOR"); + } + net_packet->data[4] = player.playernum; + if ( selectedEntity[player.playernum]->behavior == &actPlayerLimb ) + { + SDLNet_Write32((Uint32)players[selectedEntity[player.playernum]->skill[2]]->entity->getUID(), &net_packet->data[5]); + } + else { - if ( tempEntity->behavior == &actMonster ) + Entity* tempEntity = uidToEntity(selectedEntity[player.playernum]->skill[2]); + if ( tempEntity ) { - SDLNet_Write32((Uint32)tempEntity->getUID(), &net_packet->data[5]); + if ( tempEntity->behavior == &actMonster ) + { + SDLNet_Write32((Uint32)tempEntity->getUID(), &net_packet->data[5]); + } + else + { + SDLNet_Write32((Uint32)selectedEntity[player.playernum]->getUID(), &net_packet->data[5]); + } } else { SDLNet_Write32((Uint32)selectedEntity[player.playernum]->getUID(), &net_packet->data[5]); } } - else - { - SDLNet_Write32((Uint32)selectedEntity[player.playernum]->getUID(), &net_packet->data[5]); - } + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, 0); } - net_packet->address.host = net_server.host; - net_packet->address.port = net_server.port; - net_packet->len = 9; - sendPacketSafe(net_sock, -1, net_packet, 0); } } } @@ -1319,42 +1678,213 @@ Entity* Player::Ghost_t::respawn() return nullptr; } -Entity* Player::Ghost_t::spawnGhost() +void actPlayerXP(Entity* my) { - if ( !player.isLocalPlayer() ) + if ( my->ticks >= 250 ) { - return nullptr; + list_RemoveNode(my->mynode); + return; } - if ( multiplayer != CLIENT ) + auto& bounceFloor = my->fskill[0]; + auto& hover = my->fskill[1]; + auto& bounceAmount = my->fskill[2]; + auto& hoverAmount = my->fskill[3]; + auto& spawnBounce = my->skill[0]; + auto& floatTimer = my->skill[1]; + auto& numBounces = my->skill[3]; + + if ( spawnBounce == 0 ) { - int sprite = Player::Ghost_t::getSpriteForPlayer(player.playernum); - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Ghost entity. - entity->x = spawnX * 16.0 + 8; - entity->y = spawnY * 16.0 + 8; - entity->z = -4; - entity->flags[PASSABLE] = true; - entity->flags[INVISIBLE] = true; - entity->flags[GENIUS] = true; - entity->behavior = &actDeathGhost; - entity->skill[2] = player.playernum; - entity->sizex = 2; - entity->sizey = 2; - entity->yaw = 0.0; - entity->pitch = PI / 16; - if ( player.playernum == clientnum && multiplayer == CLIENT ) + my->vel_z += 0.04; + my->z += my->vel_z; + hoverAmount = 0.15; + + if ( my->z < 0.0 ) { - entity->flags[UPDATENEEDED] = false; } else { - entity->flags[UPDATENEEDED] = true; + my->z = bounceFloor; + bounceAmount = my->vel_z; + my->vel_z = 0.0; + spawnBounce = 1; } - players[player.playernum]->ghost.my = entity; - players[player.playernum]->ghost.uid = entity->getUID(); - Compendium_t::Events_t::eventUpdateMonster(player.playernum, Compendium_t::CPDM_GHOST_SPAWNED, entity, 1); - return entity; - } + my->pitch += hoverAmount; + if ( ticks % 4 == 0 ) + { + Entity* fx = spawnMagicParticleCustom(my, 261, my->scalex, 5.0); + fx->flags[SPRITE] = true; + } + } + else + { + ++floatTimer; + hoverAmount = std::max(0.025, hoverAmount * 0.99); + hover += hoverAmount; + my->z = bounceFloor; + if ( hover >= 2 * PI ) + { + hover -= 2 * PI; + } + bounceAmount *= 0.975; + bounceAmount = std::max(0.25, bounceAmount); + my->z += 3 * bounceAmount * sin(hover); + if ( ticks % 4 == 0 ) + { + Entity* fx = spawnMagicParticleCustom(my, 225, 1.0, 1.0); + fx->flags[SPRITE] = true; + } + } + + my->yaw += hoverAmount; + +} + +void spawnPlayerXP(real_t x, real_t y, int player, int xpAmount) +{ + if ( player < 0 || player >= MAXPLAYERS ) { return; } + if ( xpAmount <= 0 ) { return; } + //if ( players[player]->entity ) { return; } + + int radius = 1; + std::set goodspots; + std::set okspots; + + int sprite = 211; // Player::Ghost_t::getSpriteForPlayer(player); + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Ghost entity. + entity->flags[PASSABLE] = true; + //entity->flags[INVISIBLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = true; + entity->flags[NOCLIP_CREATURES] = true; + entity->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; + entity->sizex = 4; + entity->sizex = 4; + entity->ditheringOverride = 6; + entity->vel_z = -1.0; + entity->behavior = &actPlayerXP; + + for ( int ix = -1; ix <= 1; ++ix ) + { + for ( int iy = -1; iy <= 1; ++iy ) + { + int checkx = static_cast(x / 16) + ix; + int checky = static_cast(y / 16) + iy; + if ( checkx >= 0 && checkx < map.width && checky >= 0 && checky < map.height ) + { + if ( !map.tiles[OBSTACLELAYER + checky * MAPLAYERS + checkx * MAPLAYERS * map.height] ) + { + okspots.insert(checkx + 10000 * checky); + if ( !checkObstacle(checkx * 16.0 + 8.0, checky * 16.0 + 8.0, entity, nullptr, true, true, false, false) ) + { + goodspots.insert(checkx + 10000 * checky); + } + } + } + } + } + + int checkx = static_cast(x / 16); + int checky = static_cast(y / 16); + int destx = 0; + int desty = 0; + bool foundspot = false; + if ( goodspots.find(checkx + 10000 * checky) != goodspots.end() ) + { + destx = checkx; + desty = checky; + foundspot = true; + } + else if ( goodspots.size() ) + { + int pick = local_rng.rand() % goodspots.size(); + int index = -1; + for ( auto spot : goodspots ) + { + ++index; + if ( pick == index ) + { + destx = spot % 10000; + desty = spot / 10000; + foundspot = true; + break; + } + } + } + + if ( !foundspot ) + { + if ( okspots.find(checkx + 10000 * checky) != okspots.end() ) + { + destx = checkx; + desty = checky; + foundspot = true; + } + else if ( okspots.size() ) + { + int pick = local_rng.rand() % okspots.size(); + int index = -1; + for ( auto spot : okspots ) + { + ++index; + if ( pick == index ) + { + destx = spot % 10000; + desty = spot / 10000; + foundspot = true; + break; + } + } + } + } + + if ( foundspot ) + { + entity->x = destx * 16.0 + 8.0; + entity->y = desty * 16.0 + 8.0; + return; + } + list_RemoveNode(entity->mynode); + return; +} + +Entity* Player::Ghost_t::spawnGhost() +{ + if ( !player.isLocalPlayer() ) + { + return nullptr; + } + + if ( multiplayer != CLIENT ) + { + int sprite = Player::Ghost_t::getSpriteForPlayer(player.playernum); + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Ghost entity. + entity->x = spawnX * 16.0 + 8; + entity->y = spawnY * 16.0 + 8; + entity->z = -4; + entity->flags[PASSABLE] = true; + entity->flags[INVISIBLE] = true; + entity->flags[GENIUS] = true; + entity->behavior = &actDeathGhost; + entity->skill[2] = player.playernum; + entity->sizex = 2; + entity->sizey = 2; + entity->yaw = 0.0; + entity->pitch = PI / 16; + if ( player.playernum == clientnum && multiplayer == CLIENT ) + { + entity->flags[UPDATENEEDED] = false; + } + else + { + entity->flags[UPDATENEEDED] = true; + } + players[player.playernum]->ghost.my = entity; + players[player.playernum]->ghost.uid = entity->getUID(); + Compendium_t::Events_t::eventUpdateMonster(player.playernum, Compendium_t::CPDM_GHOST_SPAWNED, entity, 1); + return entity; + } if ( multiplayer == CLIENT ) { @@ -1626,6 +2156,16 @@ void actDeathGhostLimb(Entity* my) if ( !players[playernum] || !players[playernum]->ghost.my ) { + if ( my->monsterStoreType == 1 ) + { + if ( Entity::getMonsterTypeFromSprite(my->sprite) == DUCK_SMALL ) + { + duckSpawnFeather(my->sprite, my->x, my->y, my->z, my); + spawnPoof(my->x, my->y, 7.5, 0.5, false); + + createParticleErupt(my, 593); + } + } list_RemoveNode(my->mynode); return; } @@ -1685,6 +2225,16 @@ void actDeathGhost(Entity* my) list_RemoveNode(my->mynode); return; } + + if ( players[playernum]->entity ) + { + if ( !stats[playernum]->getEffectActive(EFF_PROJECT_SPIRIT) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } } players[playernum]->ghost.my = my; @@ -1752,6 +2302,31 @@ void actDeathGhost(Entity* my) { players[playernum]->ghost.initTeleportLocations(my->x / 16, my->y / 16); } + + // extra cosmetic limbs + entity = newEntity(-1, 1, map.entities, nullptr); + entity->behavior = &actDeathGhostLimb; + entity->sizex = 4; + entity->sizey = 4; + entity->flags[PASSABLE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[NOUPDATE] = true; + entity->flags[GENIUS] = true; + entity->monsterStoreType = 1; // cosmetic limb head flag + entity->skill[2] = GHOSTCAM_PLAYERNUM; + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + if ( multiplayer == CLIENT && GHOSTCAM_PLAYERNUM == clientnum ) + { + if ( GHOSTCAM_COSMETIC_SPRITE != 0 && players[clientnum]->entity ) + { + players[clientnum]->entity->skill[3] = 2; + } + } } if ( GHOSTCAM_DEACTIVATED ) @@ -1769,6 +2344,23 @@ void actDeathGhost(Entity* my) } float floatAnimationPercent = std::min(1.f, (float)GHOSTCAM_SPAWN_ANIM / (spawnAnimationHalfway)); float thirdPersonAnimationPercent = 0.f; + if ( players[playernum]->ghost.isSpiritGhost() ) + { + if ( node_t* node = list_Node(&my->children, 2) ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( Entity::getMonsterTypeFromSprite(entity->sprite) == DUCK_SMALL ) + { + if ( my->monsterSpecialState == DUCK_DIVE ) + { + GHOSTCAM_THIRD_PERSON_CUSTOM = 1.0; + } + thirdPersonAnimationPercent = GHOSTCAM_THIRD_PERSON_CUSTOM; + } + } + } + } if ( spawnAnimationPlaying ) { thirdPersonAnimationPercent = 1.f; @@ -1778,6 +2370,10 @@ void actDeathGhost(Entity* my) } thirdPersonAnimationPercent = std::max(0.f, thirdPersonAnimationPercent); } + + GHOSTCAM_THIRD_PERSON_CUSTOM -= std::max(0.01, GHOSTCAM_THIRD_PERSON_CUSTOM / 20.0); + GHOSTCAM_THIRD_PERSON_CUSTOM = std::max(0.0, GHOSTCAM_THIRD_PERSON_CUSTOM); + if ( GHOSTCAM_SPAWN_ANIM < spawnAnimationDuration ) { if ( !GHOSTCAM_DEACTIVATED ) @@ -1888,6 +2484,31 @@ void actDeathGhost(Entity* my) camx = my->x / 16.f; camy = my->y / 16.f; camz = my->z * 2.f + GHOSTCAM_BOB; + + if ( player->ghost.isSpiritGhost() ) + { + if ( node_t* node = list_Node(&my->children, 2) ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( node_t* innerNode = list_Node(&entity->children, 2) ) + { + if ( Entity* entity2 = (Entity*)innerNode->element ) + { + if ( GHOSTCAM_COSMETIC_SPRITE > 0 ) + { + camz = entity2->fskill[18] * 2.f + GHOSTCAM_BOB; + if ( thirdPersonAnimationPercent > 0.01 ) + { + camz = std::min(camz, 4.0 * 2.f); + } + } + } + } + } + } + } + camang = my->yaw; camvang = my->pitch; @@ -1897,10 +2518,10 @@ void actDeathGhost(Entity* my) keystatus[SDLK_h] = 0; player->ghost.setActive(false); }*/ - if ( *cvar_ghostThirdPerson || spawnAnimationPlaying ) + if ( *cvar_ghostThirdPerson || spawnAnimationPlaying || thirdPersonAnimationPercent > 0.01 ) { real_t dist = 1.0; - if ( spawnAnimationPlaying ) + if ( spawnAnimationPlaying || thirdPersonAnimationPercent > 0.01 ) { dist = cos((1.0 - thirdPersonAnimationPercent) * PI / 2); } @@ -1949,6 +2570,46 @@ void actDeathGhost(Entity* my) } } + if ( multiplayer != CLIENT ) + { + int cosmeticSprite = 0; + if ( player->entity && stats[GHOSTCAM_PLAYERNUM]->getEffectActive(EFF_PROJECT_SPIRIT) ) + { + int appearance = GHOSTCAM_PLAYERNUM; + cosmeticSprite = 2225; + if ( player->mechanics.ducksInARow.size() ) + { + appearance = player->mechanics.ducksInARow.at(0).first; + } + if ( appearance == 0 ) + { + cosmeticSprite = 2225; + } + else if ( appearance == 1 ) + { + cosmeticSprite = 2231; + } + else if ( appearance == 2 ) + { + cosmeticSprite = 2237; + } + else if ( appearance == 3 ) + { + cosmeticSprite = 2307; + } + } + bool update = false; + if ( GHOSTCAM_COSMETIC_SPRITE != cosmeticSprite || (my->getUID() % (TICKS_PER_SECOND * 3) == ticks % (TICKS_PER_SECOND * 3)) ) + { + update = true; + } + GHOSTCAM_COSMETIC_SPRITE = cosmeticSprite; + if ( update ) + { + serverUpdateEntitySkill(my, 10); + } + } + static ConsoleVariable cvar_ghostBounce("/ghost_bounce", -1.0); real_t dist = 0.0; if ( player->isLocalPlayer() ) @@ -2218,6 +2879,107 @@ void actDeathGhost(Entity* my) bodypart->z += 0.4 * sin(GHOSTCAM_HOVER); bodypart->focalx = 2.25; } + else if ( index == 2 ) + { + if ( bodypart->sprite != GHOSTCAM_COSMETIC_SPRITE ) + { + if ( GHOSTCAM_COSMETIC_SPRITE == 0 ) + { + GHOSTCAM_SPAWN_ANIM = 0; + GHOSTCAM_SPAWN_ANIM_COMPLETE = 0; + } + } + bodypart->sprite = GHOSTCAM_COSMETIC_SPRITE; + if ( Entity::getMonsterTypeFromSprite(GHOSTCAM_COSMETIC_SPRITE) == DUCK_SMALL ) + { + if ( !bodypart->skill[3] ) + { + bodypart->skill[3] = 1; + + // 2 empty nodes for stat struct + node_t* node2 = list_AddNodeFirst(&bodypart->children); + node2->element = nullptr; + node2->deconstructor = &emptyDeconstructor; + node2 = list_AddNodeFirst(&bodypart->children); + node2->element = nullptr; + node2->deconstructor = &emptyDeconstructor; + + initDuck(bodypart, nullptr); + } + + duckAnimate(bodypart, nullptr, dist); + if ( multiplayer != CLIENT ) + { + if ( bodypart->monsterSpecialState != my->monsterSpecialState ) + { + my->monsterSpecialState = bodypart->monsterSpecialState; + serverUpdateEntitySkill(my, 33); + } + } + if ( multiplayer == CLIENT ) + { + bodypart->monsterSpecialState = my->monsterSpecialState; + } + + if ( playernum == clientnum ) + { + if ( player->ghost.isActive() ) + { + if ( my->ticks % (5 * TICKS_PER_SECOND) == 0 ) + { + gameStatistics[STATISTICS_QUACKERY] += 5; + if ( gameStatistics[STATISTICS_QUACKERY] >= 5 * 60 ) + { + steamAchievement("BARONY_ACH_QUACKERY"); + } + } + } + } + + node_t* node = nullptr; + int bodypartIndex = 0; + for ( bodypartIndex = 0, node = bodypart->children.first; node != nullptr; node = node->next, ++bodypartIndex ) + { + if ( bodypartIndex < 2 ) + { + continue; + } + if ( Entity* entity = (Entity*)node->element ) + { + if ( !player->ghost.isActive() ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + entity->ditheringOverride = -1; + } + } + } + + if ( my->bodyparts.size() >= 2 ) + { + my->bodyparts.at(0)->flags[INVISIBLE] = true; + my->bodyparts.at(1)->flags[INVISIBLE] = true; + } + } + else + { + node_t* node = nullptr; + int bodypartIndex = 0; + for ( bodypartIndex = 0, node = bodypart->children.first; node != nullptr; node = node->next, ++bodypartIndex ) + { + if ( bodypartIndex < 2 ) + { + continue; + } + if ( Entity* entity = (Entity*)node->element ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + entity->ditheringOverride = -1; + } + } + } + } } } @@ -2235,30 +2997,34 @@ void actDeathGhost(Entity* my) #define DEATHCAM_IDLEPITCH_FLOAT_TO_ZERO my->fskill[4] #define DEATHCAM_IDLEYAWSPEED my->fskill[5] -void actDeathCam(Entity* my) -{ - /*if ( keystatus[SDLK_F4] ) - { - buttonStartSingleplayer(nullptr); - keystatus[SDLK_F4] = 0; - }*/ - DEATHCAM_TIME++; +#define PROJECTCAM_EXPIRED my->skill[6] - Uint32 deathcamGameoverPromptTicks = *MainMenu::cvar_fastRestart ? TICKS_PER_SECOND : - (splitscreen ? TICKS_PER_SECOND * 3 : TICKS_PER_SECOND * 6); - if ( gameModeManager.isFastDeathGrave() ) +void actProjectSpiritCam(Entity* my) +{ + auto entityTarget = uidToEntity(DEATHCAM_PLAYERTARGET); + if ( !entityTarget || + !(stats[DEATHCAM_PLAYERNUM]->getEffectActive(EFF_PROJECT_SPIRIT) && players[DEATHCAM_PLAYERNUM]->entity + && players[DEATHCAM_PLAYERNUM]->entity->skill[3] == 2) ) { - deathcamGameoverPromptTicks = TICKS_PER_SECOND * 3; + if ( players[DEATHCAM_PLAYERNUM]->entity ) + { + players[DEATHCAM_PLAYERNUM]->entity->skill[3] = 0; + } + my->removeLightField(); + list_RemoveNode(my->mynode); + return; } + DEATHCAM_TIME++; + real_t mousex_relative = mousexrel; real_t mousey_relative = mouseyrel; mousex_relative = inputs.getMouseFloat(DEATHCAM_PLAYERNUM, Inputs::ANALOGUE_XREL); mousey_relative = inputs.getMouseFloat(DEATHCAM_PLAYERNUM, Inputs::ANALOGUE_YREL); - const bool smoothmouse = playerSettings[multiplayer ? 0 : DEATHCAM_PLAYERNUM].smoothmouse; - const bool reversemouse = playerSettings[multiplayer ? 0 : DEATHCAM_PLAYERNUM].reversemouse; + const bool smoothmouse = playerSettings[multiplayer ? 0 : DEATHCAM_PLAYERNUM].smoothmouse; + const bool reversemouse = playerSettings[multiplayer ? 0 : DEATHCAM_PLAYERNUM].reversemouse; real_t mouse_speed = playerSettings[multiplayer ? 0 : DEATHCAM_PLAYERNUM].mousespeed; if ( inputs.getVirtualMouse(DEATHCAM_PLAYERNUM)->lastMovementFromController ) { @@ -2267,26 +3033,292 @@ void actDeathCam(Entity* my) if ( DEATHCAM_TIME == 1 ) { - DEATHCAM_PLAYERTARGET = DEATHCAM_PLAYERNUM; + //DEATHCAM_PLAYERTARGET = DEATHCAM_PLAYERNUM; DEATHCAM_IDLEROTATEDIRYAW = (local_rng.rand() % 2 == 0) ? 1 : -1; } - else if ( DEATHCAM_TIME < deathcamGameoverPromptTicks ) + + bool shootmode = players[DEATHCAM_PLAYERNUM]->shootmode; + if ( shootmode && !gamePaused ) { - if ( players[DEATHCAM_PLAYERNUM]->ghost.isActive() || players[DEATHCAM_PLAYERNUM]->entity ) + if ( smoothmouse ) { - DEATHCAM_DISABLE_GAMEOVER = 1; + DEATHCAM_ROTX += mousex_relative * .006 * (mouse_speed / 128.f); + DEATHCAM_ROTX = fmin(fmax(-0.35, DEATHCAM_ROTX), 0.35); } - } - else if ( DEATHCAM_TIME == deathcamGameoverPromptTicks ) - { - if ( gameModeManager.getMode() == GameModeManager_t::GAME_MODE_TUTORIAL ) + else { - gameModeManager.Tutorial.openGameoverWindow(); + DEATHCAM_ROTX = std::min(std::max(-0.35f, mousex_relative * .01f * (mouse_speed / 128.f)), 0.35f); } - else if ( !(players[DEATHCAM_PLAYERNUM]->ghost.isActive() || players[DEATHCAM_PLAYERNUM]->entity) - && DEATHCAM_DISABLE_GAMEOVER == 0 ) + my->yaw += DEATHCAM_ROTX; + if ( my->yaw >= PI * 2 ) { - MainMenu::openGameoverWindow(DEATHCAM_PLAYERNUM); + my->yaw -= PI * 2; + } + else if ( my->yaw < 0 ) + { + my->yaw += PI * 2; + } + + if ( smoothmouse ) + { + DEATHCAM_ROTY += mousey_relative * .006 * (mouse_speed / 128.f) * (reversemouse * 2 - 1); + DEATHCAM_ROTY = fmin(fmax(-0.35, DEATHCAM_ROTY), 0.35); + } + else + { + DEATHCAM_ROTY = std::min(std::max(-0.35f, mousey_relative * .01f * (mouse_speed / 128.f) * (reversemouse * 2 - 1)), 0.35f); + } + my->pitch -= DEATHCAM_ROTY; + if ( my->pitch > PI / 2 ) + { + my->pitch = PI / 2; + } + else if ( my->pitch < -PI / 2 ) + { + my->pitch = -PI / 2; + } + + if ( abs(DEATHCAM_ROTX) < 0.0001 && abs(DEATHCAM_ROTY) < 0.0001 + /*&& DEATHCAM_PLAYERTARGET == DEATHCAM_PLAYERNUM*/ + && (DEATHCAM_TIME >= TICKS_PER_SECOND * 3) ) + { + ++DEATHCAM_IDLETIME; + if ( DEATHCAM_IDLETIME >= TICKS_PER_SECOND * 3 ) + { + my->yaw += DEATHCAM_IDLEYAWSPEED * DEATHCAM_IDLEROTATEDIRYAW; + if ( my->yaw >= PI * 2 ) + { + my->yaw -= PI * 2; + } + else if ( my->yaw < 0 ) + { + my->yaw += PI * 2; + } + + if ( !DEATHCAM_IDLEROTATEPITCHINIT ) + { + DEATHCAM_IDLEROTATEPITCHINIT = 1; + DEATHCAM_IDLEPITCH_START = my->pitch; + DEATHCAM_IDLEPITCH = 0.0; + DEATHCAM_IDLEPITCH_FLOAT_TO_ZERO = 0.0; + DEATHCAM_IDLEYAWSPEED = 0.0; + } + DEATHCAM_IDLEPITCH += PI / 1024; + if ( DEATHCAM_IDLEPITCH >= PI * 2 ) + { + DEATHCAM_IDLEPITCH -= PI * 2; + } + else if ( DEATHCAM_IDLEPITCH < 0 ) + { + DEATHCAM_IDLEPITCH += PI * 2; + } + if ( (DEATHCAM_IDLEPITCH_START - DEATHCAM_IDLEPITCH_FLOAT_TO_ZERO) > .001 ) + { + DEATHCAM_IDLEPITCH_FLOAT_TO_ZERO += .0001; + } + else if ( (DEATHCAM_IDLEPITCH_START - DEATHCAM_IDLEPITCH_FLOAT_TO_ZERO) < .001 ) + { + DEATHCAM_IDLEPITCH_FLOAT_TO_ZERO -= .0001; + } + my->pitch = -DEATHCAM_IDLEPITCH_FLOAT_TO_ZERO + DEATHCAM_IDLEPITCH_START + (PI / 64) * sin(DEATHCAM_IDLEPITCH); + if ( my->pitch > PI / 2 ) + { + my->pitch = PI / 2; + } + else if ( my->pitch < -PI / 2 ) + { + my->pitch = -PI / 2; + } + + if ( DEATHCAM_IDLEYAWSPEED < .001 ) + { + DEATHCAM_IDLEYAWSPEED += .00001; + } + } + } + else + { + DEATHCAM_IDLETIME = 0; + DEATHCAM_IDLEROTATEPITCHINIT = 0; + DEATHCAM_IDLEPITCH_FLOAT_TO_ZERO = 0.0; + DEATHCAM_IDLEYAWSPEED = 0.0; + } + } + if ( smoothmouse ) + { + DEATHCAM_ROTX *= .5; + DEATHCAM_ROTY *= .5; + } + else + { + DEATHCAM_ROTX = 0; + DEATHCAM_ROTY = 0; + } + + if ( shootmode + && !players[DEATHCAM_PLAYERNUM]->GUI.isGameoverActive() + && players[DEATHCAM_PLAYERNUM]->bControlEnabled + && !players[DEATHCAM_PLAYERNUM]->usingCommand() + && !gamePaused + && (Input::inputs[DEATHCAM_PLAYERNUM].consumeBinaryToggle("Attack") + || Input::inputs[DEATHCAM_PLAYERNUM].consumeBinaryToggle("MenuConfirm")) ) + { + /*DEATHCAM_PLAYERTARGET++; + if ( DEATHCAM_PLAYERTARGET >= MAXPLAYERS ) + { + DEATHCAM_PLAYERTARGET = 0; + } + int c = 0; + while ( !Player::getPlayerInteractEntity(DEATHCAM_PLAYERTARGET) ) + { + if ( c > MAXPLAYERS ) + { + break; + } + DEATHCAM_PLAYERTARGET++; + if ( DEATHCAM_PLAYERTARGET >= MAXPLAYERS ) + { + DEATHCAM_PLAYERTARGET = 0; + } + c++; + }*/ + } + + if ( DEATHCAM_PLAYERTARGET >= 0 ) + { + if ( entityTarget ) + { + my->x = entityTarget->x; + my->y = entityTarget->y; + //my->z = entityTarget->z; + } + /*else + { + DEATHCAM_PLAYERTARGET = DEATHCAM_PLAYERNUM; + }*/ + } + + my->removeLightField(); + + my->light = addLight(my->x / 16, my->y / 16, "deathcam"); + + real_t camx, camy, camz, camang, camvang; + camx = my->x / 16.f; + camy = my->y / 16.f; + camz = my->z * 2.f; + camang = my->yaw; + camvang = my->pitch; + + camx -= cos(my->yaw) * cos(my->pitch) * 1.5; + camy -= sin(my->yaw) * cos(my->pitch) * 1.5; + camz -= sin(my->pitch) * 16; + + if ( !TimerExperiments::bUseTimerInterpolation ) + { + cameras[DEATHCAM_PLAYERNUM].x = camx; + cameras[DEATHCAM_PLAYERNUM].y = camy; + cameras[DEATHCAM_PLAYERNUM].z = camz; + cameras[DEATHCAM_PLAYERNUM].ang = camang; + cameras[DEATHCAM_PLAYERNUM].vang = camvang; + return; + } + else + { + TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].x.velocity = + TimerExperiments::lerpFactor * (camx - TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].x.position); + TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].y.velocity = + TimerExperiments::lerpFactor * (camy - TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].y.position); + TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].z.velocity = + TimerExperiments::lerpFactor * (camz - TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].z.position); + + real_t diff = camang - TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].yaw.position; + if ( diff > PI ) + { + diff -= 2 * PI; + } + else if ( diff < -PI ) + { + diff += 2 * PI; + } + TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].yaw.velocity = diff * TimerExperiments::lerpFactor; + diff = camvang - TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].pitch.position; + if ( diff >= PI ) + { + diff -= 2 * PI; + } + else if ( diff < -PI ) + { + diff += 2 * PI; + } + TimerExperiments::cameraCurrentState[DEATHCAM_PLAYERNUM].pitch.velocity = diff * TimerExperiments::lerpFactor; + } +} + +void actDeathCam(Entity* my) +{ + /*if ( keystatus[SDLK_F4] ) + { + buttonStartSingleplayer(nullptr); + keystatus[SDLK_F4] = 0; + }*/ + DEATHCAM_TIME++; + + Uint32 deathcamGameoverPromptTicks = *MainMenu::cvar_fastRestart ? TICKS_PER_SECOND : + (splitscreen ? TICKS_PER_SECOND * 3 : TICKS_PER_SECOND * 6); + if ( *MainMenu::cvar_fastRestart ) + { + if ( players[DEATHCAM_PLAYERNUM]->entity && players[DEATHCAM_PLAYERNUM]->entity->playerCreatedDeathCam != 0 ) + { + // automaton deaths, reverse the count while we're still alive + if ( DEATHCAM_TIME > 2 ) + { + --DEATHCAM_TIME; + } + } + } + if ( gameModeManager.isFastDeathGrave() ) + { + deathcamGameoverPromptTicks = TICKS_PER_SECOND * 3; + } + + real_t mousex_relative = mousexrel; + real_t mousey_relative = mouseyrel; + + mousex_relative = inputs.getMouseFloat(DEATHCAM_PLAYERNUM, Inputs::ANALOGUE_XREL); + mousey_relative = inputs.getMouseFloat(DEATHCAM_PLAYERNUM, Inputs::ANALOGUE_YREL); + + const bool smoothmouse = playerSettings[multiplayer ? 0 : DEATHCAM_PLAYERNUM].smoothmouse; + const bool reversemouse = playerSettings[multiplayer ? 0 : DEATHCAM_PLAYERNUM].reversemouse; + real_t mouse_speed = playerSettings[multiplayer ? 0 : DEATHCAM_PLAYERNUM].mousespeed; + if ( inputs.getVirtualMouse(DEATHCAM_PLAYERNUM)->lastMovementFromController ) + { + mouse_speed = 32.0; + } + + if ( DEATHCAM_TIME == 1 ) + { + DEATHCAM_PLAYERTARGET = DEATHCAM_PLAYERNUM; + DEATHCAM_IDLEROTATEDIRYAW = (local_rng.rand() % 2 == 0) ? 1 : -1; + } + else if ( DEATHCAM_TIME < deathcamGameoverPromptTicks ) + { + if ( players[DEATHCAM_PLAYERNUM]->ghost.isActive() + || (players[DEATHCAM_PLAYERNUM]->entity && players[DEATHCAM_PLAYERNUM]->entity->playerCreatedDeathCam == 0) ) + { + DEATHCAM_DISABLE_GAMEOVER = 1; + } + } + else if ( DEATHCAM_TIME == deathcamGameoverPromptTicks ) + { + if ( gameModeManager.getMode() == GameModeManager_t::GAME_MODE_TUTORIAL ) + { + gameModeManager.Tutorial.openGameoverWindow(); + } + else if ( !(players[DEATHCAM_PLAYERNUM]->ghost.isActive() + || (players[DEATHCAM_PLAYERNUM]->entity && players[DEATHCAM_PLAYERNUM]->entity->playerCreatedDeathCam == 0) ) + && DEATHCAM_DISABLE_GAMEOVER == 0 ) + { + MainMenu::openGameoverWindow(DEATHCAM_PLAYERNUM); } DEATHCAM_IDLETIME = TICKS_PER_SECOND * 3; } @@ -2300,7 +3332,8 @@ void actDeathCam(Entity* my) } bool shootmode = players[DEATHCAM_PLAYERNUM]->shootmode; - if ( shootmode && !gamePaused && !(players[DEATHCAM_PLAYERNUM]->ghost.isActive() || players[DEATHCAM_PLAYERNUM]->entity) ) + if ( shootmode && !gamePaused && !(players[DEATHCAM_PLAYERNUM]->ghost.isActive() + || (players[DEATHCAM_PLAYERNUM]->entity && players[DEATHCAM_PLAYERNUM]->entity->playerCreatedDeathCam == 0)) ) { if ( !players[DEATHCAM_PLAYERNUM]->GUI.isGameoverActive() ) { @@ -2576,7 +3609,7 @@ bool Player::PlayerMovement_t::isPlayerSwimming() } bool swimming = false; bool levitating = isLevitating(stats[PLAYER_NUM]); - if ( !levitating && !waterwalkingboots && !noclip && !skillCapstoneUnlocked(PLAYER_NUM, PRO_SWIMMING) ) + if ( !levitating && !waterwalkingboots && !noclip /*&& !skillCapstoneUnlocked(PLAYER_NUM, PRO_LEGACY_SWIMMING)*/ ) { int x = std::min(std::max(0, floor(my->x / 16)), map.width - 1); int y = std::min(std::max(0, floor(my->y / 16)), map.height - 1); @@ -2686,7 +3719,7 @@ void Player::PlayerMovement_t::startQuickTurn() } quickTurnRotation = PI * players[PLAYER_NUM]->settings.quickTurnDirection; - if ( stats[PLAYER_NUM]->EFFECTS[EFF_CONFUSED] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_CONFUSED) ) { quickTurnRotation = -quickTurnRotation; } @@ -2731,7 +3764,9 @@ void Player::PlayerMovement_t::startQuickTurn() void Player::PlayerMovement_t::handlePlayerCameraUpdate(bool useRefreshRateDelta) { - if ( !players[player.playernum]->entity ) + if ( !players[player.playernum]->entity || (players[player.playernum]->entity + && stats[player.playernum]->getEffectActive(EFF_PROJECT_SPIRIT) + && players[player.playernum]->entity->skill[3] == 2) ) { return; } @@ -2773,7 +3808,7 @@ void Player::PlayerMovement_t::handlePlayerCameraUpdate(bool useRefreshRateDelta if ( !player.usingCommand() && player.bControlEnabled && !gamePaused && my->isMobile() && !inputs.hasController(PLAYER_NUM) ) { - if ( !stats[playernum]->EFFECTS[EFF_CONFUSED] ) + if ( !stats[playernum]->getEffectActive(EFF_CONFUSED) ) { if ( noclip ) { @@ -2797,7 +3832,7 @@ void Player::PlayerMovement_t::handlePlayerCameraUpdate(bool useRefreshRateDelta } else if ( shootmode && !gamePaused ) { - if ( !stats[PLAYER_NUM]->EFFECTS[EFF_CONFUSED] ) + if ( !stats[PLAYER_NUM]->getEffectActive(EFF_CONFUSED) ) { if ( smoothmouse ) { @@ -2888,7 +3923,7 @@ void Player::PlayerMovement_t::handlePlayerCameraUpdate(bool useRefreshRateDelta if ( !player.usingCommand() && player.bControlEnabled && !gamePaused && my->isMobile() && !inputs.hasController(PLAYER_NUM) ) { - if ( !stats[PLAYER_NUM]->EFFECTS[EFF_CONFUSED] ) + if ( !stats[PLAYER_NUM]->getEffectActive(EFF_CONFUSED) ) { my->pitch += (Input::inputs[playernum].analog("Look Down") - Input::inputs[playernum].analog("Look Up")) * .05 * refreshRateDelta; } @@ -2899,7 +3934,7 @@ void Player::PlayerMovement_t::handlePlayerCameraUpdate(bool useRefreshRateDelta } if ( shootmode && !gamePaused ) { - if ( !stats[PLAYER_NUM]->EFFECTS[EFF_CONFUSED] ) + if ( !stats[PLAYER_NUM]->getEffectActive(EFF_CONFUSED) ) { if ( smoothmouse ) { @@ -2950,13 +3985,14 @@ void Player::PlayerMovement_t::handlePlayerCameraUpdate(bool useRefreshRateDelta } my->pitch -= PLAYER_ROTY * refreshRateDelta; - if ( my->pitch > PI / 3 ) + static ConsoleVariable cvar_player_cam_pitch("/player_cam_pitch", PI / 3); + if ( my->pitch > *cvar_player_cam_pitch ) { - my->pitch = PI / 3; + my->pitch = *cvar_player_cam_pitch; } - if ( my->pitch < -PI / 3 ) + if ( my->pitch < -*cvar_player_cam_pitch ) { - my->pitch = -PI / 3; + my->pitch = -*cvar_player_cam_pitch; } if ( smoothmouse ) @@ -3048,8 +4084,8 @@ void Player::PlayerMovement_t::handlePlayerCameraBobbing(bool useRefreshRateDelt && ((input.binary("Move Forward") || input.binary("Move Backward")) || (input.binary("Move Left") - input.binary("Move Right")))) || (inputs.hasController(PLAYER_NUM) - && (inputs.getController(PLAYER_NUM)->getLeftXPercentForPlayerMovement() - || inputs.getController(PLAYER_NUM)->getLeftYPercentForPlayerMovement()))) ) + && (inputs.getController(PLAYER_NUM)->getLeftXPercentForPlayerMovement(player.playernum) + || inputs.getController(PLAYER_NUM)->getLeftYPercentForPlayerMovement(player.playernum)))) ) { if ( !player.usingCommand() && player.bControlEnabled && !swimming ) @@ -3128,15 +4164,15 @@ void Player::PlayerMovement_t::handlePlayerCameraBobbing(bool useRefreshRateDelt else if ( !player.usingCommand() && player.bControlEnabled && !gamePaused - && !swimming && inputs.hasController(PLAYER_NUM) && abs(inputs.getController(PLAYER_NUM)->getLeftXPercentForPlayerMovement()) > 0.001 ) + && !swimming && inputs.hasController(PLAYER_NUM) && abs(inputs.getController(PLAYER_NUM)->getLeftXPercentForPlayerMovement(player.playernum)) > 0.001 ) { auto controller = inputs.getController(PLAYER_NUM); - if ( (controller->getLeftXPercentForPlayerMovement() > 0.001 && controller->getLeftYPercentForPlayerMovement() >= 0.0) - || (controller->getLeftXPercentForPlayerMovement() < -0.001 && controller->getLeftYPercentForPlayerMovement() < -0.001 ) ) + if ( (controller->getLeftXPercentForPlayerMovement(player.playernum) > 0.001 && controller->getLeftYPercentForPlayerMovement(player.playernum) >= 0.0) + || (controller->getLeftXPercentForPlayerMovement(player.playernum) < -0.001 && controller->getLeftYPercentForPlayerMovement(player.playernum) < -0.001 ) ) { PLAYER_SIDEBOB += 0.01 * refreshRateDelta; real_t angle = PI / 32; - if ( controller->getLeftYPercentForPlayerMovement() < 0.001 ) + if ( controller->getLeftYPercentForPlayerMovement(player.playernum) < 0.001 ) { angle = PI / 64; } @@ -3145,12 +4181,12 @@ void Player::PlayerMovement_t::handlePlayerCameraBobbing(bool useRefreshRateDelt PLAYER_SIDEBOB = angle; } } - else if ( (controller->getLeftXPercentForPlayerMovement() < -0.001 && controller->getLeftYPercentForPlayerMovement() >= 0.0) - || (controller->getLeftXPercentForPlayerMovement() > 0.001 && controller->getLeftYPercentForPlayerMovement() < -0.001) ) + else if ( (controller->getLeftXPercentForPlayerMovement(player.playernum) < -0.001 && controller->getLeftYPercentForPlayerMovement(player.playernum) >= 0.0) + || (controller->getLeftXPercentForPlayerMovement(player.playernum) > 0.001 && controller->getLeftYPercentForPlayerMovement(player.playernum) < -0.001) ) { PLAYER_SIDEBOB -= 0.01 * refreshRateDelta; real_t angle = -PI / 32; - if ( controller->getLeftYPercentForPlayerMovement() < 0.001 ) + if ( controller->getLeftYPercentForPlayerMovement(player.playernum) < 0.001 ) { angle = -PI / 64; } @@ -3290,10 +4326,14 @@ int Player::PlayerMovement_t::getCharacterModifiedWeight(int* customWeight) { weight = weight * (gameplayCustomManager.playerWeightPercent / 100.f); } - if ( stats[player.playernum]->EFFECTS[EFF_FAST] && !stats[player.playernum]->EFFECTS[EFF_SLOW] ) + if ( stats[player.playernum]->getEffectActive(EFF_FAST) && !stats[player.playernum]->getEffectActive(EFF_SLOW) ) { weight = weight * 0.5; } + if ( stats[player.playernum]->getEffectActive(EFF_LIGHTEN_LOAD) > 0 ) + { + weight = weight * (100 - std::min(100, std::max(0, (int)stats[player.playernum]->getEffectActive(EFF_LIGHTEN_LOAD)))) / 100.0; + } return weight; } @@ -3320,31 +4360,110 @@ real_t Player::PlayerMovement_t::getSpeedFactor(real_t weightratio, Sint32 DEX) { real_t slowSpeedPenalty = 0.0; real_t maxSpeed = getMaximumSpeed(); - if ( !stats[player.playernum]->EFFECTS[EFF_FAST] && stats[player.playernum]->EFFECTS[EFF_SLOW] ) + if ( !stats[player.playernum]->getEffectActive(EFF_FAST) && stats[player.playernum]->getEffectActive(EFF_SLOW) ) { DEX = std::min(DEX - 3, -2); slowSpeedPenalty = 2.0; } - else if ( stats[player.playernum]->EFFECTS[EFF_FAST] && !stats[player.playernum]->EFFECTS[EFF_SLOW] ) + else if ( !stats[player.playernum]->getEffectActive(EFF_FAST) && stats[player.playernum]->getEffectActive(EFF_DISRUPTED) ) + { + if ( stats[player.playernum]->getEffectActive(EFF_DISRUPTED) == 1 ) + { + DEX = std::min(DEX - 3, -2); + slowSpeedPenalty = 1.0; + } + else + { + DEX = std::min(DEX - 3, -2); + slowSpeedPenalty = 2.0; + } + } + else if ( stats[player.playernum]->getEffectActive(EFF_FAST) + && !stats[player.playernum]->getEffectActive(EFF_SLOW) + && !stats[player.playernum]->getEffectActive(EFF_DISRUPTED) ) + { + maxSpeed += 1.0; + } + else if ( player.mechanics.getBreakableCounterTier() >= 4 ) { maxSpeed += 1.0; } + real_t speedFactor = std::min((((DEX) * .4) + 8.5 - slowSpeedPenalty) * weightratio, maxSpeed); /*if ( DEX <= 5 ) { speedFactor = std::min(((DEX * 0.5) + 8.5) * weightratio, maxSpeed); }*/ - if ( !stats[player.playernum]->EFFECTS[EFF_DASH] && stats[player.playernum]->EFFECTS[EFF_KNOCKBACK] ) + if ( !stats[player.playernum]->getEffectActive(EFF_DASH) && stats[player.playernum]->getEffectActive(EFF_KNOCKBACK) ) { speedFactor = std::min(speedFactor, 5.0); } - - - for ( node_t* node = stats[player.playernum]->inventory.first; node != NULL; node = node->next ) + if ( stats[player.playernum]->getEffectActive(EFF_BASTION_MUSHROOM) + || stats[player.playernum]->getEffectActive(EFF_BASTION_ROOTS) ) { - Item* item = (Item*)node->element; - if ( item != NULL ) + speedFactor *= 0.3; + } + if ( stats[player.playernum]->type == SALAMANDER && stats[player.playernum]->getEffectActive(EFF_SALAMANDER_HEART) ) + { + if ( Uint8 effectStrength = stats[player.playernum]->getEffectActive(EFF_SALAMANDER_HEART) ) + { + if ( effectStrength == 2 ) + { + speedFactor += std::max(1.0, (speedFactor * .1)); + } + else + { + real_t ratioLimit = 0.5; + if ( effectStrength == 4 ) + { + speedFactor *= ratioLimit; + } + else if ( effectStrength == 3 ) + { + speedFactor *= 0.75; + } + } + } + } + + if ( stats[player.playernum]->type == MYCONID + && !stats[player.playernum]->helmet + && stats[player.playernum]->getEffectActive(EFF_GROWTH) > 1 ) + { + int bonus = std::min(3, stats[player.playernum]->getEffectActive(EFF_GROWTH) - 1); + speedFactor *= 1.0 - (bonus * 0.10); + } + if ( stats[player.playernum]->type == DRYAD + && !stats[player.playernum]->helmet + && stats[player.playernum]->getEffectActive(EFF_GROWTH) > 1 ) + { + int bonus = std::min(3, stats[player.playernum]->getEffectActive(EFF_GROWTH) - 1); + speedFactor *= 1.0 - (bonus * 0.05); + } + + if ( stats[player.playernum]->getEffectActive(EFF_NIMBLENESS) ) + { + real_t bonus = 0.025 * (stats[player.playernum]->getEffectActive(EFF_NIMBLENESS) & 0xF); + speedFactor *= 1.0 + bonus; + speedFactor = std::min(speedFactor, maxSpeed); + } + + if ( int effectStrength = player.mechanics.getBreakableCounterTier() ) + { + speedFactor *= 1.0 + effectStrength * 0.05; + speedFactor = std::min(speedFactor, maxSpeed); + } + + if ( stats[player.playernum]->getEffectActive(EFF_MAXIMISE) ) + { + speedFactor *= 1.0 - 0.1 * std::min(5, (stats[player.playernum]->getEffectActive(EFF_MAXIMISE) & 0xF)); + } + + for ( node_t* node = stats[player.playernum]->inventory.first; node != NULL; node = node->next ) + { + Item* item = (Item*)node->element; + if ( item != NULL ) { if ( item->type == TOOL_PLAYER_LOOT_BAG ) { @@ -3391,22 +4510,87 @@ void Player::PlayerMovement_t::handlePlayerMovement(bool useRefreshRateDelta) // calculate movement forces bool allowMovement = my->isMobile() && playerAllowedMovement(player.playernum); - bool pacified = stats[PLAYER_NUM]->EFFECTS[EFF_PACIFY]; - bool rooted = stats[PLAYER_NUM]->EFFECTS[EFF_ROOTED]; + bool pacified = stats[PLAYER_NUM]->getEffectActive(EFF_PACIFY) > 0; + bool rooted = stats[PLAYER_NUM]->getEffectActive(EFF_ROOTED) > 0; if ( rooted ) { allowMovement = false; } if ( !allowMovement && pacified && !rooted ) { - if ( !stats[PLAYER_NUM]->EFFECTS[EFF_PARALYZED] && !stats[PLAYER_NUM]->EFFECTS[EFF_STUNNED] - && !stats[PLAYER_NUM]->EFFECTS[EFF_ASLEEP] ) + if ( !stats[PLAYER_NUM]->getEffectActive(EFF_PARALYZED) && !stats[PLAYER_NUM]->getEffectActive(EFF_STUNNED) + && !stats[PLAYER_NUM]->getEffectActive(EFF_ASLEEP) ) { allowMovement = true; } } - if ( ((!player.usingCommand() && player.bControlEnabled && !gamePaused) || pacified) + static ConsoleVariable cvar_map_tile_slippery("/map_tile_slippery", 0.99); + static ConsoleVariable cvar_map_tile_greasy("/map_tile_greasy", 0.99); + static ConsoleVariable cvar_map_tile_slow("/map_tile_slow", 0.25); + real_t movementDrag = 0.75; + { + if ( map.tileHasAttribute(static_cast(my->x / 16), static_cast(my->y / 16), 0, map_t::TILE_ATTRIBUTE_SLIPPERY) ) + { + movementDrag = *cvar_map_tile_slippery; + } + else if ( map.tileHasAttribute(static_cast(my->x / 16), static_cast(my->y / 16), 0, map_t::TILE_ATTRIBUTE_GREASE) ) + { + movementDrag = 0.99; + } + else if ( stats[PLAYER_NUM]->getEffectActive(EFF_MAGIC_GREASE) ) + { + movementDrag = 0.95; + } + } + + bool cleats = false; + if ( stats[PLAYER_NUM]->shoes && stats[PLAYER_NUM]->shoes->type == CLEAT_BOOTS + && players[PLAYER_NUM]->entity && players[PLAYER_NUM]->entity->effectShapeshift == NOTHING ) + { + cleats = true; + if ( movementDrag >= 0.85 ) + { + movementDrag = 0.85; + } + } + + static std::map dragToSpeedFactor = + { + {99, 33}, + {98, 16.33333}, + {97, 10.77778}, + {96, 8}, + {95, 6.333333333}, + {94, 5.222222222}, + {93, 4.428571429}, + {92, 3.833333333}, + {91, 3.37037037}, + {90, 3}, + {89, 2.696969697}, + {88, 2.444444444}, + {87, 2.230769231}, + {86, 2.047619048}, + {85, 1.888888889}, + {84, 1.75}, + {83, 1.62745098}, + {82, 1.518518519}, + {81, 1.421052632}, + {80, 1.333333333}, + {79, 1.253968254}, + {78, 1.181818182}, + {77, 1.115942029}, + {76, 1.055555556}, + {75, 1} + }; + real_t speedFactorMult = 1.0; + auto find = dragToSpeedFactor.find(static_cast(100 * movementDrag)); + if ( find != dragToSpeedFactor.end() ) + { + speedFactorMult = 1 / find->second; + } + + if ( ((!gamePaused) || pacified) && allowMovement ) { //x_force and y_force represent the amount of percentage pushed on that respective axis. Given a keyboard, it's binary; either you're pushing "move left" or you aren't. On an analog stick, it can range from whatever value to whatever. @@ -3417,7 +4601,7 @@ void Player::PlayerMovement_t::handlePlayerMovement(bool useRefreshRateDelta) { x_force = 0.f; y_force = -0.1; - if ( stats[PLAYER_NUM]->EFFECTS[EFF_CONFUSED] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_CONFUSED) ) { y_force *= -1; } @@ -3442,59 +4626,62 @@ void Player::PlayerMovement_t::handlePlayerMovement(bool useRefreshRateDelta) lateralMultiplier = 0.0; } } - if ( stats[PLAYER_NUM]->EFFECTS[EFF_DASH] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) ) { backpedalMultiplier = 1.25; } - if ( !inputs.hasController(PLAYER_NUM) ) + if ( !player.usingCommand() && player.bControlEnabled ) { - if ( !stats[PLAYER_NUM]->EFFECTS[EFF_CONFUSED] ) + if ( !inputs.hasController(PLAYER_NUM) ) { - //Normal controls. - x_force = (input.binary("Move Right") - input.binary("Move Left")); - y_force = input.binary("Move Forward") - (double)input.binary("Move Backward") * backpedalMultiplier; - if ( noclip ) + if ( !stats[PLAYER_NUM]->getEffectActive(EFF_CONFUSED) ) { - if ( keystatus[SDLK_LSHIFT] ) + //Normal controls. + x_force = (input.binary("Move Right") - input.binary("Move Left")); + y_force = input.binary("Move Forward") - (double)input.binary("Move Backward") * backpedalMultiplier; + if ( noclip ) { - x_force = x_force * 0.5; - y_force = y_force * 0.5; + if ( keystatus[SDLK_LSHIFT] ) + { + x_force = x_force * 0.5; + y_force = y_force * 0.5; + } } } + else + { + //Confused controls. + x_force = input.binary("Move Left") - input.binary("Move Right"); + y_force = input.binary("Move Backward") - (double)input.binary("Move Forward") * backpedalMultiplier; + } } - else - { - //Confused controls. - x_force = input.binary("Move Left") - input.binary("Move Right"); - y_force = input.binary("Move Backward") - (double)input.binary("Move Forward") * backpedalMultiplier; - } - } - - if ( inputs.hasController(PLAYER_NUM) /*&& !input.binary("Move Left") && !input.binary("Move Right")*/ ) - { - x_force = inputs.getController(PLAYER_NUM)->getLeftXPercentForPlayerMovement(); - if ( stats[PLAYER_NUM]->EFFECTS[EFF_CONFUSED] ) + if ( inputs.hasController(PLAYER_NUM) /*&& !input.binary("Move Left") && !input.binary("Move Right")*/ ) { - x_force *= -1; - } - } - if ( inputs.hasController(PLAYER_NUM) /*&& !input.binary("Move Forward") && !input.binary("Move Backward")*/ ) - { - y_force = inputs.getController(PLAYER_NUM)->getLeftYPercentForPlayerMovement(); + x_force = inputs.getController(PLAYER_NUM)->getLeftXPercentForPlayerMovement(PLAYER_NUM); - if ( stats[PLAYER_NUM]->EFFECTS[EFF_CONFUSED] ) - { - y_force *= -1; + if ( stats[PLAYER_NUM]->getEffectActive(EFF_CONFUSED) ) + { + x_force *= -1; + } } - - if ( y_force < 0 ) + if ( inputs.hasController(PLAYER_NUM) /*&& !input.binary("Move Forward") && !input.binary("Move Backward")*/ ) { - y_force *= backpedalMultiplier; //Move backwards more slowly. + y_force = inputs.getController(PLAYER_NUM)->getLeftYPercentForPlayerMovement(PLAYER_NUM); + + if ( stats[PLAYER_NUM]->getEffectActive(EFF_CONFUSED) ) + { + y_force *= -1; + } + + if ( y_force < 0 ) + { + y_force *= backpedalMultiplier; //Move backwards more slowly. + } } + x_force *= lateralMultiplier; } - x_force *= lateralMultiplier; } real_t speedFactor = getSpeedFactor(weightratio, statGetDEX(stats[PLAYER_NUM], players[PLAYER_NUM]->entity)); @@ -3527,17 +4714,35 @@ void Player::PlayerMovement_t::handlePlayerMovement(bool useRefreshRateDelta) } messagePlayer(0, MESSAGE_DEBUG, "New: %.3f | Old: %.3f | (%+.3f)", speedFactor, speedFactorOld, 100.0 * ((speedFactor / speedFactorOld) - 1.0)); } - if ( stats[PLAYER_NUM]->EFFECTS[EFF_DASH] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) ) { PLAYER_VELX += my->monsterKnockbackVelocity * cos(my->monsterKnockbackTangentDir) * refreshRateDelta; PLAYER_VELY += my->monsterKnockbackVelocity * sin(my->monsterKnockbackTangentDir) * refreshRateDelta; - my->monsterKnockbackVelocity *= pow(0.95, refreshRateDelta); + real_t rate = 0.95; + if ( movementDrag > 0.8 ) + { + rate = 0.8; + } + /*if ( cleats ) + { + rate *= 0.5; + }*/ + my->monsterKnockbackVelocity *= pow(rate, refreshRateDelta); } - else if ( stats[PLAYER_NUM]->EFFECTS[EFF_KNOCKBACK] ) + else if ( stats[PLAYER_NUM]->getEffectActive(EFF_KNOCKBACK) ) { PLAYER_VELX += my->monsterKnockbackVelocity * cos(my->monsterKnockbackTangentDir) * refreshRateDelta; PLAYER_VELY += my->monsterKnockbackVelocity * sin(my->monsterKnockbackTangentDir) * refreshRateDelta; - my->monsterKnockbackVelocity *= pow(0.95, refreshRateDelta); + real_t rate = 0.95; + if ( movementDrag > 0.8 ) + { + rate = 0.8; + } + if ( cleats ) + { + rate *= 0.5; + } + my->monsterKnockbackVelocity *= pow(rate, refreshRateDelta); } else { @@ -3557,15 +4762,54 @@ void Player::PlayerMovement_t::handlePlayerMovement(bool useRefreshRateDelta) my->playerStrafeVelocity = 0.0f; } + if ( map.tileHasAttribute(static_cast(my->x / 16), static_cast(my->y / 16), 0, map_t::TILE_ATTRIBUTE_SLOW) ) + { + if ( !isLevitating(stats[PLAYER_NUM]) ) + { + speedFactor *= *cvar_map_tile_slow; + } + } + + speedFactor *= speedFactorMult; speedFactor *= refreshRateDelta; - PLAYER_VELX += y_force * cos(my->yaw) * .045 * speedFactor / (1 + (stats[PLAYER_NUM]->defending || stats[PLAYER_NUM]->sneaking == 1)); - PLAYER_VELY += y_force * sin(my->yaw) * .045 * speedFactor / (1 + (stats[PLAYER_NUM]->defending || stats[PLAYER_NUM]->sneaking == 1)); - PLAYER_VELX += x_force * cos(my->yaw + PI / 2) * .0225 * speedFactor / (1 + (stats[PLAYER_NUM]->defending || stats[PLAYER_NUM]->sneaking == 1)); - PLAYER_VELY += x_force * sin(my->yaw + PI / 2) * .0225 * speedFactor / (1 + (stats[PLAYER_NUM]->defending || stats[PLAYER_NUM]->sneaking == 1)); + real_t defendPenalty = (stats[PLAYER_NUM]->defending || stats[PLAYER_NUM]->sneaking == 1) ? 1.0 : 0.0; + if ( stats[PLAYER_NUM]->sneaking == 1 && !stats[PLAYER_NUM]->defending && stats[PLAYER_NUM]->type == GREMLIN ) + { + defendPenalty = 0.5; + } + if ( stats[PLAYER_NUM]->defending && !stats[PLAYER_NUM]->sneaking + && stats[PLAYER_NUM]->shield && itemTypeIsFoci(stats[PLAYER_NUM]->shield->type) ) + { + if ( int spellID = getSpellIDFromFoci(stats[PLAYER_NUM]->shield->type) ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + real_t modifier = std::min(100, stats[PLAYER_NUM]->getModifiedProficiency(spell->skillID)) / 100.f; + defendPenalty *= std::max(0.0, 1.0 - modifier); + } + } + } + + real_t defendSpeed = (1.0 + defendPenalty); + PLAYER_VELX += y_force * cos(my->yaw) * .045 * speedFactor / (defendSpeed); + PLAYER_VELY += y_force * sin(my->yaw) * .045 * speedFactor / (defendSpeed); + PLAYER_VELX += x_force * cos(my->yaw + PI / 2) * .0225 * speedFactor / (defendSpeed); + PLAYER_VELY += x_force * sin(my->yaw + PI / 2) * .0225 * speedFactor / (defendSpeed); + + } + + my->processEntityWind(); + + PLAYER_VELX *= pow(movementDrag, refreshRateDelta); + PLAYER_VELY *= pow(movementDrag, refreshRateDelta); + real_t magnitude = sqrt(pow(PLAYER_VELX, 2) + pow(PLAYER_VELY, 2)); + const real_t magnitudeMax = 5.0; + if ( magnitude > magnitudeMax ) + { + PLAYER_VELX *= (magnitudeMax / magnitude); + PLAYER_VELY *= (magnitudeMax / magnitude); } - PLAYER_VELX *= pow(0.75, refreshRateDelta); - PLAYER_VELY *= pow(0.75, refreshRateDelta); /*if ( keystatus[SDLK_g] ) { @@ -3605,8 +4849,19 @@ void Player::PlayerMovement_t::handlePlayerMovement(bool useRefreshRateDelta) bool swimming = isPlayerSwimming(); if ( swimming && !amuletwaterbreathing ) { - PLAYER_VELX *= (((stats[PLAYER_NUM]->getModifiedProficiency(PRO_SWIMMING) / 100.f) * 50.f) + 50) / 100.f; - PLAYER_VELY *= (((stats[PLAYER_NUM]->getModifiedProficiency(PRO_SWIMMING) / 100.f) * 50.f) + 50) / 100.f; + //PLAYER_VELX *= (/*((stats[PLAYER_NUM]->getModifiedProficiency(PRO_LEGACY_SWIMMING) / 100.f) * 50.f) +*/ 50) / 100.f; + //PLAYER_VELY *= (/*((stats[PLAYER_NUM]->getModifiedProficiency(PRO_LEGACY_SWIMMING) / 100.f) * 50.f) +*/ 50) / 100.f; + if ( stats[PLAYER_NUM]->type == HUMAN + || stats[PLAYER_NUM]->type == RAT ) + { + PLAYER_VELX *= (((50 / 100.f) * 50.f) + 50) / 100.f; + PLAYER_VELY *= (((50 / 100.f) * 50.f) + 50) / 100.f; + } + else + { + PLAYER_VELX *= (((25 / 100.f) * 50.f) + 50) / 100.f; + PLAYER_VELY *= (((25 / 100.f) * 50.f) + 50) / 100.f; + } if ( stats[PLAYER_NUM]->type == SKELETON ) { @@ -3622,8 +4877,11 @@ void Player::PlayerMovement_t::handlePlayerMovement(bool useRefreshRateDelta) } } +real_t Player::PlayerMovement_t::minimiseMaximiseCameraZ = 0.4; + void Player::PlayerMovement_t::handlePlayerCameraPosition(bool useRefreshRateDelta) { + players[player.playernum]->worldUI.modifiedTooltipDrawHeight = 0.0; if ( !players[player.playernum]->isLocalPlayer() ) { return; @@ -3672,9 +4930,51 @@ void Player::PlayerMovement_t::handlePlayerCameraPosition(bool useRefreshRateDel { cameraSetpointZ -= 2; } + else if ( playerRace == GREMLIN ) + { + cameraSetpointZ -= 1.0; + players[player.playernum]->worldUI.modifiedTooltipDrawHeight = 3.0; + } + else if ( playerRace == GNOME ) + { + cameraSetpointZ -= 2.0; + players[player.playernum]->worldUI.modifiedTooltipDrawHeight = 3.0; + } + else if ( playerRace == DRYAD ) + { + if ( my->z >= 1.5 ) + { + cameraSetpointZ -= 3.0; + players[player.playernum]->worldUI.modifiedTooltipDrawHeight = 3.0; + } + else + { + cameraSetpointZ -= 2.0; + } + } + else if ( playerRace == MYCONID ) + { + if ( my->z >= 0.5 ) + { + cameraSetpointZ -= 1.0; + players[player.playernum]->worldUI.modifiedTooltipDrawHeight = 3.0; + } + else + { + cameraSetpointZ -= 1.0; + } + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_MINIMISE) ) + { + cameraSetpointZ += Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[PLAYER_NUM]->getEffectActive(EFF_MINIMISE) & 0xF); + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_MAXIMISE) ) + { + cameraSetpointZ -= Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[PLAYER_NUM]->getEffectActive(EFF_MAXIMISE) & 0xF); + } real_t diff = abs(PLAYER_CAMERAZ_ACCEL - cameraSetpointZ); - if ( diff > 0.01 ) + if ( diff > 0.01 && abs(my->creatureHoverZ) < 0.01 ) { real_t rateChange = std::min(2.0, std::max(0.3, diff * 0.5)) * refreshRateDelta; @@ -4437,7 +5737,7 @@ void doStatueEditor(int player) } } -int playerHeadSprite(Monster race, sex_t sex, int appearance, int frame) { +int playerHeadSprite(Monster race, sex_t sex, int appearance, int frame, int player) { if (race == HUMAN) { if (appearance < 5) { return 113 + 12 * sex + appearance; @@ -4494,119 +5794,1433 @@ int playerHeadSprite(Monster race, sex_t sex, int appearance, int frame) { else if (race == AUTOMATON) { return sex == FEMALE ? 770 : 742; } + else if ( race == DRYAD ) { + return sex == FEMALE ? 1992 : 1963; + } + else if ( race == MYCONID ) { + return sex == MALE ? 1998 : 1997; + } + else if ( race == GREMLIN ) { + return sex == FEMALE ? 2048 : 2047; + } + else if ( race == SALAMANDER ) { + if ( player >= 0 && player < MAXPLAYERS ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_SALAMANDER_HEART); + if ( effectStrength == 1 || effectStrength == 2 ) + { + return sex == FEMALE ? 2017 : 2016; + } + else if ( effectStrength == 3 || effectStrength == 4 ) + { + return sex == FEMALE ? 2019 : 2018; + } + } + return sex == FEMALE ? 2015 : 2014; + } + else if ( race == GNOME ) + { + return sex == FEMALE ? 2214 : 2213; + } else { return 481; // shadow head due to unknown creature } } -void actPlayer(Entity* my) +bool shieldSpriteIsSpellbook(int sprite) { - if (!my) - { - return; - } - if ( logCheckObstacle ) + static std::set spellbookSprites; + if ( spellbookSprites.size() == 0 ) { - if ( ticks % 50 == 0 ) + for ( int i = 0; i < NUMITEMS; ++i ) { - messagePlayer(0, MESSAGE_DEBUG, "checkObstacle() calls/sec: %d", logCheckObstacleCount); - logCheckObstacleCount = 0; + if ( items[i].category == SPELLBOOK ) + { + for ( int j = 0; j < items[i].variations; ++j ) + { + spellbookSprites.insert(items[i].index + j); + if ( items[i].indexShort >= 0 ) + { + spellbookSprites.insert(items[i].indexShort + j); + } + } + } } } - if ( spamming && my->ticks % 2 == 0 ) + + return spellbookSprites.find(sprite) != spellbookSprites.end(); +} + +bool shieldSpriteAllowedImpForm(int sprite) +{ + static std::set allowedShieldSprites; + if ( allowedShieldSprites.size() == 0 ) { - for (int i = 0; i < 1; ++i) + for ( int i = 0; i < NUMITEMS; ++i ) { - char s[64] = ""; - char alphanum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - - for ( int j = 0; j < 63; ++j ) { - s[j] = alphanum[local_rng.rand() % (sizeof(alphanum) - 1)]; - } - Uint32 totalSize = 0; - for ( size_t c = 0; c < HASH_SIZE; ++c ) { - totalSize += list_Size(&ttfTextHash[c]); + if ( items[i].category == SPELLBOOK || itemTypeIsFoci(ItemType(i)) ) + { + for ( int j = 0; j < items[i].variations; ++j ) + { + allowedShieldSprites.insert(items[i].index + j); + if ( items[i].indexShort >= 0 ) + { + allowedShieldSprites.insert(items[i].indexShort + j); + } + } } - messagePlayer(0, MESSAGE_DEBUG, "IMGREF: %d, total size: %d", imgref, totalSize); - s[63] = '\0'; - messagePlayer(0, MESSAGE_DEBUG, "%s", s); - //messagePlayer(0, "Lorem ipsum dolor sit amet, dico accusam reprehendunt ne mea, ea est illum tincidunt voluptatibus. Ne labore voluptua eos, nostro fierent mnesarchum an mei, cu mea dolor verear epicuri. Est id iriure principes, unum cotidieque qui te. An sit tractatos complectitur."); } } - if ( autoLimbReload && ticks % 20 == 0 && (PLAYER_NUM == clientnum) ) + + return allowedShieldSprites.find(sprite) != allowedShieldSprites.end(); +} + +bool weaponSpriteAllowedImpForm(int sprite) +{ + static std::set allowedWeaponSprites; + if ( allowedWeaponSprites.size() == 0 ) { - if ( ticks % 40 == 0 ) - { - consoleCommand("/reloadlimbs"); - } - else + for ( int i = 0; i < NUMITEMS; ++i ) { - consoleCommand("/reloadequipmentoffsets"); + if ( items[i].category == MAGICSTAFF ) + { + for ( int j = 0; j < items[i].variations; ++j ) + { + allowedWeaponSprites.insert(items[i].index + j); + if ( items[i].indexShort >= 0 ) + { + allowedWeaponSprites.insert(items[i].indexShort + j); + } + } + } } } - Entity* entity; - Entity* entity2 = nullptr; - Entity* rightbody = nullptr; - Entity* weaponarm = nullptr; - Entity* shieldarm = nullptr; - Entity* additionalLimb = nullptr; - Entity* torso = nullptr; - node_t* node; - int i, bodypart; - double dist = 0; - bool wearingring = false; - bool levitating = false; - bool isHumanoid = true; - bool showEquipment = true; - if ( PLAYER_NUM < 0 || PLAYER_NUM >= MAXPLAYERS ) - { - return; - } + return allowedWeaponSprites.find(sprite) != allowedWeaponSprites.end(); +} - Monster playerRace = HUMAN; - int spriteTorso = 106 + 12 * stats[PLAYER_NUM]->sex; - int spriteLegRight = 107 + 12 * stats[PLAYER_NUM]->sex; - int spriteLegLeft = 108 + 12 * stats[PLAYER_NUM]->sex; - int spriteArmRight = 109 + 12 * stats[PLAYER_NUM]->sex; - int spriteArmLeft = 110 + 12 * stats[PLAYER_NUM]->sex; - int playerAppearance = stats[PLAYER_NUM]->stat_appearance; - - if ( my->effectShapeshift != NOTHING ) +void playerDebugTests(Entity* my) +{ +#ifndef NDEBUG + if ( !my ) { return; } + if ( !(svFlags & SV_FLAG_CHEATS) ) { - playerRace = static_cast(my->effectShapeshift); - stats[PLAYER_NUM]->type = playerRace; + return; } - else if ( stats[PLAYER_NUM]->playerRace > 0 || stats[PLAYER_NUM]->EFFECTS[EFF_POLYMORPH] || my->effectPolymorph != NOTHING ) + if ( multiplayer == CLIENT || intro ) { - playerRace = my->getMonsterFromPlayerRace(stats[PLAYER_NUM]->playerRace); - if ( my->effectPolymorph != NOTHING ) - { - if ( my->effectPolymorph > NUMMONSTERS ) - { - playerRace = HUMAN; - playerAppearance = my->effectPolymorph - 100; - } - else - { - playerRace = static_cast(my->effectPolymorph); - } - } - if ( stats[PLAYER_NUM]->stat_appearance == 0 || my->effectPolymorph != NOTHING ) - { - stats[PLAYER_NUM]->type = playerRace; - } - else - { - stats[PLAYER_NUM]->type = HUMAN; // appearance of 1 is aesthetic only - } + return; } - else + + static ConsoleVariable cvar_test_frameskip("/test_frameskip", false); + if ( *cvar_test_frameskip && ticks % (5 * TICKS_PER_SECOND) == 0 ) { - stats[PLAYER_NUM]->type = HUMAN; + SDL_Delay(50); } - if ( stats[PLAYER_NUM]->type == RAT || stats[PLAYER_NUM]->type == SPIDER ) + static ConsoleVariable cvar_test_xp("/test_xp", 0); + struct XPGain_t + { + int numKills = 0; + int numXP = 0; + }; + static std::map xpGained; + static std::map> xpGainedBiome; + static std::map killingDoneLookup; + if ( currentlevel == 0 ) + { + xpGained.clear(); + killingDoneLookup.clear(); + xpGainedBiome.clear(); + } + else if ( *cvar_test_xp && ticks % 2 == 0 && ticks > 5 && my->skill[2] == 0 ) + { + bool killingDone = true; + for ( auto node = map.entities->first; node; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->behavior == &actMonster ) + { + if ( entity->getStats() && entity->getStats()->HP > 0 ) + { + messagePlayer(0, MESSAGE_DEBUG, "Kill mon: %d", entity->getMonsterTypeFromSprite()); + entity->setHP(0); + Sint32 oldXP = stats[0]->EXP; + if ( *cvar_test_xp == 3 || *cvar_test_xp == 5 ) + { + if ( local_rng.rand() % 4 > 0 ) + { + my->awardXP(entity, true, true); + } + } + else + { + my->awardXP(entity, true, true); + } + if ( stats[0]->EXP > oldXP ) + { + xpGained[monstertypename[entity->getStats()->type]].numXP += stats[0]->EXP - oldXP; + xpGained[monstertypename[entity->getStats()->type]].numKills++; + xpGainedBiome[map.filename][monstertypename[entity->getStats()->type]].numXP += stats[0]->EXP - oldXP; + xpGainedBiome[map.filename][monstertypename[entity->getStats()->type]].numKills++; + } + killingDone = false; + break; + } + } + else if ( entity->behavior == &actColliderDecoration && entity->isDamageableCollider() ) + { + entity->colliderCurrentHP = 0; + entity->colliderKillerUid = my->getUID(); + killingDone = false; + break; + } + else if ( entity->behavior == &actHeadstone && !entity->skill[3] ) + { + entity->flags[INVISIBLE] = false; + entity->skill[28] = 2; + + /*Entity* oldSelected = selectedEntity[0]; + selectedEntity[0] = entity; + bool oldInRange = inrange[0]; + inrange[0] = true; + actSink(entity); + inrange[0] = oldInRange; + selectedEntity[0] = oldSelected;*/ + killingDone = false; + break; + } + else if ( entity->behavior == &actSink ) + { + if ( entity->skill[0] > 0 ) + { + if ( *cvar_test_xp == 2 ) + { + entity->skill[0]--; + if ( local_rng.rand() % 2 == 0 ) + { + // spawn slime + int ox = entity->x / 16; + int oy = entity->y / 16; + Entity* monster = summonMonster(SLIME, ox * 16 + 8, oy * 16 + 8); + if ( monster ) + { + auto& rng = entity->entity_rng ? *entity->entity_rng : local_rng; + monster->seedEntityRNG(rng.getU32()); + slimeSetType(monster, monster->getStats(), true, &rng); + } + } + } + else + { + Entity* oldSelected = selectedEntity[0]; + selectedEntity[0] = entity; + bool oldInRange = inrange[0]; + inrange[0] = true; + actSink(entity); + inrange[0] = oldInRange; + selectedEntity[0] = oldSelected; + } + killingDone = false; + break; + } + } + else if ( entity->behavior == &actFountain ) + { + if ( entity->skill[0] > 0 ) + { + Entity* oldSelected = selectedEntity[0]; + selectedEntity[0] = entity; + bool oldInRange = inrange[0]; + inrange[0] = true; + actFountain(entity); + inrange[0] = oldInRange; + selectedEntity[0] = oldSelected; + killingDone = false; + break; + } + } + else if ( entity->behavior == &actSummonTrap ) + { + if ( entity->skill[7] == 0 ) + { + entity->skill[8] = 0; + if ( !entity->skill[4] ) + { + entity->skill[28] = 2; + } + else + { + entity->skill[28] = 1; + } + killingDone = false; + break; + } + } + } + } + if ( killingDone && killingDoneLookup.find(currentlevel + (secretlevel ? 100 : 0)) == killingDoneLookup.end() ) + { + killingDoneLookup[currentlevel + (secretlevel ? 100 : 0)] = true; + + messagePlayer(0, MESSAGE_DEBUG, "Killing done"); + if ( currentlevel == 35 ) + { + std::vector> sortedXP; + + for ( auto& pair : xpGained ) + { + sortedXP.push_back(std::make_pair(pair.first, pair.second.numXP / (real_t)(std::max(1, pair.second.numKills)))); + } + std::sort(sortedXP.begin(), sortedXP.end(), [](std::pair& a, std::pair& b) { + return a.second > b.second; + }); + for ( auto& p : sortedXP ) + { + auto pair = xpGained.find(p.first); + if ( pair != xpGained.end() ) + { + printlog("Total XP: %s: XP: %d Kills: %d: Ratio: %.2f", pair->first.c_str(), pair->second.numXP, pair->second.numKills, (real_t)pair->second.numXP / (real_t)std::max(1, pair->second.numKills)); + } + } + for ( auto& pair : xpGainedBiome ) + { + sortedXP.clear(); + for ( auto& pair2 : pair.second ) + { + sortedXP.push_back(std::make_pair(pair2.first, pair2.second.numXP / (real_t)(std::max(1, pair2.second.numKills)))); + } + std::sort(sortedXP.begin(), sortedXP.end(), [](std::pair& a, std::pair& b) { + return a.second > b.second; + }); + for ( auto& p : sortedXP ) + { + auto pair2 = pair.second.find(p.first); + if ( pair2 != pair.second.end() ) + { + printlog("%s XP: %s: XP: %d Kills: %d: Ratio: %.2f", pair.first.c_str(), pair2->first.c_str(), pair2->second.numXP, pair2->second.numKills, (real_t)pair2->second.numXP / (real_t)std::max(1, pair2->second.numKills)); + } + } + } + } + else + { + if ( currentlevel == 20 ) + { + consoleCommand("/jumplevel 6"); + } + else + { + if ( *cvar_test_xp == 4 || *cvar_test_xp == 5 ) + { + if ( secretlevel ) + { + consoleCommand("/togglesecretlevel"); + consoleCommand("/jumplevel -1"); + } + else + { + if ( currentlevel == 2 || currentlevel == 8 || currentlevel == 9 || currentlevel == 13 + || currentlevel == 16 + || currentlevel == 28 || currentlevel == 33 ) + { + consoleCommand("/togglesecretlevel"); + } + consoleCommand("/nextlevel"); + } + } + else + { + consoleCommand("/nextlevel"); + } + } + } + } + } +#endif +} + +void actPlayer(Entity* my) +{ + if (!my) + { + return; + } + if ( logCheckObstacle ) + { + if ( ticks % 50 == 0 ) + { + messagePlayer(0, MESSAGE_DEBUG, "checkObstacle() calls/sec: %d", logCheckObstacleCount); + logCheckObstacleCount = 0; + } + } + if ( spamming && my->ticks % 2 == 0 ) + { + for (int i = 0; i < 1; ++i) + { + char s[64] = ""; + char alphanum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + for ( int j = 0; j < 63; ++j ) { + s[j] = alphanum[local_rng.rand() % (sizeof(alphanum) - 1)]; + } + Uint32 totalSize = 0; + for ( size_t c = 0; c < HASH_SIZE; ++c ) { + totalSize += list_Size(&ttfTextHash[c]); + } + messagePlayer(0, MESSAGE_DEBUG, "IMGREF: %d, total size: %d", imgref, totalSize); + s[63] = '\0'; + messagePlayer(0, MESSAGE_DEBUG, "%s", s); + //messagePlayer(0, "Lorem ipsum dolor sit amet, dico accusam reprehendunt ne mea, ea est illum tincidunt voluptatibus. Ne labore voluptua eos, nostro fierent mnesarchum an mei, cu mea dolor verear epicuri. Est id iriure principes, unum cotidieque qui te. An sit tractatos complectitur."); + } + } + if ( autoLimbReload && ticks % 20 == 0 && (PLAYER_NUM == clientnum) ) + { + if ( ticks % 40 == 0 ) + { + consoleCommand("/reloadlimbs"); + } + else + { + consoleCommand("/reloadequipmentoffsets"); + } + } + +#ifndef NDEBUG + //if ( my->ticks == 1 ) + //{ + // //consoleCommand("/allspells4"); + // //if ( PLAYER_NUM == 0 ) + // //{ + // // //consoleCommand("/god"); + // //} + //} +#endif + + if ( svFlags & SV_FLAG_CHEATS ) + { + if ( PLAYER_NUM == 0 ) + { + playerDebugTests(my); + } + } + + static ConsoleVariable cvar_pbaoe("/pbaoe", 15); + if ( keystatus[SDLK_x] && enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + //spawnPlayerXP(my->x + 16.0, my->y, 0, 10); + Entity* fx = spawnFlameSprites(my, 288); + /*for ( int i = 0; i < 3; ++i ) + { + Entity* fx1 = createParticleAestheticOrbit(my, 2401, 5 * TICKS_PER_SECOND, PARTICLE_EFFECT_SMITE_PINPOINT); + fx1->yaw = my->yaw + PI / 2 + 2 * i * PI / 3; + fx1->fskill[6] = fx1->yaw; + fx1->skill[3] = my->getUID(); + if ( i != 0 ) + { + fx1->actmagicNoLight = 1; + } + }*/ + + keystatus[SDLK_x] = 0; + Uint32 color = makeColorRGB(255, 255, 255); + if ( *cvar_pbaoe == 0 ) + { + Entity* spellTimer = createParticleTimer(my, 1.25 * TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_IGNITE; + spellTimer->particleTimerCountdownSprite = -1; + color = makeColor(255, 128, 0, 255); + } + else if ( *cvar_pbaoe == 1 ) + { + Entity* spellTimer = createParticleTimer(my, 1.25 * TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SHATTER; + spellTimer->particleTimerCountdownSprite = -1; + color = makeColor(255, 0, 255, 255); + } + + if ( *cvar_pbaoe == 20 ) + { + //Entity* fx = spawnFlameSprites(my, 263); + for ( int i = 0; i < 2; ++i ) + { + if ( Entity* fx = createParticleAestheticOrbit(my, 263, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_HEAT_ORBIT_SPIN) ) + { + fx->flags[SPRITE] = true; + fx->x = my->x; + fx->y = my->y; + fx->z = 7.5; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->vel_z = -0.5; + fx->actmagicOrbitDist = 5; + fx->fskill[2] = my->yaw + PI / 4.0 + i * PI; + fx->yaw = fx->fskill[2]; + fx->fskill[4] = 0.25; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + fx->actmagicNoLight = 1; + + serverSpawnMiscParticles(my, PARTICLE_EFFECT_HEAT_ORBIT_SPIN, 263, 0, fx->skill[0]); + } + } + } + else if ( *cvar_pbaoe == 13 ) + { + Entity* leaf = newEntity(1912, 1, map.entities, nullptr); //Gib entity. + if ( leaf != NULL ) + { + leaf->x = my->x + 40.0 * cos(my->yaw); + leaf->y = my->y + 40.0 * sin(my->yaw); + leaf->z = 5.0; + leaf->fskill[6] = leaf->z; + leaf->fskill[7] = -7.5 - leaf->fskill[6]; + leaf->vel_z = 0.0; + leaf->sizex = 2; + leaf->sizey = 2; + leaf->scalex = 0.5; + leaf->scaley = 0.5; + leaf->scalez = 0.5; + leaf->yaw = local_rng.rand() % 360 * (PI / 180.0); + leaf->skill[0] = 30 * TICKS_PER_SECOND; + leaf->fskill[4] = leaf->x; + leaf->fskill[5] = leaf->y; + leaf->fskill[8] = 12.0 + 0.25 * (local_rng.rand() % 13); // 12-15.0 + leaf->parent = 0; + leaf->behavior = &actLeafParticle; + leaf->flags[NOCLIP_CREATURES] = true; + leaf->flags[UPDATENEEDED] = false; + leaf->flags[NOUPDATE] = true; + leaf->flags[PASSABLE] = true; + leaf->flags[UNCLICKABLE] = true; + if ( multiplayer != CLIENT ) + { + --entity_uids; + } + leaf->setUID(-3); + } + } + else if ( *cvar_pbaoe == 14 ) + { + spawnLeafPile(my->x + 40.0 * cos(my->yaw), my->y + 40.0 * sin(my->yaw), true); + //Entity* leaf = newEntity(1913, 1, map.entities, nullptr); //Gib entity. + //if ( leaf != NULL ) + //{ + // leaf->x = my->x + 40.0 * cos(my->yaw); + // leaf->y = my->y + 40.0 * sin(my->yaw); + // leaf->z = 0.0; + // leaf->yaw = local_rng.rand() % 360 * (PI / 180.0); + // leaf->sizex = 4; + // leaf->sizey = 4; + // leaf->behavior = &actLeafPile; + // leaf->skill[0] = 30 * TICKS_PER_SECOND; + // leaf->flags[NOCLIP_CREATURES] = true; + // leaf->flags[UPDATENEEDED] = true; + // leaf->flags[NOUPDATE] = false; + // leaf->flags[PASSABLE] = true; + // leaf->flags[UNCLICKABLE] = true; + //} + } + else if ( *cvar_pbaoe == 15 ) + { + /*Entity* spellTimer = createParticleTimer(my, TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_BASTION_MUSHROOM; + spellTimer->particleTimerCountdownSprite = -1; + + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 40.0 * cos(my->yaw); + spellTimer->y = my->y + 40.0 * sin(my->yaw);*/ + + //for ( int i = 0; i < 8; ++i ) + //{ + // Entity* fx = createParticleAestheticOrbit(my, 1885, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_MUSHROOM_SPELL); + // fx->x = my->x; + // fx->y = my->y; + // fx->actmagicOrbitDist = 32; + // fx->yaw = my->yaw + (i * PI / 4.0); + // fx->pitch = -PI; + // fx->fskill[4] = fx->yaw; + + // Entity* gib = spawnGib(my, 1885); + // gib->vel_x = 1.5 * cos(fx->yaw); + // gib->vel_y = 1.5 * sin(fx->yaw); + // gib->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; + //} + + //if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, 0.0, TICKS_PER_SECOND * 2, 32) ) + //{ + // fx->actSpriteFollowUID = my->getUID(); + // fx->actSpriteCheckParentExists = 0; + // //fx->scalex = 0.8; + // //fx->scaley = 0.8; + // if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + // { + // //indicator->arc = PI / 2; + // indicator->indicatorColor = makeColorRGB(0, 145, 16); + // indicator->loop = false; + // indicator->gradient = 4; + // indicator->framesPerTick = 2; + // indicator->ticksPerUpdate = 1; + // indicator->delayTicks = 0; + // indicator->expireAlphaRate = 0.95; + // } + //} + //playSoundEntityLocal(my, 169, 128); + //playSoundEntityLocal(my, 717 + local_rng.rand() % 3, 128); + } + else if ( *cvar_pbaoe == 16 ) + { + createParticleBolas(my, 1917, 2 * TICKS_PER_SECOND, nullptr); + } + else if ( *cvar_pbaoe == 7 ) + { + Entity* spellTimer = createParticleTimer(my, 25, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_BOOBY_TRAP; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->particleTimerVariable1 = 20; + spellTimer->particleTimerVariable2 = SPELL_BOOBY_TRAP; + color = makeColor(255, 0, 255, 255); + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 40.0 * cos(my->yaw); + spellTimer->y = my->y + 40.0 * sin(my->yaw); + + if ( Entity* fx = createParticleAOEIndicator(spellTimer, spellTimer->x, spellTimer->y, 0.0, TICKS_PER_SECOND, 24) ) + { + fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + Uint8 r, g, b, a; + getColor(color, &r, &g, &b, &a); + a *= 0.5; + indicator->indicatorColor = makeColor(r, g, b, a); + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + } + } + for ( int i = 0; i < 2; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(spellTimer, spellTimer->x, spellTimer->y, -7.5, TICKS_PER_SECOND, 24) ) + { + fx->actSpriteCheckParentExists = 0; + if ( i == 1 ) + { + fx->pitch = PI; + } + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 15; + } + } + } + } + else if ( *cvar_pbaoe == 3 ) + { + color = makeColor(255, 0, 255, 255); + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, -7.5, TICKS_PER_SECOND * 5, 8) ) + { + fx->yaw = my->yaw; + fx->skill[7] = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = TICKS_PER_SECOND; + } + } + } + else if ( *cvar_pbaoe == 2 ) + { + color = makeColor(255, 0, 255, 255); + for ( int i = 0; i < 6; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, -7.5, TICKS_PER_SECOND * 5, 16 + (i / 2) * 4) ) + { + fx->yaw = my->yaw + PI / 2; + if ( i % 2 == 1 ) + { + fx->pitch += PI; + } + fx->z += (i / 2) * 2.0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->arc = PI / 4; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = TICKS_PER_SECOND; + } + } + } + } + else if ( *cvar_pbaoe == 4 ) + { + color = makeColor(255, 0, 255, 255); + for ( int i = 0; i < 6; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, -7.5, TICKS_PER_SECOND * 5, 16 + (i / 2) * 4) ) + { + fx->yaw = my->yaw + PI / 2; + if ( i % 2 == 1 ) + { + fx->pitch += PI; + } + fx->z += (i / 2) * 2.0; + fx->fskill[0] = 0.2; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->arc = PI / 4; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = TICKS_PER_SECOND; + } + } + } + } + else if ( *cvar_pbaoe == 6 ) + { + Entity* spellTimer = createParticleTimer(my, 4 * TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_VORTEX; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->particleTimerVariable2 = SPELL_LIFT; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 16.0 * cos(my->yaw); + spellTimer->y = my->y + 16.0 * sin(my->yaw); + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + else if ( *cvar_pbaoe == 8 ) + { + color = makeColor(255, 128, 0, 255); + for ( int i = 0; i < 4; ++i ) + { + real_t x = my->x + 16.0 * cos(my->yaw); + real_t y = my->y + 16.0 * sin(my->yaw); + if ( Entity* fx = createParticleAOEIndicator(my, x, y, -7.5, TICKS_PER_SECOND, 16) ) + { + //fx->yaw = my->yaw + PI / 2; + if ( i == 0 ) + { + spawnExplosion(fx->x, fx->y, 0.0); + } + if ( i >= 2 ) + { + fx->pitch -= PI / 8; + } + else + { + fx->pitch += PI / 8; + } + static ConsoleVariable cvar_pbaoe8_var1("/pbaoe8_var1", 0.25); + static ConsoleVariable cvar_pbaoe8_var2("/pbaoe8_var2", 0.8); + /*if ( i >= 2 ) + { + fx->yaw += PI; + }*/ + if ( i % 2 == 1 ) + { + fx->pitch += PI; + } + fx->z = 0.0; + fx->actSpriteFollowUID = 0; + //fx->vel_z -= *cvar_pbaoe5_velz; + fx->fskill[0] = *cvar_pbaoe8_var1; // rotate + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 5; + indicator->expireAlphaRate = *cvar_pbaoe8_var2; + } + } + } + } + else if ( *cvar_pbaoe == 9 ) + { + createParticleSpin(my); + } + else if ( *cvar_pbaoe == 10 ) + { + Entity* spellTimer = createParticleTimer(my, 5 * TICKS_PER_SECOND + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 16.0 * cos(my->yaw); + spellTimer->y = my->y + 16.0 * sin(my->yaw); + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + else if ( *cvar_pbaoe == 12 ) + { + Entity* spellTimer = createParticleTimer(my, 5 * TICKS_PER_SECOND + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SHATTER_EARTH; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 16.0 * cos(my->yaw); + spellTimer->y = my->y + 16.0 * sin(my->yaw); + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + else if ( *cvar_pbaoe == 11 ) + { + Entity* spellTimer = createParticleTimer(my, 3 * TICKS_PER_SECOND + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_ETERNALS_GAZE; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 16.0 * cos(my->yaw); + spellTimer->y = my->y + 16.0 * sin(my->yaw); + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + else if ( *cvar_pbaoe == 5 || *cvar_pbaoe == 16 ) + { + color = makeColor(255, 255, 255, 255); + for ( int i = 0; i < 24; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, -7.5, TICKS_PER_SECOND * 5, 16 + (i / 2) * 2) ) + { + fx->yaw = my->yaw + PI / 2 - (i / 2) * PI / 3; + fx->pitch += PI / 32; + if ( i % 2 == 1 ) + { + fx->pitch += PI; + } + fx->z = 8.0; + fx->z -= (i / 2) * 0.5; + static ConsoleVariable cvar_pbaoe5_velz("/pbaoe5_velz", 0.25); + static ConsoleVariable cvar_pbaoe5_yaw("/pbaoe5_yaw", 0.3); + fx->vel_z -= *cvar_pbaoe5_velz; + fx->fskill[0] = *cvar_pbaoe5_yaw; // rotate + fx->scalex = 0.5;// + (i / 2) * 0.25 / 12; + fx->scaley = 0.5;// + (i / 2) * 0.25 / 12; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + if ( *cvar_pbaoe == 16 ) + { + indicator->cacheType = AOEIndicators_t::CACHE_CASTING; + } + indicator->arc = PI / 4; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + } + } + } + } + else + { + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, 0.0, TICKS_PER_SECOND * 5, 32) ) + { + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + Uint8 r, g, b, a; + getColor(color, &r, &g, &b, &a); + a *= 0.25; + indicator->indicatorColor = makeColor(r, g, b, a); + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 4; + indicator->delayTicks = 0; + } + } + for ( int i = 0; i < 2; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, -7.5, TICKS_PER_SECOND * 5, 32) ) + { + if ( i == 1 ) + { + fx->pitch = PI; + } + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = TICKS_PER_SECOND; + } + } + } + } + } + if ( keystatus[SDLK_v] && enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + keystatus[SDLK_v] = 0; + + static std::map> effLocations; + + static ConsoleVariable cvar_particle_sprite("/particle_sprite", 1718); + static ConsoleVariable cvar_particle_test("/particle_test", 7); + int lifetime = TICKS_PER_SECOND * 2; + real_t dist = 64.0 * 1.25; + if ( *cvar_particle_test == 3 ) + { + dist = 32.0; + } + else if ( *cvar_particle_test == 4 ) + { + dist = 0.0; + } + else if ( *cvar_particle_test == ParticleTimerEffect_t::EffectType::EFFECT_SPORES ) + { + floorMagicCreateSpores(my, 0.0, 0.0, my, 0, SPELL_SPORES); + } + else if ( *cvar_particle_test == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF ) + { + Entity* spellTimer = createParticleTimer(my, 5 * TICKS_PER_SECOND + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_ROOTS1; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 16.0 * cos(my->yaw); + spellTimer->y = my->y + 16.0 * sin(my->yaw); + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + else if ( *cvar_particle_test == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE ) + { + Entity* spellTimer = createParticleTimer(my, 5 * TICKS_PER_SECOND + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 16.0 * cos(my->yaw); + spellTimer->y = my->y + 16.0 * sin(my->yaw); + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + else if ( *cvar_particle_test == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_PATH ) + { + Entity* spellTimer = createParticleTimer(my, 5 * TICKS_PER_SECOND + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_ROOTS_PATH; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + 16.0 * cos(my->yaw); + spellTimer->y = my->y + 16.0 * sin(my->yaw); + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + else if ( *cvar_particle_test == ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT ) + { + real_t dist = 40.0; + Uint32 lifetime = TICKS_PER_SECOND * 3; + + Entity* spellTimer = createParticleTimer(my, lifetime + TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_LIGHTNING; + spellTimer->particleTimerCountdownSprite = 1757; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + dist * cos(my->yaw); + spellTimer->y = my->y + dist * sin(my->yaw); + spellTimer->flags[NOUPDATE] = false; // spawn for client + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->skill[2] = -18; + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + spellTimer->particleTimerEffectLifetime = lifetime; + floorMagicCreateLightningSequence(spellTimer, 0); + } + else + { + Entity* spellTimer = createParticleTimer(my, lifetime + TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_MAGIC_WAVE; + spellTimer->particleTimerCountdownSprite = *cvar_particle_sprite; + spellTimer->yaw = my->yaw; + spellTimer->x = my->x + dist * cos(my->yaw); + spellTimer->y = my->y + dist * sin(my->yaw); + int lifetime_tick = 0; + auto& timerEffects = particleTimerEffects[spellTimer->getUID()]; + + + std::vector locations = { + 0 * PI / 4, + 1 * PI / 4, + 2 * PI / 4, + 3 * PI / 4, + 4 * PI / 4, + 5 * PI / 4, + 6 * PI / 4, + 7 * PI / 4, + }; + + effLocations[ParticleTimerEffect_t::EffectType::EFFECT_ICE_WAVE] = + { + {0.0, 0.0, 0.25, 0.75, 172}, + {PI / 16, 0.0, 0.0, 1.0, 172}, + {-PI / 16, 0.0, 0.0, 1.0, 0}, + {0.0, 0.0, 0.25, 1.0, 0}, + {PI / 16, 0.0, 0.0, 1.25, 172}, + {-PI / 16, 0.0, 0.0, 1.25, 0} + }; + + effLocations[ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH].clear(); + int roll = local_rng.rand() % 8; + for ( int i = 0; i < 16; ++i ) + { + effLocations[ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH].push_back(ParticleTimerEffect_t::EffectLocations_t()); + auto& data = effLocations[ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH].back(); + if ( i == 0 ) + { + data.sfx = 151; + } + data.seconds = 1 / 16.0; + data.dist = 0.25 + (0.75 * i / 16.0); + real_t angle = (i / 8.0) * PI + ((roll) / 8.0) * PI; + data.xOffset = 8.0 * sin(angle); + data.xOffset += 2.0 * (local_rng.rand() % 16) / 16.0; + data.yawOffset = cos(angle) + ((local_rng.rand() % 4) / 4.0) * 2 * PI; + } + + { + effLocations[ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF].clear(); + { + effLocations[ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF] = + { + { 0.0, 0.0, 0.1, 0.5, 0 }, + /*{ 0.0, -8.0, 0.1, 0.5, 0 }, + { 0.0, 8.0, 0.1, 0.5, 0 }, + { 0.0, 0.0, 0.1, 0.25, 0 }, + { 0.0, -8.0, 0.1, 0.25, 0 }, + { 0.0, 8.0, 0.1, 0.25, 0 }*/ + }; + } + } + + int index = -1; + + if ( *cvar_particle_test == -1 ) + { + static ConsoleVariable c1("/c1", 1.0); + static ConsoleVariable c2("/c2", 4); + static ConsoleVariable c3("/c3", 13.75); + static ConsoleVariable c4("/c4", 5); + real_t grouping = *c3; + real_t scale = *c1; + if ( *c4 == 3 ) + { + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_PULSE, + 1751, my->x + 16.0 * cos(my->yaw), my->y + 16.0 * sin(my->yaw), 6.25, PI / 2 + my->yaw, TICKS_PER_SECOND * 10, false); + wave->skill[1] = 6; // frames + wave->skill[5] = *c2; // frame time + wave->ditheringOverride = 6; + wave->scalex = scale; + wave->scalez = scale; + wave->parent = spellTimer->getUID(); + } + else if ( *c4 == 2 ) + { + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_KINETIC_FIELD, + 1739, my->x + 16.0 * cos(my->yaw), my->y + 16.0 * sin(my->yaw), 6.25, PI / 2 + my->yaw, TICKS_PER_SECOND * 10, false); + wave->skill[1] = 12; // frames + wave->skill[5] = *c2; // frame time + wave->ditheringOverride = 6; + wave->parent = spellTimer->getUID(); + } + else if ( *c4 == 9 ) + { + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_KINETIC_FIELD, + 1857, my->x + 16.0 * cos(my->yaw), my->y + 16.0 * sin(my->yaw), 5.25, PI / 2 + my->yaw, TICKS_PER_SECOND * 10, false); + wave->skill[1] = 8; // frames + wave->skill[5] = *c2; // frame time + wave->ditheringOverride = 6; + wave->parent = spellTimer->getUID(); + } + else if ( *c4 == 5 ) + { + createRadiusMagic(SPELL_NULL_AREA, my, my->x + 32.0 * cos(my->yaw), my->y + 32.0 * sin(my->yaw), 24, 5 * TICKS_PER_SECOND, nullptr); + } + else if ( *c4 == 6 ) + { + createRadiusMagic(SPELL_SPHERE_SILENCE, my, my->x + 32.0 * cos(my->yaw), my->y + 32.0 * sin(my->yaw), 24, 5 * TICKS_PER_SECOND, my); + } + else if ( *c4 == 7 ) + { + for ( int i = 0; i < 3; ++i ) + { + Entity* entity = newEntity(576, 1, map.entities, nullptr); + entity->yaw = i * 2 * PI / 3; + entity->x = my->x; + entity->y = my->y; + entity->z = 7.5; + double missile_speed = 4; + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->actmagicIsOrbiting = 2; + entity->actmagicOrbitDist = 8.0; + entity->actmagicOrbitStationaryCurrentDist = 0.0; + entity->actmagicOrbitStartZ = entity->z; + //entity->roll -= (PI / 8); + entity->actmagicOrbitVerticalSpeed = -0.3; + entity->actmagicOrbitVerticalDirection = 1; + entity->actmagicOrbitLifetime = TICKS_PER_SECOND / 2; + entity->actmagicOrbitStationaryX = my->x; + entity->actmagicOrbitStationaryY = my->y; + entity->vel_z = -0.1; + entity->behavior = &actMagicParticleCircling2; + + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = false; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } + } + else if ( *c4 == 1 ) + { + spellTimer->particleTimerDuration = TICKS_PER_SECOND * 5; + for ( int i = 0; i < 3; ++i ) + { + if ( i == 0 || i == 2 ) { continue; } + bool light = false; + if ( i == 1 ) + { + light = true; + } + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_FIRE_WAVE, + 1733, my->x + 16.0 * cos(my->yaw), my->y + 16.0 * sin(my->yaw), 2.75, + -PI / 2 + my->yaw - PI / 3 + i * PI / 3, + spellTimer->particleTimerDuration, light); + wave->skill[1] = 6; // frames + wave->skill[5] = *c2; // frame time + wave->ditheringOverride = 6; + wave->parent = spellTimer->getUID(); + real_t startScale = 0.1; + wave->scalex = startScale; + wave->scaley = startScale; + wave->scalez = startScale; + wave->focaly = startScale * grouping; + wave->fskill[0] = scale; // final scale + wave->fskill[1] = grouping; // final grouping + wave->skill[6] = 1; // grow to scale + wave->flags[UPDATENEEDED] = true; + } + } + else if ( *c4 == 4 ) + { + spellTimer->particleTimerDuration = TICKS_PER_SECOND * 5; + bool light = true; + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_TUNNEL, + 1810, my->x + 16.0 * cos(my->yaw), my->y + 16.0 * sin(my->yaw), 2.75, my->yaw + PI / 2, + spellTimer->particleTimerDuration, light); + wave->skill[1] = 4; // frames + wave->skill[5] = *c2; // frame time + wave->ditheringOverride = 6; + wave->parent = spellTimer->getUID(); + real_t startScale = 0.1; + wave->scalex = startScale; + wave->scaley = startScale; + wave->scalez = startScale; + wave->focaly = 0.0; + wave->fskill[0] = scale; // final scale + wave->fskill[1] = 0.0; // final grouping + wave->skill[6] = 1; // grow to scale + wave->flags[UPDATENEEDED] = true; + } + else + { + for ( int i = 0; i < 4; ++i ) + { + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_FIRE_WAVE, 1733, my->x + 16.0 * cos(my->yaw), my->y + 16.0 * sin(my->yaw), 2.75, my->yaw + i * PI / 2, TICKS_PER_SECOND * 20, false); + wave->skill[1] = 6; // frames + wave->skill[5] = *c2; // frame time + wave->ditheringOverride = 6; + /*if ( i == 4 ) { grouping -= 4.0; } + if ( i >= 4 ) + { + static ConsoleVariable c1("/c1", 1.0); + static ConsoleVariable c2("/c2", 4); + wave->skill[5] = *c2; + }*/ + if ( i >= 4 ) + { + wave->z -= 8.0; + } + //wave->pitch = PI / 2; + wave->scalex = *c1; + wave->scaley = *c1; + //wave->scaley = *c1; + wave->focaly = grouping; + //wave->x += grouping * cos(wave->yaw - PI / 2); + //wave->y += grouping * sin(wave->yaw - PI / 2); + } + } + lifetime_tick = lifetime + 1; + } + else if ( *cvar_particle_test == -3 ) + { + Entity* root = createParticleRoot(1766, my->x + 16.0 * cos(my->yaw), my->y + 16.0 * sin(my->yaw), + 7.5, my->yaw, TICKS_PER_SECOND * 3); + root->focalz = -0.5; + } + //else if ( *cvar_particle_test == -2 ) + //{ + // Entity* wave = createParticleWave(1721, my->x + 16.0 * cos(my->yaw), my->y + 16.0 * sin(my->yaw), 0.f, 0.f, TICKS_PER_SECOND * 20); + // wave->skill[1] = 12; // frames + // lifetime_tick = lifetime + 1; + //} + while ( lifetime_tick <= lifetime ) + { + ++index; + auto& effect = timerEffects.effectMap[lifetime_tick == 0 ? 1 : lifetime_tick]; // first behavior tick only occurs at 1 + real_t ratio = lifetime_tick / (real_t)lifetime; + effect.effectType = (ParticleTimerEffect_t::EffectType)(*cvar_particle_test); + effect.x = my->x + dist * (0.75 * ratio + 0.25) * cos(my->yaw); + effect.y = my->y + dist * (0.75 * ratio + 0.25) * sin(my->yaw); + if ( effect.effectType == ParticleTimerEffect_t::EffectType::EFFECT_ICE_WAVE ) + { + auto& data = effLocations[effect.effectType][index]; + effect.sfx = data.sfx; + effect.yaw = my->yaw + data.yawOffset; + real_t dist = 40.0; + effect.x = my->x + dist * (data.dist) * cos(effect.yaw); + effect.y = my->y + dist * (data.dist) * sin(effect.yaw); + lifetime_tick += std::max(1.0, TICKS_PER_SECOND * data.seconds); + if ( index + 1 >= effLocations[effect.effectType].size() ) + { + break; + } + } + else if ( effect.effectType == ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH ) + { + auto& data = effLocations[effect.effectType][index]; + effect.sfx = data.sfx; + effect.x = my->x + dist * (data.dist) * cos(my->yaw) + data.xOffset * cos(my->yaw + PI / 2); + effect.y = my->y + dist * (data.dist) * sin(my->yaw) + data.xOffset * sin(my->yaw + PI / 2); + + effect.yaw = my->yaw + data.yawOffset; + + lifetime_tick += std::max(1.0, TICKS_PER_SECOND * data.seconds); + if ( index + 1 >= effLocations[effect.effectType].size() ) + { + break; + } + } + else if ( effect.effectType == ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT ) + { + auto& data = effLocations[effect.effectType][index]; + effect.sfx = data.sfx; + effect.x = /*my->x + dist * (data.dist) * cos(my->yaw) +*/ data.xOffset * cos(my->yaw + PI / 2); + effect.y = /*my->y + dist * (data.dist) * sin(my->yaw) +*/ data.xOffset * sin(my->yaw + PI / 2); + + effect.yaw = my->yaw + data.yawOffset; + + if ( index == 0 ) + { + //createParticleCircling(spellTimer, TICKS_PER_SECOND, 931); + } + + lifetime_tick += std::max(1.0, TICKS_PER_SECOND * data.seconds); + if ( index + 1 >= effLocations[effect.effectType].size() ) + { + break; + } + } + else if ( effect.effectType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF ) + { + spellTimer->particleTimerCountdownSprite = 1765; + + auto& data = effLocations[effect.effectType][index]; + effect.sfx = data.sfx; + effect.x = my->x + dist * (data.dist) * cos(my->yaw) + data.xOffset * cos(my->yaw + PI / 2); + effect.y = my->y + dist * (data.dist) * sin(my->yaw) + data.xOffset * sin(my->yaw + PI / 2); + + effect.yaw = my->yaw + data.yawOffset; + + lifetime_tick += std::max(1.0, TICKS_PER_SECOND * data.seconds); + if ( index + 1 >= effLocations[effect.effectType].size() ) + { + break; + } + } + else if ( effect.effectType == ParticleTimerEffect_t::EffectType::EFFECT_TEST_2 ) + { + lifetime_tick += 2; + } + else if ( effect.effectType == ParticleTimerEffect_t::EffectType::EFFECT_TEST_3 ) + { + if ( locations.size() > 0 ) + { + int pick = local_rng.rand() % locations.size(); + auto coord = locations[local_rng.rand() % locations.size()]; + locations.erase(locations.begin() + pick); + effect.x = my->x + 16.0 * cos(my->yaw + coord); + effect.y = my->y + 16.0 * sin(my->yaw + coord); + } + else + { + timerEffects.effectMap.erase(lifetime_tick); + } + lifetime_tick += TICKS_PER_SECOND / 4; + } + else + { + lifetime_tick += TICKS_PER_SECOND / 4; + } + } + } + + //for ( int i = 0; i < 8; ++i ) + //{ + // Entity* entity = newEntity(i >= 4 ? 224 : 223, 1, map.entities, nullptr); //Sprite entity. + // entity->x = my->x + 16.0 * cos(my->yaw); + // entity->y = my->y + 16.0 * sin(my->yaw); + // entity->z = 7.499; + // static ConsoleVariable cvar_sprite_scale("/sprite_scale", 1.0); + // static ConsoleVariable cvar_sprite_rotate("/sprite_rotate", 0.0); + // static ConsoleVariable cvar_sprite_alpha("/sprite_alpha", 1.0); + // static ConsoleVariable cvar_sprite_alpha_glow("/sprite_alpha_glow", 0.0); + // static ConsoleVariable cvar_sprite_grouping("/sprite_grouping", 8.0); + // entity->ditheringDisabled = true; + // entity->flags[SPRITE] = true; + // entity->flags[PASSABLE] = true; + // entity->flags[NOUPDATE] = true; + // entity->flags[UNCLICKABLE] = true; + // entity->flags[BRIGHT] = true; + // entity->scalex = *cvar_sprite_scale; + // entity->scaley = 2.0 * *cvar_sprite_scale; + // //entity->scalex = *cvar_sprite_scale; + // entity->behavior = &actSprite; + // entity->yaw = my->yaw + PI / 2 + (i * PI / 2); + // entity->x += *cvar_sprite_grouping * cos(entity->yaw - PI / 2); + // entity->y += *cvar_sprite_grouping * sin(entity->yaw - PI / 2); + // entity->pitch = 0.0; + // entity->roll = 0.0; + // if ( i >= 4 ) + // { + // entity->roll = PI; + // } + // entity->skill[0] = 1; + // entity->skill[1] = 1; + // entity->skill[2] = 350; + // entity->fskill[0] = *cvar_sprite_rotate; + // entity->fskill[2] = *cvar_sprite_alpha; // alpha + // entity->fskill[3] = *cvar_sprite_alpha_glow; + // entity->skill[6] = 1; // use alpha + // entity->skill[7] = 1; // no billboard + // if ( multiplayer != CLIENT ) + // { + // entity_uids--; + // } + // entity->setUID(-3); + //} + } + + Entity* entity; + Entity* entity2 = nullptr; + Entity* rightbody = nullptr; + Entity* weaponarm = nullptr; + Entity* shieldarm = nullptr; + Entity* additionalLimb = nullptr; + Entity* torso = nullptr; + node_t* node; + int i, bodypart; + double dist = 0; + bool wearingring = false; + bool levitating = false; + bool isHumanoid = true; + bool showEquipment = true; + if ( PLAYER_NUM < 0 || PLAYER_NUM >= MAXPLAYERS ) + { + return; + } + + Monster playerRace = HUMAN; + int spriteTorso = 106 + 12 * stats[PLAYER_NUM]->sex; + int spriteLegRight = 107 + 12 * stats[PLAYER_NUM]->sex; + int spriteLegLeft = 108 + 12 * stats[PLAYER_NUM]->sex; + int spriteArmRight = 109 + 12 * stats[PLAYER_NUM]->sex; + int spriteArmLeft = 110 + 12 * stats[PLAYER_NUM]->sex; + int playerAppearance = stats[PLAYER_NUM]->stat_appearance; + + if ( my->effectShapeshift != NOTHING ) + { + playerRace = static_cast(my->effectShapeshift); + stats[PLAYER_NUM]->type = playerRace; + } + else if ( stats[PLAYER_NUM]->playerRace > 0 || stats[PLAYER_NUM]->getEffectActive(EFF_POLYMORPH) || my->effectPolymorph != NOTHING ) + { + playerRace = my->getMonsterFromPlayerRace(stats[PLAYER_NUM]->playerRace); + if ( my->effectPolymorph != NOTHING ) + { + if ( my->effectPolymorph > NUMMONSTERS ) + { + playerRace = HUMAN; + playerAppearance = my->effectPolymorph - 100; + } + else + { + playerRace = static_cast(my->effectPolymorph); + } + } + if ( stats[PLAYER_NUM]->stat_appearance == 0 || my->effectPolymorph != NOTHING ) + { + stats[PLAYER_NUM]->type = playerRace; + } + else + { + stats[PLAYER_NUM]->type = HUMAN; // appearance of 1 is aesthetic only + } + } + else + { + stats[PLAYER_NUM]->type = HUMAN; + } + + if ( stats[PLAYER_NUM]->type == RAT || stats[PLAYER_NUM]->type == SPIDER ) { isHumanoid = false; } @@ -4617,7 +7231,7 @@ void actPlayer(Entity* my) if ( multiplayer != CLIENT ) { - if ( stats[PLAYER_NUM]->EFFECTS[EFF_SHAPESHIFT] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_SHAPESHIFT) ) { stats[PLAYER_NUM]->playerShapeshiftStorage = my->effectShapeshift; // keep track of player shapeshift effects } @@ -4630,7 +7244,7 @@ void actPlayer(Entity* my) } } - if ( stats[PLAYER_NUM]->EFFECTS[EFF_POLYMORPH] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_POLYMORPH) ) { stats[PLAYER_NUM]->playerPolymorphStorage = my->effectPolymorph; // keep track of player polymorph effects } @@ -4646,7 +7260,15 @@ void actPlayer(Entity* my) if ( players[PLAYER_NUM]->isLocalPlayer() ) // TODO: hotbar code splitscreen { - if ( stats[PLAYER_NUM]->type != HUMAN && stats[PLAYER_NUM]->EFFECTS[EFF_SHAPESHIFT] ) + if ( !stats[PLAYER_NUM]->getEffectActive(EFF_PROJECT_SPIRIT) && !players[PLAYER_NUM]->ghost.isActive() ) + { + if ( PLAYER_DEBUGCAM == 2 ) + { + PLAYER_DEBUGCAM = 0; + } + } + + if ( stats[PLAYER_NUM]->type != HUMAN && stats[PLAYER_NUM]->getEffectActive(EFF_SHAPESHIFT) ) { if ( players[PLAYER_NUM]->hotbar.swapHotbarOnShapeshift == 0 ) { @@ -4659,7 +7281,7 @@ void actPlayer(Entity* my) initShapeshiftHotbar(PLAYER_NUM); } } - else if ( !stats[PLAYER_NUM]->EFFECTS[EFF_SHAPESHIFT] && players[PLAYER_NUM]->hotbar.swapHotbarOnShapeshift > 0 ) + else if ( !stats[PLAYER_NUM]->getEffectActive(EFF_SHAPESHIFT) && players[PLAYER_NUM]->hotbar.swapHotbarOnShapeshift > 0 ) { deinitShapeshiftHotbar(PLAYER_NUM); } @@ -4681,6 +7303,26 @@ void actPlayer(Entity* my) my->scaley = 1.01; my->scalez = 1.01; } + else if ( playerRace == DRYAD ) + { + if ( my->sprite == 1992 || my->sprite == 1993 ) + { + my->focalz += 0.5; + } + my->scalex = 1.f; + my->scaley = 1.f; + my->scalez = 1.f; + } + else if ( playerRace == MYCONID ) + { + if ( my->sprite == 1998 ) + { + my->focalz += 0.5; + } + my->scalex = 1.f; + my->scaley = 1.f; + my->scalez = 1.f; + } else { my->scalex = 1.f; @@ -4820,6 +7462,27 @@ void actPlayer(Entity* my) entity->skill[2] = PLAYER_NUM; entity->behavior = &actHudArrowModel; my->bodyparts.push_back(entity); + + // hud magic rangefinder + entity = newEntity(-1, 1, map.entities, nullptr); //HUD entity. + entity->flags[PASSABLE] = true; + entity->flags[OVERDRAW] = false; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->skill[2] = PLAYER_NUM; + entity->behavior = &actMagicRangefinder; + players[PLAYER_NUM]->hud.magicRangefinder = entity; + my->bodyparts.push_back(entity); + + // hud additional 2 limb + entity = newEntity(-1, 1, map.entities, nullptr); //HUD entity. + entity->flags[PASSABLE] = true; + entity->flags[OVERDRAW] = true; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->skill[2] = PLAYER_NUM; + entity->behavior = &actHudAdditional2; + my->bodyparts.push_back(entity); } else { @@ -4847,7 +7510,7 @@ void actPlayer(Entity* my) node->deconstructor = &emptyDeconstructor; node->size = sizeof(Entity*); my->bodyparts.push_back(entity); - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_TORSO); + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_TORSO, my->sprite); // right leg entity = newEntity(spriteLegRight, 1, map.entities, nullptr); //Limb entity. @@ -4867,7 +7530,7 @@ void actPlayer(Entity* my) node->deconstructor = &emptyDeconstructor; node->size = sizeof(Entity*); my->bodyparts.push_back(entity); - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_RIGHTLEG); + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_RIGHTLEG, my->sprite); // left leg entity = newEntity(spriteLegLeft, 1, map.entities, nullptr); //Limb entity. @@ -4887,7 +7550,7 @@ void actPlayer(Entity* my) node->deconstructor = &emptyDeconstructor; node->size = sizeof(Entity*); my->bodyparts.push_back(entity); - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_LEFTLEG); + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_LEFTLEG, my->sprite); // right arm entity = newEntity(spriteArmRight, 1, map.entities, nullptr); //Limb entity. @@ -4907,7 +7570,7 @@ void actPlayer(Entity* my) node->deconstructor = &emptyDeconstructor; node->size = sizeof(Entity*); my->bodyparts.push_back(entity); - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_RIGHTARM); + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_RIGHTARM, my->sprite); // left arm entity = newEntity(spriteArmLeft, 1, map.entities, nullptr); //Limb entity. @@ -4927,7 +7590,7 @@ void actPlayer(Entity* my) node->deconstructor = &emptyDeconstructor; node->size = sizeof(Entity*); my->bodyparts.push_back(entity); - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_LEFTARM); + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_LEFTARM, my->sprite); // world weapon entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. @@ -4962,6 +7625,7 @@ void actPlayer(Entity* my) entity->focaly = limbs[playerRace][7][1]; entity->focalz = limbs[playerRace][7][2]; entity->behavior = &actPlayerLimb; + entity->skill[4] = 1; // shield denote entity->parent = my->getUID(); entity->focalx = 2; node = list_AddNodeLast(&my->children); @@ -5132,9 +7796,112 @@ void actPlayer(Entity* my) if ( PLAYER_ALIVETIME == 0 ) { my->createWorldUITooltip(); + players[PLAYER_NUM]->mechanics.previouslyLevitating = false; } + players[PLAYER_NUM]->player_last_x = my->x; + players[PLAYER_NUM]->player_last_y = my->y; + PLAYER_ALIVETIME++; + + if ( multiplayer != CLIENT && PLAYER_ALIVETIME == 1 ) + { + node_t* nextnode = nullptr; + for ( auto node = stats[PLAYER_NUM]->void_chest_inventory.first; node; node = nextnode ) + { + nextnode = node->next; + if ( Item* item = (Item*)node->element ) + { + if ( item->type == TOOL_DUCK ) + { + list_RemoveNode(node); + } + } + } + } + + /*if ( players[PLAYER_NUM]->isLocalPlayer() + && ((stats[PLAYER_NUM]->playerRace >= 13 && stats[PLAYER_NUM]->playerRace <= 17) + || client_classes[PLAYER_NUM] >= 21) ) + { +#ifdef STEAMWORKS + if ( !enabledDLCPack3 || !SteamApps()->BIsDlcInstalled(1010822) ) + { + int* potato = NULL; + (*potato) = 322; + } +#endif + }*/ + + if ( players[PLAYER_NUM]->isLocalPlayer() && PLAYER_ALIVETIME == 1 && currentlevel > 0 ) + { + for ( auto duck : players[PLAYER_NUM]->mechanics.ducksInARow ) + { + bool birdInHand = false; + for ( auto node = stats[PLAYER_NUM]->inventory.first; node; node = node->next ) + { + if ( Item* item = (Item*)node->element ) + { + if ( item->type == TOOL_DUCK ) + { + if ( item->getDuckPlayer() == PLAYER_NUM ) + { + if ( ((item->appearance % items[TOOL_DUCK].variations) / MAXPLAYERS) == duck.first ) + { + birdInHand = true; + break; + } + } + } + } + } + + if ( !birdInHand ) + { + if ( multiplayer == CLIENT ) + { + // request duck + strcpy((char*)net_packet->data, "DUCK"); + net_packet->data[4] = PLAYER_NUM; + net_packet->data[5] = duck.first; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + else + { + players[PLAYER_NUM]->mechanics.pendingDucks.push_back( + std::make_pair(duck.first, ticks + (3 + (local_rng.rand() % 30)) * TICKS_PER_SECOND)); + } + } + } + } + + if ( PLAYER_ALIVETIME > 300 && players[PLAYER_NUM]->mechanics.pendingDucks.size() ) + { + for ( auto it = players[PLAYER_NUM]->mechanics.pendingDucks.begin(); it != players[PLAYER_NUM]->mechanics.pendingDucks.end(); ) + { + if ( ticks >= it->second ) + { + int dirx = local_rng.rand() % 2; + int diry = local_rng.rand() % 2; + int mapx = dirx ? 1 : map.width - 2; + int mapy = diry ? 1 : map.height - 2; + + int appearance = it->first * MAXPLAYERS + PLAYER_NUM; + Item* duckItem = newItem(TOOL_DUCK, EXCELLENT, 0, 1, appearance, true, nullptr); + duckItem->applyDuck(0, mapx * 16 + 8.0, mapy * 16 + 8.0, nullptr, true); + free(duckItem); + it = players[PLAYER_NUM]->mechanics.pendingDucks.erase(it); + } + else + { + ++it; + } + } + } + if ( PLAYER_NUM == clientnum ) // specifically the host - in splitscreen we only process this once for all players. { if ( PLAYER_ALIVETIME == 300 && gameModeManager.currentMode == GameModeManager_t::GameModes::GAME_MODE_DEFAULT ) @@ -5389,6 +8156,30 @@ void actPlayer(Entity* my) } } + if ( my->effectShapeshift == NOTHING && stats[PLAYER_NUM]->shield && stats[PLAYER_NUM]->shield->type == TOOL_DUCK ) + { + if ( stats[PLAYER_NUM]->shield->getDuckPlayer() == PLAYER_NUM ) + { + for ( auto& duck : players[PLAYER_NUM]->mechanics.ducksInARow ) + { + if ( duck.first == ((stats[PLAYER_NUM]->shield->appearance % items[TOOL_DUCK].variations) / MAXPLAYERS) ) + { + duck.second++; + if ( duck.second % (1 * TICKS_PER_SECOND) == 0 ) + { + Compendium_t::Events_t::eventUpdate(PLAYER_NUM, Compendium_t::CPDM_GYROBOT_TIME_SPENT, TOOL_DUCK, (1 * TICKS_PER_SECOND)); + } + if ( duck.second == 15 * 60 * TICKS_PER_SECOND ) + { + createEnsembleHUDParticleCircling(my); + playSoundEntityLocal(my, 784 + local_rng.rand() % 2, 128); + } + break; + } + } + } + } + if ( players[PLAYER_NUM]->movement.monsterEmoteGimpTimer > 0 ) { --players[PLAYER_NUM]->movement.monsterEmoteGimpTimer; @@ -5405,10 +8196,27 @@ void actPlayer(Entity* my) { serverUpdateEntityBodypart(my, i); } + // extra torso limb + serverUpdateEntityBodypart(my, 13); } } if ( multiplayer != CLIENT ) { + if ( stats[PLAYER_NUM]->getEffectActive(EFF_PROJECT_SPIRIT) ) + { + if ( PLAYER_ALIVETIME == 25 ) + { + stats[PLAYER_NUM]->EFFECTS_TIMERS[EFF_PROJECT_SPIRIT] = 1; + } + else if ( PLAYER_ALIVETIME > 25 && !players[PLAYER_NUM]->ghost.isActive() ) + { + if ( stats[PLAYER_NUM]->EFFECTS_ACCRETION_TIME[EFF_PROJECT_SPIRIT] >= 5 * TICKS_PER_SECOND ) + { + stats[PLAYER_NUM]->EFFECTS_TIMERS[EFF_PROJECT_SPIRIT] = 1; + } + } + } + if ( PLAYER_ALIVETIME == 50 && currentlevel == 0 ) { int monsterSquad = 0; @@ -5466,7 +8274,7 @@ void actPlayer(Entity* my) } } } - if ( stats[PLAYER_NUM]->EFFECTS[EFF_WITHDRAWAL] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_WITHDRAWAL) ) { if ( PLAYER_ALIVETIME == 500 ) { @@ -5609,18 +8417,55 @@ void actPlayer(Entity* my) { if ( my->getUID() % TICKS_PER_SECOND == ticks % TICKS_PER_SECOND ) { - monsterAllyFormations.updateFormation(my->getUID()); + monsterAllyFormations.updateFormation(my->getUID()); + } + } + + if ( players[PLAYER_NUM]->isLocalPlayer() && PLAYER_ALIVETIME == 10 && currentlevel > 0 && !intro ) + { + auto& appraisal = players[PLAYER_NUM]->inventoryUI.appraisal; + real_t appraisalTimerReduce = 0.75 - 0.25 * std::max(0, std::min(100, (stats[PLAYER_NUM]->getModifiedProficiency(PRO_APPRAISAL) + statGetPER(stats[PLAYER_NUM], my)))) / 100.0; + bool anyUnid = false; + for ( auto node = stats[PLAYER_NUM]->inventory.first; node; node = node->next ) + { + if ( Item* item = (Item*)node->element ) + { + if ( !item->identified ) + { + anyUnid = true; + auto find = appraisal.appraisalProgressionItems.find(item->uid); + if ( find == appraisal.appraisalProgressionItems.end() ) + { + appraisal.appraisalProgressionItems[item->uid] = std::max((int)(appraisalTimerReduce * appraisal.getAppraisalTime(item)), 1); + } + else + { + appraisal.appraisalProgressionItems[item->uid] = std::max((int)(appraisalTimerReduce * appraisal.appraisalProgressionItems[item->uid]), 1); + } + + if ( appraisal.current_item == item->uid ) + { + appraisal.timer = std::max(1, std::min(appraisal.timer, appraisal.appraisalProgressionItems[item->uid])); + } + } + } + } + + if ( anyUnid ) + { + messagePlayerColor(PLAYER_NUM, MESSAGE_INVENTORY | MESSAGE_HINT, makeColorRGB(255, 255, 0), Language::get(6992)); } } if ( players[PLAYER_NUM]->isLocalPlayer() - && players[PLAYER_NUM]->inventoryUI.appraisal.timer > 0 ) + && players[PLAYER_NUM]->inventoryUI.appraisal.timer > 0 && !intro ) { Item* tempItem = uidToItem(players[PLAYER_NUM]->inventoryUI.appraisal.current_item); if ( tempItem ) { if ( tempItem->identified ) { + players[PLAYER_NUM]->inventoryUI.appraisal.appraisalProgressionItems.erase(players[PLAYER_NUM]->inventoryUI.appraisal.current_item); players[PLAYER_NUM]->inventoryUI.appraisal.timer = 0; players[PLAYER_NUM]->inventoryUI.appraisal.current_item = 0; } @@ -5634,6 +8479,7 @@ void actPlayer(Entity* my) tempItem->identified = true; tempItem->notifyIcon = true; messagePlayer(PLAYER_NUM, MESSAGE_INVENTORY, Language::get(570), tempItem->description()); + players[PLAYER_NUM]->inventoryUI.appraisal.appraisalProgressionItems.erase(players[PLAYER_NUM]->inventoryUI.appraisal.current_item); players[PLAYER_NUM]->inventoryUI.appraisal.current_item = 0; players[PLAYER_NUM]->inventoryUI.appraisal.timer = 0; @@ -5698,43 +8544,7 @@ void actPlayer(Entity* my) { if ( !appearancesOfSimilarItems.empty() ) { - Uint32 originalAppearance = tempItem->appearance; - int originalVariation = originalAppearance % items[tempItem->type].variations; - - int tries = 100; - // we need to find a unique appearance within the list. - tempItem->appearance = local_rng.rand(); - if ( tempItem->appearance % items[tempItem->type].variations != originalVariation ) - { - // we need to match the variation for the new appearance, take the difference so new varation matches - int change = (tempItem->appearance % items[tempItem->type].variations - originalVariation); - if ( tempItem->appearance < change ) // underflow protection - { - tempItem->appearance += items[tempItem->type].variations; - } - tempItem->appearance -= change; - int newVariation = tempItem->appearance % items[tempItem->type].variations; - assert(newVariation == originalVariation); - } - auto it = appearancesOfSimilarItems.find(tempItem->appearance); - while ( it != appearancesOfSimilarItems.end() && tries > 0 ) - { - tempItem->appearance = local_rng.rand(); - if ( tempItem->appearance % items[tempItem->type].variations != originalVariation ) - { - // we need to match the variation for the new appearance, take the difference so new varation matches - int change = (tempItem->appearance % items[tempItem->type].variations - originalVariation); - if ( tempItem->appearance < change ) // underflow protection - { - tempItem->appearance += items[tempItem->type].variations; - } - tempItem->appearance -= change; - int newVariation = tempItem->appearance % items[tempItem->type].variations; - assert(newVariation == originalVariation); - } - it = appearancesOfSimilarItems.find(tempItem->appearance); - --tries; - } + Item::itemFindUniqueAppearance(tempItem, appearancesOfSimilarItems); } if ( multiplayer == CLIENT && itemIsEquipped(tempItem, PLAYER_NUM) && players[PLAYER_NUM]->paperDoll.isItemOnDoll(*tempItem) ) @@ -5746,22 +8556,30 @@ void actPlayer(Entity* my) else { players[PLAYER_NUM]->inventoryUI.appraisal.timer -= 1; //De-increment appraisal timer. + int checkAppraiseTime = players[PLAYER_NUM]->inventoryUI.appraisal.getAppraisalTime(tempItem); + if ( checkAppraiseTime < players[PLAYER_NUM]->inventoryUI.appraisal.timer ) + { + players[PLAYER_NUM]->inventoryUI.appraisal.timer = checkAppraiseTime; + } + players[PLAYER_NUM]->inventoryUI.appraisal.appraisalProgressionItems[players[PLAYER_NUM]->inventoryUI.appraisal.current_item] = players[PLAYER_NUM]->inventoryUI.appraisal.timer; if ( players[PLAYER_NUM]->inventoryUI.appraisal.timer <= 0) { players[PLAYER_NUM]->inventoryUI.appraisal.timer = 0; + players[PLAYER_NUM]->inventoryUI.appraisal.appraisalProgressionItems.erase(players[PLAYER_NUM]->inventoryUI.appraisal.current_item); //Cool. Time to identify the item. bool success = false; if ( stats[PLAYER_NUM]->getModifiedProficiency(PRO_APPRAISAL) < 100 ) { - if ( tempItem->type != GEM_GLASS ) + /*if ( tempItem->type != GEM_GLASS ) { - success = (stats[PLAYER_NUM]->getModifiedProficiency(PRO_APPRAISAL) + my->getPER() * 5 >= items[tempItem->type].value / 10); + success = (stats[PLAYER_NUM]->getModifiedProficiency(PRO_APPRAISAL) + my->getPER() * 5 >= items[tempItem->type].gold_value / 10); } else { success = (stats[PLAYER_NUM]->getModifiedProficiency(PRO_APPRAISAL) + my->getPER() * 5 >= 100); - } + }*/ + success = players[PLAYER_NUM]->inventoryUI.appraisal.appraisalPossible(tempItem); } else { @@ -5787,56 +8605,76 @@ void actPlayer(Entity* my) } //Attempt a level up. - if ( tempItem && items[tempItem->type].value > 0 && stats[PLAYER_NUM] ) + if ( tempItem && tempItem->getGoldValue() > 0 && stats[PLAYER_NUM] ) { if ( tempItem->identified ) { int appraisalEaseOfDifficulty = 0; - if ( items[tempItem->type].value < 100 ) + bool increaseSkill = false; + if ( stats[PLAYER_NUM]->getProficiency(PRO_APPRAISAL) < 50 ) { - // easy junk items - appraisalEaseOfDifficulty = 2; + if ( tempItem->getGoldValue() < 100 ) + { + // easy junk items + appraisalEaseOfDifficulty = 2; + } + else if ( tempItem->getGoldValue() < 200 ) + { + // medium + appraisalEaseOfDifficulty = 1; + } + else if ( tempItem->getGoldValue() < 300 ) + { + // medium + appraisalEaseOfDifficulty = 0; + } + else if ( tempItem->getGoldValue() < 400 ) + { + // hardest + appraisalEaseOfDifficulty = -1; + } + else + { + // hardest + appraisalEaseOfDifficulty = -1; + } + appraisalEaseOfDifficulty += stats[PLAYER_NUM]->getProficiency(PRO_APPRAISAL) / 20; + // difficulty ranges from 1-in-1 to 1-in-6 + appraisalEaseOfDifficulty = std::max(appraisalEaseOfDifficulty, 1); + //messagePlayer(0, "Appraisal level up chance: 1 in %d", appraisalEaseOfDifficulty); + increaseSkill = true; } - else if ( items[tempItem->type].value < 200 ) + + if ( tempItem->getGoldValue() > 300 || tempItem->type == GEM_GLASS ) { - // medium appraisalEaseOfDifficulty = 1; + increaseSkill = true; } - else if ( items[tempItem->type].value < 300 ) - { - // medium - appraisalEaseOfDifficulty = 0; - } - else if ( items[tempItem->type].value < 400 ) - { - // hardest - appraisalEaseOfDifficulty = -1; - } - else - { - // hardest - appraisalEaseOfDifficulty = -1; - } - appraisalEaseOfDifficulty += stats[PLAYER_NUM]->getProficiency(PRO_APPRAISAL) / 20; - // difficulty ranges from 1-in-1 to 1-in-6 - appraisalEaseOfDifficulty = std::max(appraisalEaseOfDifficulty, 1); - //messagePlayer(0, "Appraisal level up chance: 1 in %d", appraisalEaseOfDifficulty); - if ( local_rng.rand() % appraisalEaseOfDifficulty == 0 ) + + if ( increaseSkill && appraisalEaseOfDifficulty > 0 ) { - if ( multiplayer == CLIENT ) + int attempts = itemTypeIsQuiver(tempItem->type) ? 1 : tempItem->count; + for ( int i = 0; i < attempts; ++i ) { - // request level up - strcpy((char*)net_packet->data, "CSKL"); - net_packet->data[4] = PLAYER_NUM; - net_packet->data[5] = PRO_APPRAISAL; - net_packet->address.host = net_server.host; - net_packet->address.port = net_server.port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, 0); - } - else - { - my->increaseSkill(PRO_APPRAISAL); + if ( local_rng.rand() % appraisalEaseOfDifficulty == 0 ) + { + if ( multiplayer == CLIENT ) + { + // request level up + strcpy((char*)net_packet->data, "CSKL"); + net_packet->data[4] = PLAYER_NUM; + net_packet->data[5] = PRO_APPRAISAL; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + else + { + my->increaseSkill(PRO_APPRAISAL); + } + break; + } } } } @@ -5869,7 +8707,7 @@ void actPlayer(Entity* my) Item* item2 = (Item*)node->element; if ( item2 && item2 != tempItem && !itemCompare(tempItem, item2, false) ) { - if ( (itemTypeIsQuiver(item2->type) && (tempItem->count + item2->count) >= QUIVER_MAX_AMMO_QTY) + if ( ((itemTypeIsQuiver(item2->type) || itemTypeIsThrownBall(item2->type)) && (tempItem->count + item2->count) >= QUIVER_MAX_AMMO_QTY) || ((item2->type == TOOL_MAGIC_SCRAP || item2->type == TOOL_METAL_SCRAP) && (tempItem->count + item2->count) >= SCRAP_MAX_STACK_QTY) ) { @@ -5920,18 +8758,20 @@ void actPlayer(Entity* my) if ( multiplayer == CLIENT && itemIsEquipped(item2, PLAYER_NUM) ) { // if incrementing qty and holding item, then send "equip" for server to update their count of your held item. - strcpy((char*)net_packet->data, "EQUS"); - SDLNet_Write32((Uint32)item2->type, &net_packet->data[4]); - SDLNet_Write32((Uint32)item2->status, &net_packet->data[8]); - SDLNet_Write32((Uint32)item2->beatitude, &net_packet->data[12]); - SDLNet_Write32((Uint32)item2->count, &net_packet->data[16]); - SDLNet_Write32((Uint32)item2->appearance, &net_packet->data[20]); - net_packet->data[24] = item2->identified; - net_packet->data[25] = PLAYER_NUM; - net_packet->address.host = net_server.host; - net_packet->address.port = net_server.port; - net_packet->len = 27; - sendPacketSafe(net_sock, -1, net_packet, 0); + Item** slot = itemSlot(stats[PLAYER_NUM], item2); + if ( slot ) + { + if ( slot == &stats[PLAYER_NUM]->weapon ) + { + clientSendEquipUpdateToServer(EQUIP_ITEM_SLOT_WEAPON, EQUIP_ITEM_SUCCESS_UPDATE_QTY, PLAYER_NUM, + item2->type, item2->status, item2->beatitude, item2->count, item2->appearance, item2->identified); + } + else if ( slot == &stats[PLAYER_NUM]->shield ) + { + clientSendEquipUpdateToServer(EQUIP_ITEM_SLOT_SHIELD, EQUIP_ITEM_SUCCESS_UPDATE_QTY, PLAYER_NUM, + item2->type, item2->status, item2->beatitude, item2->count, item2->appearance, item2->identified); + } + } } if ( tempItem->count <= 0 ) { @@ -6016,61 +8856,7 @@ void actPlayer(Entity* my) { if ( !appearancesOfSimilarItems.empty() ) { - Uint32 originalAppearance = tempItem->appearance; - int originalVariation = originalAppearance % items[tempItem->type].variations; - - int tries = 100; - bool robot = false; - // we need to find a unique appearance within the list. - if ( tempItem->type == TOOL_SENTRYBOT || tempItem->type == TOOL_SPELLBOT || tempItem->type == TOOL_GYROBOT - || tempItem->type == TOOL_DUMMYBOT ) - { - robot = true; - tempItem->appearance += (local_rng.rand() % 100000) * 10; - } - else - { - tempItem->appearance = local_rng.rand(); - if ( tempItem->appearance % items[tempItem->type].variations != originalVariation ) - { - // we need to match the variation for the new appearance, take the difference so new varation matches - int change = (tempItem->appearance % items[tempItem->type].variations - originalVariation); - if ( tempItem->appearance < change ) // underflow protection - { - tempItem->appearance += items[tempItem->type].variations; - } - tempItem->appearance -= change; - int newVariation = tempItem->appearance % items[tempItem->type].variations; - assert(newVariation == originalVariation); - } - } - auto it = appearancesOfSimilarItems.find(tempItem->appearance); - while ( it != appearancesOfSimilarItems.end() && tries > 0 ) - { - if ( robot ) - { - tempItem->appearance += (local_rng.rand() % 100000) * 10; - } - else - { - tempItem->appearance = local_rng.rand(); - if ( tempItem->appearance % items[tempItem->type].variations != originalVariation ) - { - // we need to match the variation for the new appearance, take the difference so new varation matches - int change = (tempItem->appearance % items[tempItem->type].variations - originalVariation); - if ( tempItem->appearance < change ) // underflow protection - { - tempItem->appearance += items[tempItem->type].variations; - } - tempItem->appearance -= change; - int newVariation = tempItem->appearance % items[tempItem->type].variations; - assert(newVariation == originalVariation); - } - } - it = appearancesOfSimilarItems.find(tempItem->appearance); - --tries; - } - + Item::itemFindUniqueAppearance(tempItem, appearancesOfSimilarItems); } if ( multiplayer == CLIENT && itemIsEquipped(tempItem, PLAYER_NUM) && players[PLAYER_NUM]->paperDoll.isItemOnDoll(*tempItem) ) { @@ -6253,6 +9039,27 @@ void actPlayer(Entity* my) } } } + + real_t renderSetpoint = 0.0; + if ( stats[PLAYER_NUM]->getEffectActive(EFF_MIST_FORM) ) + { + renderSetpoint = 1.0; + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_FORCE_SHIELD) > 0 ) + { + renderSetpoint += 0.1; + } + else if ( stats[PLAYER_NUM]->getEffectActive(EFF_REFLECTOR_SHIELD) > 0 ) + { + renderSetpoint += 0.2; + } + + if ( abs(my->mistformGLRender - renderSetpoint) > 0.05 + || (my->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10)) ) + { + my->mistformGLRender = renderSetpoint; + serverUpdateEntityFSkill(my, 22); + } } } @@ -6260,10 +9067,11 @@ void actPlayer(Entity* my) bool oldInsectoidLevitate = players[PLAYER_NUM]->movement.insectoidLevitating; bool& insectoidLevitating = players[PLAYER_NUM]->movement.insectoidLevitating; insectoidLevitating = false; + bool shortModel = false; if ( players[PLAYER_NUM]->isLocalPlayer() || multiplayer == SERVER ) { - switch ( stats[PLAYER_NUM]->type ) + switch ( playerRace ) { case RAT: zOffset = 6; @@ -6277,13 +9085,46 @@ void actPlayer(Entity* my) case CREATURE_IMP: zOffset = -3.5; break; + case DRYAD: + if ( my->sprite == 1992 || my->sprite == 1993 ) + { + zOffset = 2.5; + shortModel = true; + } + else + { + zOffset = 0.06; + } + break; + case MYCONID: + if ( my->sprite == 1998 ) + { + zOffset = 1.5; + shortModel = true; + } + else + { + zOffset = -0.5; + } + break; + case GREMLIN: + zOffset = 1.5; + shortModel = true; + break; + case GNOME: + zOffset = 2.25; + shortModel = true; + break; + case SALAMANDER: + zOffset = -1.25; + break; default: break; } - bool prevlevitating = false; + bool prevlevitating = players[PLAYER_NUM]->mechanics.previouslyLevitating; if ( multiplayer != CLIENT ) { - if ( abs(zOffset) <= 0.05 ) + /*if ( abs(zOffset) <= 0.05 ) { if ( (my->z >= -2.05 && my->z <= -1.95 ) || (my->z >= -1.55 && my->z <= -1.45) ) { @@ -6297,11 +9138,11 @@ void actPlayer(Entity* my) { prevlevitating = true; } - } + }*/ } // sleeping - if ( stats[PLAYER_NUM]->EFFECTS[EFF_ASLEEP] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_ASLEEP) || stats[PLAYER_NUM]->getEffectActive(EFF_PROJECT_SPIRIT) ) { switch ( playerRace ) { @@ -6321,6 +9162,35 @@ void actPlayer(Entity* my) case TROLL: my->z = 1.5; break; + case DRYAD: + if ( my->sprite == 1992 || my->sprite == 1993 ) + { + my->z = 3.75; + } + else + { + my->z = 3.0; + } + break; + case MYCONID: + if ( my->sprite == 1998 ) + { + my->z = 3.75; + } + else + { + my->z = 3.0; + } + break; + case GREMLIN: + my->z = 4.0; + break; + case GNOME: + my->z = 4.0; + break; + case SALAMANDER: + my->z = 3.0; + break; default: my->z = 1.5; break; @@ -6342,11 +9212,18 @@ void actPlayer(Entity* my) // levitation levitating = isLevitating(stats[PLAYER_NUM]); + players[PLAYER_NUM]->mechanics.previouslyLevitating = levitating; if ( levitating ) { my->z -= 1; // floating - insectoidLevitating = (playerRace == INSECTOID && stats[PLAYER_NUM]->EFFECTS[EFF_FLUTTER]); + insectoidLevitating = (playerRace == INSECTOID && stats[PLAYER_NUM]->getEffectActive(EFF_FLUTTER)); + if ( playerRace == SALAMANDER + && stats[PLAYER_NUM]->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && stats[PLAYER_NUM]->getEffectActive(EFF_SALAMANDER_HEART) <= 2 ) + { + insectoidLevitating = true; + my->z += 1; + } if ( players[PLAYER_NUM]->isLocalPlayer() ) { int x = std::min(std::max(1, my->x / 16), map.width - 2); @@ -6357,6 +9234,24 @@ void actPlayer(Entity* my) } } } + else + { + //if ( players[PLAYER_NUM]->isLocalPlayer() || multiplayer == SERVER ) + //{ + // int x = std::min(std::max(0, floor(my->x / 16)), map.width - 1); + // int y = std::min(std::max(0, floor(my->y / 16)), map.height - 1); + // int mapTile = map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]; + // if ( animatedtiles[mapTile] ) + // { + // if ( mapTile >= 447 && mapTile <= 454 ) + // { + // my->z += 1.0; // float a little lower in bog. + // } + // } + //} + } + + my->creatureHandleLiftZ(); if ( !levitating && prevlevitating ) { @@ -6487,7 +9382,13 @@ void actPlayer(Entity* my) } else { - if ( playerRace == INSECTOID && stats[PLAYER_NUM]->EFFECTS[EFF_FLUTTER] && (my->z >= -2.05 && my->z <= -1.95) ) + if ( playerRace == INSECTOID && stats[PLAYER_NUM]->getEffectActive(EFF_FLUTTER) && (my->z >= -2.05 && my->z <= -1.95) ) + { + insectoidLevitating = true; + } + if ( playerRace == SALAMANDER + && ((stats[PLAYER_NUM]->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && stats[PLAYER_NUM]->getEffectActive(EFF_SALAMANDER_HEART) <= 2) + || (my->sprite == 2016 || my->sprite == 2017)) ) { insectoidLevitating = true; } @@ -6502,6 +9403,7 @@ void actPlayer(Entity* my) waterwalkingboots = true; } } + bool swimming = players[PLAYER_NUM]->movement.isPlayerSwimming(); if ( players[PLAYER_NUM]->isLocalPlayer() || multiplayer == SERVER ) { @@ -6511,7 +9413,7 @@ void actPlayer(Entity* my) int y = std::min(std::max(0, floor(my->y / 16)), map.height - 1); if ( local_rng.rand() % 400 == 0 && multiplayer != CLIENT ) { - my->increaseSkill(PRO_SWIMMING); + my->increaseSkill(PRO_LEGACY_SWIMMING); } my->z = 7; if ( playerRace == SPIDER || playerRace == RAT ) @@ -6552,15 +9454,18 @@ void actPlayer(Entity* my) playSoundPlayer(PLAYER_NUM, 249, 128); cameravars[PLAYER_NUM].shakex += .1; cameravars[PLAYER_NUM].shakey += 10; + createWaterSplash(my->x, my->y, 30); } else if ( swimmingtiles[map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]] && stats[PLAYER_NUM]->type == AUTOMATON ) { messagePlayer(PLAYER_NUM, MESSAGE_STATUS, Language::get(3702)); playSound(136, 128); + createWaterSplash(my->x, my->y, 30); } else if ( swimmingtiles[map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]] ) { playSound(136, 128); + createWaterSplash(my->x, my->y, 30); } } @@ -6589,16 +9494,16 @@ void actPlayer(Entity* my) Compendium_t::Events_t::eventUpdateWorld(PLAYER_NUM, Compendium_t::CPDM_SWIM_BURN_CURED, "murky water", 1); } - if ( stats[PLAYER_NUM]->EFFECTS[EFF_POLYMORPH] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_POLYMORPH) ) { - if ( stats[PLAYER_NUM]->EFFECTS[EFF_POLYMORPH] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_POLYMORPH) ) { my->setEffect(EFF_POLYMORPH, false, 0, true); my->effectPolymorph = 0; serverUpdateEntitySkill(my, 50); messagePlayer(PLAYER_NUM, MESSAGE_STATUS, Language::get(3192)); - if ( !stats[PLAYER_NUM]->EFFECTS[EFF_SHAPESHIFT] ) + if ( !stats[PLAYER_NUM]->getEffectActive(EFF_SHAPESHIFT) ) { messagePlayer(PLAYER_NUM, MESSAGE_STATUS, Language::get(3185)); } @@ -6662,7 +9567,7 @@ void actPlayer(Entity* my) if ( !my->flags[BURNING] ) { // Attempt to set the Entity on fire - my->SetEntityOnFire(); + my->SetEntityOnFire(nullptr); } if ( stats[PLAYER_NUM]->type == AUTOMATON || stats[PLAYER_NUM]->type == SKELETON ) { @@ -6704,7 +9609,7 @@ void actPlayer(Entity* my) CalloutRadialMenu& calloutMenu = CalloutMenu[PLAYER_NUM]; // object interaction - if ( intro == false ) + if ( intro == false && !players[PLAYER_NUM]->ghost.isActive() ) { clickDescription(PLAYER_NUM, NULL); // inspecting objects doStatueEditor(PLAYER_NUM); @@ -6857,7 +9762,6 @@ void actPlayer(Entity* my) { real_t startx = cameras[PLAYER_NUM].x * 16.0; real_t starty = cameras[PLAYER_NUM].y * 16.0; - static ConsoleVariable cvar_followerStartZ("/follower_start_z", -2.5); real_t startz = cameras[PLAYER_NUM].z + (4.5 - cameras[PLAYER_NUM].z) / 2.0 + *cvar_followerStartZ; real_t pitch = cameras[PLAYER_NUM].vang; if ( pitch < 0 || pitch > PI ) @@ -6865,8 +9769,6 @@ void actPlayer(Entity* my) pitch = 0; } - static ConsoleVariable cvar_followerMoveTo("/follower_moveto_z", 0.1); - static ConsoleVariable cvar_followerStartZLimit("/follower_start_z_limit", 7.5); // draw line from the players height and direction until we hit the ground. real_t previousx = startx; real_t previousy = starty; @@ -7306,53 +10208,101 @@ void actPlayer(Entity* my) } } } + + Entity* telekinesisTarget = nullptr; + if ( selectedEntity[PLAYER_NUM] == nullptr ) + { + if ( players[PLAYER_NUM]->magic.telekinesisTarget != 0 ) + { + telekinesisTarget = uidToEntity(players[PLAYER_NUM]->magic.telekinesisTarget); + selectedEntity[PLAYER_NUM] = telekinesisTarget; + players[PLAYER_NUM]->magic.telekinesisTarget = 0; + } + } + if ( selectedEntity[PLAYER_NUM] != NULL ) { followerMenu.followerToCommand = nullptr; Entity* parent = uidToEntity(selectedEntity[PLAYER_NUM]->skill[2]); - if ( selectedEntity[PLAYER_NUM]->behavior == &actMonster || (parent && parent->behavior == &actMonster) ) + if ( !telekinesisTarget ) { - // see if we selected a follower to process right click menu. - if ( parent && parent->monsterAllyIndex == PLAYER_NUM ) - { - followerMenu.followerToCommand = parent; - //messagePlayer(0, "limb"); - } - else if ( selectedEntity[PLAYER_NUM]->monsterAllyIndex == PLAYER_NUM ) - { - followerMenu.followerToCommand = selectedEntity[PLAYER_NUM]; - //messagePlayer(0, "head"); - } - - if ( followerMenu.followerToCommand ) + if ( selectedEntity[PLAYER_NUM]->behavior == &actMonster || (parent && parent->behavior == &actMonster) ) { - if ( players[PLAYER_NUM] && players[PLAYER_NUM]->entity - && followerMenu.followerToCommand->monsterTarget == players[PLAYER_NUM]->entity->getUID() ) + // see if we selected a follower to process right click menu. + if ( parent && parent->monsterAllyIndex == PLAYER_NUM ) { - // your ally is angry at you! - followerMenu.followerToCommand = nullptr; - followerMenu.optionPrevious = -1; + followerMenu.followerToCommand = parent; + //messagePlayer(0, "limb"); } - else + else if ( selectedEntity[PLAYER_NUM]->monsterAllyIndex == PLAYER_NUM ) { - followerMenu.recentEntity = followerMenu.followerToCommand; - followerMenu.initfollowerMenuGUICursor(true); - followerMenu.updateScrollPartySheet(); - selectedEntity[PLAYER_NUM] = NULL; - Player::soundActivate(); + followerMenu.followerToCommand = selectedEntity[PLAYER_NUM]; + //messagePlayer(0, "head"); + } + + if ( followerMenu.followerToCommand ) + { + if ( players[PLAYER_NUM] && players[PLAYER_NUM]->entity + && followerMenu.followerToCommand->monsterTarget == players[PLAYER_NUM]->entity->getUID() ) + { + // your ally is angry at you! + followerMenu.followerToCommand = nullptr; + followerMenu.optionPrevious = -1; + } + else + { + followerMenu.recentEntity = followerMenu.followerToCommand; + followerMenu.initfollowerMenuGUICursor(true); + followerMenu.updateScrollPartySheet(); + selectedEntity[PLAYER_NUM] = NULL; + Player::soundActivate(); + } } } } + if ( selectedEntity[PLAYER_NUM] ) { input.consumeBinaryToggle("Use"); //input.consumeBindingsSharedWithBinding("Use"); bool foundTinkeringKit = false; - if ( entityDist(my, selectedEntity[PLAYER_NUM]) <= TOUCHRANGE ) + bool wallLockInteract = false; + Item* foundWallLockKey = nullptr; + if ( telekinesisTarget || entityDist(my, selectedEntity[PLAYER_NUM]) <= TOUCHRANGE ) { inrange[PLAYER_NUM] = true; + if ( selectedEntity[PLAYER_NUM]->sprite >= 1585 && selectedEntity[PLAYER_NUM]->sprite <= 1592 ) + { + // wall lock keys, overwrite selection to base + if ( Entity* parent = uidToEntity(selectedEntity[PLAYER_NUM]->parent) ) + { + if ( parent->behavior == &actWallLock ) + { + selectedEntity[PLAYER_NUM] = parent; + } + } + } + else if ( selectedEntity[PLAYER_NUM]->sprite == 1151 || selectedEntity[PLAYER_NUM]->sprite == 1152 ) + { + // wall button, overwrite selection to base + if ( Entity* parent = uidToEntity(selectedEntity[PLAYER_NUM]->parent) ) + { + if ( parent->behavior == &actWallButton ) + { + selectedEntity[PLAYER_NUM] = parent; + } + } + } - if ( (selectedEntity[PLAYER_NUM]->behavior == &actItem + if ( selectedEntity[PLAYER_NUM]->behavior == &actWallLock ) + { + wallLockInteract = true; + if ( selectedEntity[PLAYER_NUM]->wallLockState == 0 ) + { + foundWallLockKey = players[PLAYER_NUM]->inventoryUI.hasKeyForWallLock(*selectedEntity[PLAYER_NUM]); + } + } + else if ( (selectedEntity[PLAYER_NUM]->behavior == &actItem || selectedEntity[PLAYER_NUM]->behavior == &actTorch || selectedEntity[PLAYER_NUM]->behavior == &actCrystalShard) && stats[PLAYER_NUM] && stats[PLAYER_NUM]->shield && stats[PLAYER_NUM]->defending @@ -7373,7 +10323,15 @@ void actPlayer(Entity* my) { if ( inrange[PLAYER_NUM] ) { - if ( foundTinkeringKit ) + if ( foundWallLockKey ) + { + strcpy((char*)net_packet->data, "LKEY"); // has key + } + else if ( wallLockInteract ) + { + strcpy((char*)net_packet->data, "LNOK"); // no key + } + else if ( foundTinkeringKit ) { strcpy((char*)net_packet->data, "SALV"); } @@ -7442,7 +10400,7 @@ void actPlayer(Entity* my) messagePlayer(PLAYER_NUM, MESSAGE_INTERACTION, Language::get(576), stats[i]->name); if ( players[PLAYER_NUM]->isLocalPlayer() && players[i] && players[i]->entity) { - if ( !stats[PLAYER_NUM]->EFFECTS[EFF_ROOTED] ) + if ( !stats[PLAYER_NUM]->getEffectActive(EFF_ROOTED) ) { double tangent = atan2(my->y - players[i]->entity->y, my->x - players[i]->entity->x); PLAYER_VELX += cos(tangent); @@ -7475,6 +10433,11 @@ void actPlayer(Entity* my) } equipmentBonus = std::max(-6, std::min(equipmentBonus, 4)); } + if ( stats[PLAYER_NUM]->type == GREMLIN ) + { + equipmentBonus += 2; + } + const int fociCastRange = -2; if (!intro && *cvar_playerLight) { if (!players[PLAYER_NUM]->isLocalPlayer() && multiplayer == CLIENT) { switch (PLAYER_TORCH) { @@ -7497,6 +10460,48 @@ void actPlayer(Entity* my) ++range_bonus; } break; + case 4: + light_type = "magic_foci_idle_red"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + break; + case 5: + light_type = "magic_foci_idle_blue"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + break; + case 6: + light_type = "magic_foci_idle_purple"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + break; + case 7: + light_type = "magic_foci_idle_yellow"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + break; + case 8: + light_type = "magic_foci_idle_brown"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + break; + case 9: + light_type = "magic_foci_idle_dark"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + break; + case 10: + light_type = "magic_foci_idle_light"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + break; } } else { // multiplayer != CLIENT if (stats[PLAYER_NUM]->shield && showEquipment && isHumanoid) { @@ -7518,8 +10523,62 @@ void actPlayer(Entity* my) ++range_bonus; } } + else if ( stats[PLAYER_NUM]->shield->type == TOOL_FOCI_FIRE ) { + light_type = "magic_foci_idle_red"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + } + else if ( stats[PLAYER_NUM]->shield->type == TOOL_FOCI_SNOW ) { + light_type = "magic_foci_idle_blue"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + } + else if ( stats[PLAYER_NUM]->shield->type == TOOL_FOCI_NEEDLES ) { + light_type = "magic_foci_idle_purple"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + } + else if ( stats[PLAYER_NUM]->shield->type == TOOL_FOCI_ARCS ) { + light_type = "magic_foci_idle_yellow"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + } + else if ( stats[PLAYER_NUM]->shield->type == TOOL_FOCI_SAND ) { + light_type = "magic_foci_idle_brown"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + } + else if ( stats[PLAYER_NUM]->shield->type == TOOL_FOCI_DARK_LIFE + || stats[PLAYER_NUM]->shield->type == TOOL_FOCI_DARK_RIFT + || stats[PLAYER_NUM]->shield->type == TOOL_FOCI_DARK_SILENCE + || stats[PLAYER_NUM]->shield->type == TOOL_FOCI_DARK_SUPPRESS + || stats[PLAYER_NUM]->shield->type == TOOL_FOCI_DARK_VENGEANCE ) { + light_type = "magic_foci_idle_dark"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + } + else if ( stats[PLAYER_NUM]->shield->type == TOOL_FOCI_LIGHT_JUSTICE + || stats[PLAYER_NUM]->shield->type == TOOL_FOCI_LIGHT_PEACE + || stats[PLAYER_NUM]->shield->type == TOOL_FOCI_LIGHT_PROVIDENCE + || stats[PLAYER_NUM]->shield->type == TOOL_FOCI_LIGHT_PURITY + || stats[PLAYER_NUM]->shield->type == TOOL_FOCI_LIGHT_SANCTUARY ) { + light_type = "magic_foci_idle_light"; + if ( stats[PLAYER_NUM]->defending ) { + range_bonus += fociCastRange; + } + } else if (players[PLAYER_NUM]->isLocalPlayer()) { ambientLight = true; + if ( stats[PLAYER_NUM]->getEffectActive(EFF_ENSEMBLE_LYRE) ) + { + range_bonus += static_cast(stats[PLAYER_NUM]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_TIER_2)); + } if ( stats[PLAYER_NUM]->sneaking ) { light_type = "player_sneaking"; range_bonus += equipmentBonus; @@ -7527,13 +10586,20 @@ void actPlayer(Entity* my) else { light_type = "player_ambient"; + if ( stats[PLAYER_NUM]->getEffectActive(EFF_ENSEMBLE_LYRE) ) + { + light_type = "player_ambient_ensemble"; + } } } } else if (players[PLAYER_NUM]->isLocalPlayer()) { ambientLight = true; - + if ( stats[PLAYER_NUM]->getEffectActive(EFF_ENSEMBLE_LYRE) ) + { + range_bonus += static_cast(stats[PLAYER_NUM]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_TIER_2)); + } // carrying no light source if (playerRace == RAT) { if ( stats[PLAYER_NUM]->sneaking ) @@ -7581,25 +10647,88 @@ void actPlayer(Entity* my) } else { light_type = "player_ambient"; + if ( stats[PLAYER_NUM]->getEffectActive(EFF_ENSEMBLE_LYRE) ) + { + light_type = "player_ambient_ensemble"; + } } } } } if (*cvar_playerLight) { - if (my->flags[BURNING]) { + if (my->flags[BURNING] ) { my->light = addLight(my->x / 16, my->y / 16, "player_burning"); } + else if ( my->mistformGLRender > 0.9 ) + { + my->light = addLight(my->x / 16, my->y / 16, "mistform_glow", range_bonus); + } + else if ( playerRace == SALAMANDER && (my->sprite == 2016 || my->sprite == 2017) ) + { + my->light = addLight(my->x / 16, my->y / 16, "magic_foci_idle_red"); + } else if (!my->light) { my->light = addLight(my->x / 16, my->y / 16, light_type, range_bonus, ambientLight ? PLAYER_NUM + 1 : 0); } } + if ( !intro && PLAYER_NUM == clientnum ) + { + if ( stats[clientnum]->type == MYCONID && stats[clientnum]->playerRace == RACE_MYCONID && stats[clientnum]->stat_appearance == 0 + && stats[clientnum]->helmet ) + { + gameStatistics[STATISTICS_NO_CAP] = std::max(0, gameStatistics[STATISTICS_NO_CAP]); + } + else + { + gameStatistics[STATISTICS_NO_CAP] = -1; + } + if ( stats[clientnum]->getEffectActive(EFF_GROWTH) >= 2 + && ((stats[clientnum]->type == MYCONID && stats[clientnum]->playerRace == RACE_MYCONID) + || (stats[clientnum]->type == DRYAD && stats[clientnum]->playerRace == RACE_DRYAD)) && stats[clientnum]->stat_appearance == 0 + && !stats[clientnum]->helmet ) + { + gameStatistics[STATISTICS_DONT_TOUCH_HAIR] = std::max(0, gameStatistics[STATISTICS_DONT_TOUCH_HAIR]); + } + else + { + gameStatistics[STATISTICS_DONT_TOUCH_HAIR] = -1; + } + if ( stats[clientnum]->type == SALAMANDER && stats[clientnum]->playerRace == RACE_SALAMANDER && stats[clientnum]->stat_appearance == 0 + && stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) >= 3 && stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) <= 4 ) + { + gameStatistics[STATISTICS_GARGOYLES_QUEST] = std::max(0, gameStatistics[STATISTICS_GARGOYLES_QUEST]); + } + else + { + gameStatistics[STATISTICS_GARGOYLES_QUEST] = -1; + } + if ( stats[clientnum]->type == SALAMANDER && stats[clientnum]->playerRace == RACE_SALAMANDER && stats[clientnum]->stat_appearance == 0 + && stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) <= 2 ) + { + gameStatistics[STATISTICS_FIRE_FIGHTER] = std::max(0, gameStatistics[STATISTICS_FIRE_FIGHTER]); + } + else + { + gameStatistics[STATISTICS_FIRE_FIGHTER] = -1; + } + if ( stats[clientnum]->type == SALAMANDER && stats[clientnum]->playerRace == RACE_SALAMANDER && stats[clientnum]->stat_appearance == 0 + && !stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) ) + { + gameStatistics[STATISTICS_DISCIPLINE] = std::max(0, gameStatistics[STATISTICS_DISCIPLINE]); + } + else + { + gameStatistics[STATISTICS_DISCIPLINE] = -1; + } + } + // server controls players primarily if ( players[PLAYER_NUM]->isLocalPlayer() || multiplayer == SERVER || StatueManager.activeEditing ) { // set head model my->sprite = playerHeadSprite(playerRace, stats[PLAYER_NUM]->sex, - playerAppearance, PLAYER_ATTACK ? PLAYER_ATTACKTIME : 0); + playerAppearance, PLAYER_ATTACK ? PLAYER_ATTACKTIME : 0, PLAYER_NUM); } if ( multiplayer != CLIENT ) { @@ -7685,6 +10814,39 @@ void actPlayer(Entity* my) { Compendium_t::Events_t::eventUpdateCodex(PLAYER_NUM, Compendium_t::CPDM_CLASS_WGT_SLOWEST, "wgt", players[PLAYER_NUM]->movement.getCharacterWeight()); + if ( stats[PLAYER_NUM]->type == MYCONID ) + { + Uint32 lifetime = TICKS_PER_SECOND * 3; + Entity* spellTimer = createParticleTimer(my, lifetime + TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SPORES_TRAIL; + spellTimer->particleTimerCountdownSprite = 248; + spellTimer->yaw = 0.0; + spellTimer->x = my->x; + spellTimer->y = my->y; + spellTimer->particleTimerVariable1 = 0; + spellTimer->particleTimerVariable2 = SPELL_MYCELIUM_SPORES; + spellTimer->particleTimerVariable4 = 0; + + my->thrownProjectileParticleTimerUID = spellTimer->getUID(); + particleTimerEffects.emplace(std::pair(spellTimer->getUID(), ParticleTimerEffect_t())); + + auto findEffects = particleTimerEffects.find(spellTimer->getUID()); + auto& effect = findEffects->second.effectMap[spellTimer->ticks + 1]; // insert x ticks beyond last effect + if ( findEffects->second.effectMap.size() == 1 ) + { + effect.firstEffect = true; + } + int spellID = spellTimer->particleTimerVariable2; + auto particleEffectType = (spellID == SPELL_MYCELIUM_BOMB || spellID == SPELL_MYCELIUM_SPORES) + ? ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM + : ParticleTimerEffect_t::EffectType::EFFECT_SPORES; + effect.effectType = particleEffectType; + effect.x = static_cast(my->x / 16) * 16.0 + 8.0; + effect.y = static_cast(my->y / 16) * 16.0 + 8.0; + effect.yaw = 0.0; + //floorMagicCreateSpores(my, my->x, my->y, my, 5, SPELL_MYCELIUM_BOMB); + } + // die //TODO: Refactor. playSoundEntity(my, 28, 128); Entity* gib = spawnGib(my); @@ -7719,6 +10881,16 @@ void actPlayer(Entity* my) spellnode = spellnode->next; spell_t* spell = (spell_t*)oldnode->element; spell->magic_effects_node = NULL; + if ( spell->sustainEffectDissipate >= 0 ) + { + if ( stats[PLAYER_NUM]->getEffectActive(spell->sustainEffectDissipate) ) + { + if ( stats[PLAYER_NUM]->EFFECTS_TIMERS[spell->sustainEffectDissipate] > 0 ) + { + stats[PLAYER_NUM]->EFFECTS_TIMERS[spell->sustainEffectDissipate] = 1; + } + } + } list_RemoveNode(oldnode); } int c; @@ -8004,8 +11176,16 @@ void actPlayer(Entity* my) { continue; } - if ( item->type == ARTIFACT_ORB_PURPLE ) + if ( item->type == ARTIFACT_ORB_PURPLE || item->type == TOOL_DUCK ) { + Item** slot = itemSlot(stats[PLAYER_NUM], item); + if ( slot != nullptr ) + { + *slot = nullptr; + } + + players[PLAYER_NUM]->paperDoll.updateSlots(); + int c = item->count; for ( c = item->count; c > 0; c-- ) { @@ -8030,7 +11210,7 @@ void actPlayer(Entity* my) entity->skill[14] = item->appearance; entity->skill[15] = item->identified; } - break; + list_RemoveNode(node); } } } @@ -8070,6 +11250,42 @@ void actPlayer(Entity* my) } } } + else + { + Item** slots[] = { + &stats[PLAYER_NUM]->helmet, + &stats[PLAYER_NUM]->breastplate, + &stats[PLAYER_NUM]->gloves, + &stats[PLAYER_NUM]->shoes, + &stats[PLAYER_NUM]->shield, + &stats[PLAYER_NUM]->weapon, + &stats[PLAYER_NUM]->cloak, + &stats[PLAYER_NUM]->amulet, + &stats[PLAYER_NUM]->ring, + &stats[PLAYER_NUM]->mask, + }; + constexpr int num_slots = sizeof(slots) / sizeof(slots[0]); + for ( int c = 0; c < num_slots; ++c ) { + if ( slots[c] ) + { + if ( Item* item = *(slots[c]) ) + { + if ( item->type == ARTIFACT_ORB_PURPLE || item->type == TOOL_DUCK ) + { + *(slots[c]) = nullptr; + if ( item->node ) + { + list_RemoveNode(item->node); + } + else + { + free(item); + } + } + } + } + } + } deleteMultiplayerSaveGames(); //Will only delete save games if was last player alive. } @@ -8174,12 +11390,12 @@ void actPlayer(Entity* my) if ( (players[PLAYER_NUM]->isLocalPlayer()) && intro == false ) { // effects of drunkenness - if ( (stats[PLAYER_NUM]->EFFECTS[EFF_DRUNK] && (stats[PLAYER_NUM]->type != GOATMAN)) - || stats[PLAYER_NUM]->EFFECTS[EFF_WITHDRAWAL] ) + if ( (stats[PLAYER_NUM]->getEffectActive(EFF_DRUNK) && (stats[PLAYER_NUM]->type != GOATMAN)) + || stats[PLAYER_NUM]->getEffectActive(EFF_WITHDRAWAL) ) { my->char_drunk++; int drunkInterval = TICKS_PER_SECOND * 6; - if ( stats[PLAYER_NUM]->EFFECTS[EFF_WITHDRAWAL] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_WITHDRAWAL) ) { if ( PLAYER_ALIVETIME < TICKS_PER_SECOND * 16 ) { @@ -8247,64 +11463,190 @@ void actPlayer(Entity* my) // perform collision detection dist = clipMove(&my->x, &my->y, PLAYER_VELX, PLAYER_VELY, my); + if ( multiplayer != CLIENT && !intro ) + { + if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) >= 2 ) + { + if ( hit.entity ) + { + my->handleKnockbackDamage(*stats[PLAYER_NUM], hit.entity); + } + } + + if ( my->isInvisible() && !assailant[PLAYER_NUM] ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, dist, 0.1, nullptr); + } + if ( levitating && !swimming ) + { + const int index_x = static_cast(my->x) >> 4; + const int index_y = static_cast(my->y) >> 4; + int index = (index_y)*MAPLAYERS + (index_x)*MAPLAYERS * map.height; + if ( index_x >= 0 && index_x < map.width && index_y >= 0 && index_y < map.height ) + { + if ( !map.tiles[index] || swimmingtiles[map.tiles[index_y * MAPLAYERS + index_x * MAPLAYERS * map.height]] + || lavatiles[map.tiles[index_y * MAPLAYERS + index_x * MAPLAYERS * map.height]] ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_LEVITATION, dist, 0.5, nullptr); + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_FLUTTER) ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_FLUTTER, dist, 0.5, nullptr); + } + } + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_LIGHTEN_LOAD) ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_LIGHTEN_LOAD, dist, 0.1, nullptr); + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_DASH, dist, 0.4, nullptr); + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_FAST) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( effectStrength & (1 << (i + 1)) && players[i]->entity ) + { + players[i]->mechanics.updateSustainedSpellEvent(SPELL_SPEED, dist, 0.025, nullptr); + break; + } + } + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_NIMBLENESS) ) + { + int caster = ((stats[PLAYER_NUM]->getEffectActive(EFF_NIMBLENESS) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_NIMBLENESS, dist, 0.002, nullptr); + } + } + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_GREATER_MIGHT) ) + { + int caster = ((stats[PLAYER_NUM]->getEffectActive(EFF_GREATER_MIGHT) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_GREATER_MIGHT, dist, 0.002, nullptr); + } + } + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_COUNSEL) ) + { + int caster = ((stats[PLAYER_NUM]->getEffectActive(EFF_COUNSEL) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_COUNSEL, dist, 0.002, nullptr); + } + } + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_STURDINESS) ) + { + int caster = ((stats[PLAYER_NUM]->getEffectActive(EFF_STURDINESS) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_STURDINESS, dist, 0.002, nullptr); + } + } + } + } + // bumping into monsters disturbs them - if ( hit.entity && !intro && multiplayer != CLIENT ) + if ( hit.entity && !intro ) { - if ( !everybodyfriendly && hit.entity->behavior == &actMonster ) + if ( multiplayer != CLIENT ) { - bool enemy = my->checkEnemy(hit.entity); - if ( enemy && !hit.entity->isInertMimic() ) + if ( !everybodyfriendly && hit.entity->behavior == &actMonster ) { - if ( hit.entity->monsterState == MONSTER_STATE_WAIT || (hit.entity->monsterState == MONSTER_STATE_HUNT && hit.entity->monsterTarget == 0) ) + bool enemy = my->checkEnemy(hit.entity); + if ( enemy && !hit.entity->isInertMimic() ) { - hit.entity->lookAtEntity(*my); + if ( hit.entity->monsterState == MONSTER_STATE_WAIT || (hit.entity->monsterState == MONSTER_STATE_HUNT && hit.entity->monsterTarget == 0) ) + { + hit.entity->lookAtEntity(*my); + } + } + } + else if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) && hit.entity->behavior == &actDoor ) + { + if ( hit.entity->doorHealth > 0 ) + { + hit.entity->doorHealth = 0; + magicOnSpellCastEvent(my, my, nullptr, + SPELL_DASH, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + + if ( hit.entity->behavior == &actDoorFrame && + hit.entity->flags[INVISIBLE] ) + { + // code that almost fixes door frame collision + bool doSlide = true; + if ( hit.entity->yaw >= -0.1 && hit.entity->yaw <= 0.1 ) + { + // east/west doorway + if ( static_cast(hit.entity->y / 16) != static_cast(my->y / 16) ) + { + doSlide = false; + } + } + else + { + // north/south doorway + if ( static_cast(hit.entity->x / 16) != static_cast(my->x / 16) ) + { + doSlide = false; + } + } + + if ( doSlide ) + { + real_t centerx = floor(hit.entity->x / 16) * 16 + 8; + real_t centery = floor(hit.entity->y / 16) * 16 + 8; + real_t tangent = atan2(centery - my->y, centerx - my->x); + real_t dir = atan2(my->vel_y, my->vel_x); + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + //real_t dirx = cos(tangent - dir); + //real_t diry = sin(tangent - dir); + //real_t tangent2 = atan2(diry, dirx); + real_t diff = tangent - dir; + if ( diff >= PI ) + { + diff -= 2 * PI; + } + else if ( diff < -PI ) + { + diff += 2 * PI; + } + //messagePlayer(0, MESSAGE_DEBUG, "%.2f", diff); + if ( abs(diff) < PI / 2 ) + { + dir += (tangent - dir); + my->vel_x = spd * cos(dir); + my->vel_y = spd * sin(dir); } } + + /*if ( abs(slidex) > 0.0 ) + { + PLAYER_VELX += slidex; + } + if ( abs(slidey) > 0.0 ) + { + PLAYER_VELY += slidey; + }*/ + //dist += clipMove(&my->x, &my->y, slidex, slidey, my); } - else if ( stats[PLAYER_NUM]->EFFECTS[EFF_DASH] && hit.entity->behavior == &actDoor ) - { - hit.entity->doorHealth = 0; - } - //else if ( hit.entity->behavior == &actDoorFrame && - // hit.entity->flags[INVISIBLE] ) - //{ - // // code that almost fixes door frame collision - // if ( hit.entity->yaw >= -0.1 && hit.entity->yaw <= 0.1 ) - // { - // // east/west doorway - // if ( my->y < floor(hit.entity->y / 16) * 16 + 8 ) - // { - // // slide south - // PLAYER_VELX = 0; - // PLAYER_VELY = .25; - // } - // else - // { - // // slide north - // PLAYER_VELX = 0; - // PLAYER_VELY = -.25; - // } - // } - // else - // { - // // north/south doorway - // if ( my->x < floor(hit.entity->x / 16) * 16 + 8 ) - // { - // // slide east - // PLAYER_VELX = .25; - // PLAYER_VELY = 0; - // } - // else - // { - // // slide west - // PLAYER_VELX = -.25; - // PLAYER_VELY = 0; - // } - // } - // my->x += PLAYER_VELX; - // my->y += PLAYER_VELY; - // dist = sqrt(PLAYER_VELX * PLAYER_VELX + PLAYER_VELY * PLAYER_VELY); - //} } } else @@ -8423,6 +11765,102 @@ void actPlayer(Entity* my) dist = clipMove(&my->x, &my->y, PLAYER_VELX, PLAYER_VELY, my); + if ( multiplayer != CLIENT && !intro ) + { + if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) >= 2 ) + { + if ( hit.entity ) + { + my->handleKnockbackDamage(*stats[PLAYER_NUM], hit.entity); + } + } + if ( my->isInvisible() ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, dist, 0.1, nullptr); + } + if ( levitating && !swimming ) + { + const int index_x = static_cast(my->x) >> 4; + const int index_y = static_cast(my->y) >> 4; + int index = (index_y)*MAPLAYERS + (index_x)*MAPLAYERS * map.height; + if ( index_x >= 0 && index_x < map.width && index_y >= 0 && index_y < map.height ) + { + if ( !map.tiles[index] || swimmingtiles[map.tiles[index_y * MAPLAYERS + index_x * MAPLAYERS * map.height]] + || lavatiles[map.tiles[index_y * MAPLAYERS + index_x * MAPLAYERS * map.height]] ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_LEVITATION, dist, 0.5, nullptr); + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_FLUTTER) ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_FLUTTER, dist, 0.5, nullptr); + } + } + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_LIGHTEN_LOAD) ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_LIGHTEN_LOAD, dist, 0.1, nullptr); + } + if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) ) + { + players[PLAYER_NUM]->mechanics.updateSustainedSpellEvent(SPELL_DASH, dist, 0.4, nullptr); + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_FAST) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( effectStrength & (1 << (i + 1)) && players[i]->entity ) + { + players[i]->mechanics.updateSustainedSpellEvent(SPELL_SPEED, dist, 0.025, nullptr); + break; + } + } + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_NIMBLENESS) ) + { + int caster = ((stats[PLAYER_NUM]->getEffectActive(EFF_NIMBLENESS) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_NIMBLENESS, dist, 0.002, nullptr); + } + } + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_GREATER_MIGHT) ) + { + int caster = ((stats[PLAYER_NUM]->getEffectActive(EFF_GREATER_MIGHT) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_GREATER_MIGHT, dist, 0.002, nullptr); + } + } + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_COUNSEL) ) + { + int caster = ((stats[PLAYER_NUM]->getEffectActive(EFF_COUNSEL) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_COUNSEL, dist, 0.002, nullptr); + } + } + } + if ( Uint8 effectStrength = stats[PLAYER_NUM]->getEffectActive(EFF_STURDINESS) ) + { + int caster = ((stats[PLAYER_NUM]->getEffectActive(EFF_STURDINESS) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_STURDINESS, dist, 0.002, nullptr); + } + } + } + } + // bumping into monsters disturbs them if ( hit.entity && !intro ) { @@ -8437,9 +11875,14 @@ void actPlayer(Entity* my) } } } - else if ( stats[PLAYER_NUM]->EFFECTS[EFF_DASH] && hit.entity->behavior == &actDoor ) + else if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) && hit.entity->behavior == &actDoor ) { - hit.entity->doorHealth = 0; + if ( hit.entity->doorHealth > 0 ) + { + hit.entity->doorHealth = 0; + magicOnSpellCastEvent(my, my, nullptr, + SPELL_DASH, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } } } } @@ -8456,17 +11899,27 @@ void actPlayer(Entity* my) dist = sqrt(PLAYER_VELX * PLAYER_VELX + PLAYER_VELY * PLAYER_VELY); } - if ( (players[PLAYER_NUM]->isLocalPlayer()) && ticks % 65 == 0 && stats[PLAYER_NUM]->EFFECTS[EFF_TELEPATH] ) + if ( (players[PLAYER_NUM]->isLocalPlayer()) && ticks % 65 == 0 && stats[PLAYER_NUM]->getEffectActive(EFF_TELEPATH) ) { for ( node_t* mapNode = map.creatures->first; mapNode != nullptr; mapNode = mapNode->next ) { Entity* mapCreature = (Entity*)mapNode->element; if ( mapCreature ) { - if ( stats[PLAYER_NUM]->EFFECTS[EFF_TELEPATH] && !intro ) + if ( (stats[PLAYER_NUM]->getEffectActive(EFF_TELEPATH) + /*|| (mapCreature->getStats() && mapCreature->getStats()->getEffectActive(EFF_DETECT_ENEMY))*/ + ) + && !intro ) { // periodically set the telepath rendering flag. - mapCreature->monsterEntityRenderAsTelepath = 1; + /*if ( (mapCreature->getStats() && mapCreature->getStats()->getEffectActive(EFF_DETECT_ENEMY)) ) + { + mapCreature->monsterEntityRenderAsTelepath = 2; + } + else*/ + { + mapCreature->monsterEntityRenderAsTelepath = 1; + } } else { @@ -8477,6 +11930,16 @@ void actPlayer(Entity* my) } Entity* helmet = nullptr; + bool ignoreHelmetOffsetFromMask = false; + Entity* legleft = nullptr; + Entity* legright = nullptr; + int torsoCovering = 0; + + if ( auto node = list_Node(&my->children, 11) ) + { + additionalLimb = (Entity*)node->element; + } + // move bodyparts if ( isHumanoid ) @@ -8504,13 +11967,26 @@ void actPlayer(Entity* my) { entity->z += 0.25; } + else if ( playerRace == GNOME ) + { + entity->z += 0.5; + } } if ( bodypart > 12 ) + { + // for GENIUS flag to not draw into the camera + entity->sizex = 4; + entity->sizey = 4; + } + if ( bodypart > 13 ) { entity->flags[INVISIBLE] = true; entity->flags[INVISIBLE_DITHER] = false; - continue; + if ( bodypart > 15 ) + { + continue; + } } entity->yaw = my->yaw; @@ -8519,6 +11995,7 @@ void actPlayer(Entity* my) if ( bodypart == 2 ) { rightbody = (Entity*)node->next->element; + legright = entity; } if ( bodypart == 5 ) { @@ -8531,7 +12008,7 @@ void actPlayer(Entity* my) limbSpeed = 1 / 12.f; pitchLimit = PI / 8.f; } - if ( stats[PLAYER_NUM]->EFFECTS[EFF_DASH] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) ) { limbSpeed = 1 / 12.f; } @@ -8544,8 +12021,7 @@ void actPlayer(Entity* my) { bendArm = false; } - else if ( shield->sprite >= items[SPELLBOOK_LIGHT].index - && shield->sprite < (items[SPELLBOOK_LIGHT].index + items[SPELLBOOK_LIGHT].variations) ) + else if ( shieldSpriteIsSpellbook(shield->sprite) ) { bendArm = false; } @@ -8688,10 +12164,118 @@ void actPlayer(Entity* my) } else if ( bodypart == 3 || bodypart == 4 || bodypart == 8 ) // left leg, right arm, cloak { + if ( bodypart == 3 ) + { + legleft = entity; + } + if ( bodypart == 4 ) { weaponarm = entity; - if ( PLAYER_ATTACK == 1 || PLAYER_ATTACK == PLAYER_POSE_GOLEM_SMASH ) + if ( PLAYER_ATTACK == MONSTER_POSE_PARRY ) + { + if ( PLAYER_ATTACKTIME == 0 ) + { + PLAYER_ARMBENDED = 0; + PLAYER_WEAPONYAW = 0; + entity->pitch = 0; + entity->roll = -PI / 4; + entity->skill[1] = 0; + } + else + { + limbAnimateToLimit(my, ANIMATE_WEAPON_YAW, -0.15, 15 * PI / 8, false, 0.0); + limbAnimateToLimit(entity, ANIMATE_PITCH, -0.25, 15 * PI / 8, false, 0.0); + if ( PLAYER_ATTACKTIME >= 35 ) + { + entity->skill[0] = rightbody->skill[0]; + entity->skill[1] = 0; + PLAYER_WEAPONYAW = 0; + entity->pitch = rightbody->pitch; + entity->roll = 0; + PLAYER_ARMBENDED = 0; + PLAYER_ATTACK = 0; + } + } + } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + // magic wiggle hands + entity->skill[1] = 0; + PLAYER_ARMBENDED = 0; + PLAYER_WEAPONYAW = 0; + entity->roll = 0; + entity->pitch = 0; + entity->yaw = my->yaw; + + real_t circleAmount = (1 * PI / 8); + if ( playerRace == TROLL || playerRace == CREATURE_IMP ) + { + circleAmount *= 0.2; + } + real_t scaleDown = 1.0; + if ( (my->playerCastTimeAnim - PLAYER_ATTACKTIME) < 10 ) + { + // scale down + scaleDown = 1.0 - (10 - (my->playerCastTimeAnim - PLAYER_ATTACKTIME)) / 10.0; + } + circleAmount *= scaleDown; + real_t circleTime = 20.0; + entity->pitch = circleAmount * cos(2 * PI * (PLAYER_ATTACKTIME / (real_t)circleTime)); + if ( playerRace == TROLL ) + { + entity->pitch -= scaleDown * (PI / 4) * std::min(1.0, (PLAYER_ATTACKTIME / (real_t)5)); + } + else if ( playerRace == CREATURE_IMP ) + { + entity->pitch -= scaleDown * (PI / 8) * std::min(1.0, (PLAYER_ATTACKTIME / (real_t)5)); + } + PLAYER_WEAPONYAW = circleAmount * sin(2 * PI * (PLAYER_ATTACKTIME / (real_t)circleTime)); + + if ( PLAYER_ATTACKTIME >= my->playerCastTimeAnim ) + { + my->playerCastTimeAnim = 0; + entity->skill[0] = rightbody->skill[0]; + PLAYER_WEAPONYAW = 0; + entity->pitch = rightbody->pitch; + entity->roll = 0; + PLAYER_ARMBENDED = 0; + PLAYER_ATTACK = 0; + } + } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2 ) + { + // touch spell intermission + my->playerCastTimeAnim = 0; + entity->skill[1] = 0; + PLAYER_ARMBENDED = 0; + PLAYER_WEAPONYAW = 0; + entity->roll = 0; + entity->pitch = 0; + entity->yaw = my->yaw; + + if ( playerRace == TROLL ) + { + entity->pitch -= (PI / 4) * std::min(1.0, (PLAYER_ATTACKTIME / (real_t)5)); + } + else if ( playerRace == CREATURE_IMP ) + { + entity->pitch -= (PI / 8) * std::min(1.0, (PLAYER_ATTACKTIME / (real_t)5)); + } + } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_CAST2 ) + { + // cancelled spell + entity->skill[1] = 0; + my->playerCastTimeAnim = 0; + entity->skill[0] = rightbody->skill[0]; + PLAYER_WEAPONYAW = 0; + entity->pitch = rightbody->pitch; + entity->roll = 0; + PLAYER_ARMBENDED = 0; + PLAYER_ATTACK = 0; + } + else if ( PLAYER_ATTACK == 1 || PLAYER_ATTACK == PLAYER_POSE_GOLEM_SMASH ) { // vertical chop if ( PLAYER_ATTACKTIME == 0 ) @@ -8934,7 +12518,9 @@ void actPlayer(Entity* my) if ( weaponarm->skill[1] == 0 ) { real_t targetPitch = 14 * PI / 8; - if ( weaponarm->sprite == items[CROSSBOW].index || weaponarm->sprite == items[HEAVY_CROSSBOW].index ) + if ( weaponarm->sprite == items[CROSSBOW].index + || weaponarm->sprite == items[HEAVY_CROSSBOW].index + || weaponarm->sprite == items[BLACKIRON_CROSSBOW].index ) { targetPitch = 15 * PI / 8; } @@ -8976,6 +12562,36 @@ void actPlayer(Entity* my) } } } + else if ( PLAYER_ATTACK == MONSTER_POSE_FLAIL_SWING ) + { + if ( PLAYER_ATTACKTIME == 0 ) + { + // init rotations + PLAYER_ARMBENDED = 1; + PLAYER_WEAPONYAW = 0; + entity->pitch = 13 * PI / 8; + entity->roll = 0; + } + + entity->skill[1]++; + if ( PLAYER_ATTACKTIME >= TICKS_PER_SECOND * .75 ) + { + PLAYER_ATTACK = MONSTER_POSE_FLAIL_SWING_RETURN; + } + } + else if ( PLAYER_ATTACK == MONSTER_POSE_FLAIL_SWING_RETURN ) + { + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, 0.15, 7 * PI / 4, false, 0.0) ) + { + entity->skill[0] = rightbody->skill[0]; + entity->skill[1] = 0; + PLAYER_WEAPONYAW = 0; + entity->pitch = rightbody->pitch; + entity->roll = 0; + PLAYER_ARMBENDED = 0; + PLAYER_ATTACK = 0; + } + } } else if ( bodypart == 8 ) { @@ -9032,6 +12648,34 @@ void actPlayer(Entity* my) } } } + + if ( bodypart == 8 ) + { + if ( fabs(PLAYER_VELX) > 0.1 || fabs(PLAYER_VELY) > 0.1 ) + { + if ( entity->skill[0] == 0 ) + { + entity->pitch -= dist * PLAYERWALKSPEED * 0.05; + if ( entity->pitch < -PI / 4.0 ) + { + entity->skill[0] = 1; + } + } + else + { + entity->pitch += dist * PLAYERWALKSPEED * 0.05; + if ( entity->pitch > -PI / 8.0 ) + { + entity->skill[0] = 0; + } + } + } + else + { + entity->pitch += PLAYERWALKSPEED / 5; + entity->pitch = std::min(entity->pitch, 0.0); + } + } } else if ( bodypart != 4 || (PLAYER_ATTACK == 0 && PLAYER_ATTACKTIME == 0) ) { @@ -9050,7 +12694,7 @@ void actPlayer(Entity* my) limbSpeed = 1 / 12.f; pitchLimit = PI / 8.f; } - if ( stats[PLAYER_NUM]->EFFECTS[EFF_DASH] ) + if ( stats[PLAYER_NUM]->getEffectActive(EFF_DASH) ) { limbSpeed = 1 / 12.f; } @@ -9109,842 +12753,1457 @@ void actPlayer(Entity* my) switch ( bodypart ) { // torso - case 1: - torso = entity; - entity->focalx = limbs[playerRace][1][0]; - entity->focaly = limbs[playerRace][1][1]; - entity->focalz = limbs[playerRace][1][2]; - entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; - if ( multiplayer != CLIENT ) + case 1: + torso = entity; + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[playerRace][1][0]; + entity->focaly = limbs[playerRace][1][1]; + entity->focalz = limbs[playerRace][1][2]; + entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + if ( multiplayer != CLIENT ) + { + if ( stats[PLAYER_NUM]->breastplate == NULL || !showEquipment ) + { + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_TORSO, my->sprite); + } + else { - if ( stats[PLAYER_NUM]->breastplate == NULL || !showEquipment ) + int torsoModel = itemModel(stats[PLAYER_NUM]->breastplate, shortModel); + if ( (torsoModel >= items[SHAWL].index && torsoModel < items[SHAWL].index + items[SHAWL].variations) + || (torsoModel >= items[SHAWL].indexShort && torsoModel < items[SHAWL].indexShort + items[SHAWL].variations) + || (torsoModel >= items[IRON_PAULDRONS].index && torsoModel < items[IRON_PAULDRONS].index + items[IRON_PAULDRONS].variations) + || (torsoModel >= items[IRON_PAULDRONS].indexShort && torsoModel < items[IRON_PAULDRONS].indexShort + items[IRON_PAULDRONS].variations) ) { - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_TORSO); + torsoCovering = torsoModel; + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_TORSO, my->sprite); } else { - entity->sprite = itemModel(stats[PLAYER_NUM]->breastplate); + entity->sprite = torsoModel; } - if ( multiplayer == SERVER ) + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) - { - serverUpdateEntityBodypart(my, bodypart); - } + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); } } } - my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_TORSO); - break; - // right leg - case 2: - entity->focalx = limbs[playerRace][2][0]; - entity->focaly = limbs[playerRace][2][1]; - entity->focalz = limbs[playerRace][2][2]; - entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; - if ( multiplayer != CLIENT ) + } + my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_TORSO); + break; + // right leg + case 2: + { + entity->focalx = limbs[playerRace][2][0]; + entity->focaly = limbs[playerRace][2][1]; + entity->focalz = limbs[playerRace][2][2]; + entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + bool shortSprite = (torso->sprite == items[ROBE_WIZARD].index + || torso->sprite == items[ROBE_HEALER].index + || torso->sprite == items[ROBE_CULTIST].index + || torso->sprite == items[ROBE_MONK].index) + && !(entity->flags[INVISIBLE] && !entity->flags[INVISIBLE_DITHER]); + if ( multiplayer != CLIENT ) + { + if ( stats[PLAYER_NUM]->shoes == NULL || !showEquipment ) + { + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_RIGHTLEG, my->sprite); + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_RIGHT_OFFSET, shortSprite); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + if ( shortSprite && Entity::isBootSpriteShortArmor(entity) ) + { + entity->z += 0.75; + } + my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_RIGHTLEG); + break; + } + // left leg + case 3: + { + entity->focalx = limbs[playerRace][3][0]; + entity->focaly = limbs[playerRace][3][1]; + entity->focalz = limbs[playerRace][3][2]; + entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + bool shortSprite = (torso->sprite == items[ROBE_WIZARD].index + || torso->sprite == items[ROBE_HEALER].index + || torso->sprite == items[ROBE_CULTIST].index + || torso->sprite == items[ROBE_MONK].index) + && !(entity->flags[INVISIBLE] && !entity->flags[INVISIBLE_DITHER]); + if ( multiplayer != CLIENT ) + { + if ( stats[PLAYER_NUM]->shoes == NULL || !showEquipment ) + { + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_LEFTLEG, my->sprite); + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_LEFT_OFFSET, shortSprite); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + if ( shortSprite && Entity::isBootSpriteShortArmor(entity) ) + { + entity->z += 0.75; + } + my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_LEFTLEG); + break; + } + // right arm + case 4: + { + entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + if ( multiplayer != CLIENT ) + { + if ( stats[PLAYER_NUM]->gloves == NULL || !showEquipment ) + { + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_RIGHTARM, my->sprite); + } + else + { + if ( setGloveSprite(stats[PLAYER_NUM], entity, SPRITE_GLOVE_RIGHT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( (!PLAYER_ARMBENDED && showEquipment) + || ((PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 || PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2) && showEquipment) + || (insectoidLevitating && PLAYER_ATTACK == 0 && PLAYER_ATTACKTIME == 0) ) + { + entity->sprite += 2 * (stats[PLAYER_NUM]->weapon != NULL); + + if ( stats[PLAYER_NUM]->weapon == nullptr + && (insectoidLevitating || ((PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 || PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2) && showEquipment)) ) + { + entity->sprite += 2; + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + node_t* tempNode = list_Node(&my->children, 6); + if ( tempNode ) + { + Entity* weapon = (Entity*)tempNode->element; + + bool bendArm = PLAYER_ARMBENDED || playerRace == CREATURE_IMP; + if ( !((PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 || PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2) + && showEquipment) ) { - if ( stats[PLAYER_NUM]->shoes == NULL || !showEquipment ) + bendArm |= (weapon->flags[INVISIBLE] && !weapon->flags[INVISIBLE_DITHER]); + } + + if ( bendArm ) + { + if ( playerRace == INCUBUS || playerRace == SUCCUBUS ) + { + entity->focalx = limbs[playerRace][4][0] - 0.25; + entity->focaly = limbs[playerRace][4][1] - 0.25; + entity->focalz = limbs[playerRace][4][2]; + } + else if ( playerRace == DRYAD ) + { + entity->focalx = limbs[playerRace][4][0]; + entity->focaly = limbs[playerRace][4][1] - 0.25; + entity->focalz = limbs[playerRace][4][2]; + + if ( entity->sprite == 1966 + || entity->sprite == 1970 ) + { + entity->focaly += 0.25; + } + } + else if ( playerRace == MYCONID ) { - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_RIGHTLEG); + entity->focalx = limbs[playerRace][4][0]; + entity->focaly = limbs[playerRace][4][1]; + entity->focalz = limbs[playerRace][4][2]; + + if ( entity->sprite == 2000 ) + { + entity->focaly += 0.25; + } } else { - my->setBootSprite(entity, SPRITE_BOOT_RIGHT_OFFSET); + entity->focalx = limbs[playerRace][4][0]; // 0 + entity->focaly = limbs[playerRace][4][1]; // 0 + entity->focalz = limbs[playerRace][4][2]; // 1.5 } - if ( multiplayer == SERVER ) + } + else + { + if ( playerRace == INCUBUS || playerRace == SUCCUBUS ) { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + entity->focalx = limbs[playerRace][4][0]; + entity->focaly = limbs[playerRace][4][1]; + entity->focalz = limbs[playerRace][4][2]; + } + else if ( playerRace == AUTOMATON ) + { + entity->focalx = limbs[playerRace][4][0] + 1.5; // 1 + entity->focaly = limbs[playerRace][4][1] + 0.25; // 0 + entity->focalz = limbs[playerRace][4][2] - 1; // 1 + } + else if ( playerRace == MYCONID || playerRace == SALAMANDER ) + { + entity->focalx = limbs[playerRace][4][0] + 0.75; + entity->focaly = limbs[playerRace][4][1] + 0.25; + entity->focalz = limbs[playerRace][4][2] - 0.75; + } + else if ( playerRace == GREMLIN ) + { + entity->focalx = limbs[playerRace][4][0] + 1; + entity->focaly = limbs[playerRace][4][1] + 0.25; + entity->focalz = limbs[playerRace][4][2] - 0.75; + } + else if ( playerRace == GNOME ) + { + entity->focalx = limbs[playerRace][4][0] + 1; + entity->focaly = limbs[playerRace][4][1] + 0.25; + entity->focalz = limbs[playerRace][4][2] - 0.75; + } + else + { + entity->focalx = limbs[playerRace][4][0] + 0.75; + entity->focaly = limbs[playerRace][4][1]; + entity->focalz = limbs[playerRace][4][2] - 0.75; + } + } + } + my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_RIGHTARM); + entity->yaw += PLAYER_WEAPONYAW; + break; + } + // left arm + case 5: + { + entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + if ( multiplayer != CLIENT ) + { + if ( stats[PLAYER_NUM]->gloves == NULL || !showEquipment ) + { + entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_LEFTARM, my->sprite); + } + else + { + if ( setGloveSprite(stats[PLAYER_NUM], entity, SPRITE_GLOVE_LEFT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( showEquipment ) + { + bool bendArm = false; + if ( insectoidLevitating ) + { + bendArm = true; + } + if ( stats[PLAYER_NUM]->shield != NULL ) + { + if ( itemCategory(stats[PLAYER_NUM]->shield) == SPELLBOOK ) { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) - { - serverUpdateEntityBodypart(my, bodypart); - } + bendArm = false; + } + else + { + bendArm = true; + } + } + entity->sprite += 2 * (bendArm); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + if ( weaponarm && !showEquipment && + (PLAYER_ATTACK == MONSTER_POSE_SPECIAL_WINDUP1 + || PLAYER_ATTACK == MONSTER_POSE_SPECIAL_WINDUP2 + || PLAYER_ATTACK == PLAYER_POSE_GOLEM_SMASH) ) + { + // special swing - copy the right arm movements. + entity->pitch = weaponarm->pitch; + entity->roll = -weaponarm->roll; + } + else + { + entity->roll = 0.f; + } + + node_t* tempNode = list_Node(&my->children, 7); + if ( tempNode ) + { + Entity* shield = (Entity*)tempNode->element; + bool bendArm = true; + if ( shield->flags[INVISIBLE] && !shield->flags[INVISIBLE_DITHER] ) + { + bendArm = false; + if ( insectoidLevitating ) + { + bendArm = true; + } + } + else if ( shieldSpriteIsSpellbook(shield->sprite) ) + { + bendArm = false; + } + else if ( playerRace == CREATURE_IMP ) + { + bendArm = false; + } + + if ( !bendArm ) + { + if ( playerRace == INCUBUS || playerRace == SUCCUBUS ) + { + entity->focalx = limbs[playerRace][5][0] - 0.25; + entity->focaly = limbs[playerRace][5][1] + 0.25; + entity->focalz = limbs[playerRace][5][2]; + } + else if ( playerRace == DRYAD ) + { + entity->focalx = limbs[playerRace][5][0]; // 0 + entity->focaly = limbs[playerRace][5][1] + .25; // 0 + entity->focalz = limbs[playerRace][5][2]; // 2 + + if ( entity->sprite == 1965 + || entity->sprite == 1969 ) + { + entity->focaly -= 0.25; + } + } + else if ( playerRace == MYCONID ) + { + entity->focalx = limbs[playerRace][5][0]; + entity->focaly = limbs[playerRace][5][1]; + entity->focalz = limbs[playerRace][5][2]; + + if ( entity->sprite == 1999 ) + { + entity->focaly -= 0.25; } } + else + { + entity->focalx = limbs[playerRace][5][0]; // 0 + entity->focaly = limbs[playerRace][5][1]; // 0 + entity->focalz = limbs[playerRace][5][2]; // 1.5 + } } - my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_RIGHTLEG); - break; - // left leg - case 3: - entity->focalx = limbs[playerRace][3][0]; - entity->focaly = limbs[playerRace][3][1]; - entity->focalz = limbs[playerRace][3][2]; - entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; - if ( multiplayer != CLIENT ) + else { - if ( stats[PLAYER_NUM]->shoes == NULL || !showEquipment ) + if ( playerRace == INCUBUS || playerRace == SUCCUBUS ) + { + entity->focalx = limbs[playerRace][5][0]; + entity->focaly = limbs[playerRace][5][1]; + entity->focalz = limbs[playerRace][5][2]; + } + else if ( playerRace == AUTOMATON ) + { + entity->focalx = limbs[playerRace][5][0] + 1.5; // 1 + entity->focaly = limbs[playerRace][5][1] - 0.25; // 0 + entity->focalz = limbs[playerRace][5][2] - 1; // 1 + } + else if ( playerRace == MYCONID || playerRace == SALAMANDER ) + { + entity->focalx = limbs[playerRace][5][0] + 0.75; + entity->focaly = limbs[playerRace][5][1] - 0.25; + entity->focalz = limbs[playerRace][5][2] - 0.75; + } + else if ( playerRace == GREMLIN ) { - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_LEFTLEG); + entity->focalx = limbs[playerRace][5][0] + 1; + entity->focaly = limbs[playerRace][5][1] - 0.25; + entity->focalz = limbs[playerRace][5][2] - 0.75; + } + else if ( playerRace == GNOME ) + { + entity->focalx = limbs[playerRace][5][0] + 1; // 1 + entity->focaly = limbs[playerRace][5][1] - 0.25; // 0 + entity->focalz = limbs[playerRace][5][2] - 0.75; // 1 } else { - my->setBootSprite(entity, SPRITE_BOOT_LEFT_OFFSET); + entity->focalx = limbs[playerRace][5][0] + 0.75; + entity->focaly = limbs[playerRace][5][1]; + entity->focalz = limbs[playerRace][5][2] - 0.75; } - if ( multiplayer == SERVER ) + } + } + my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_LEFTARM); + if ( multiplayer != CLIENT ) + { + real_t prevYaw = PLAYER_SHIELDYAW; + if ( stats[PLAYER_NUM]->defending ) + { + if ( playerRace == CREATURE_IMP ) { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) - { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) - { - serverUpdateEntityBodypart(my, bodypart); - } - } + PLAYER_SHIELDYAW = PI / 25; + } + else + { + PLAYER_SHIELDYAW = PI / 5; } } - my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_LEFTLEG); - break; - // right arm - case 4: + else + { + PLAYER_SHIELDYAW = 0; + } + if ( prevYaw != PLAYER_SHIELDYAW || ticks % 200 == 0 ) + { + serverUpdateEntityFSkill(players[PLAYER_NUM]->entity, 8); + } + } + entity->yaw += PLAYER_SHIELDYAW; + break; + } + // weapon + case 6: + if ( multiplayer != CLIENT ) { - entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; - if ( multiplayer != CLIENT ) + entity->flags[INVISIBLE_DITHER] = false; + if ( swimming ) + { + entity->flags[INVISIBLE] = true; + } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2 ) { - if ( stats[PLAYER_NUM]->gloves == NULL || !showEquipment ) + entity->flags[INVISIBLE] = true; + } + else + { + if ( stats[PLAYER_NUM]->weapon == NULL ) { - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_RIGHTARM); + entity->flags[INVISIBLE] = true; + entity->sprite = 0; } else { - if ( setGloveSprite(stats[PLAYER_NUM], entity, SPRITE_GLOVE_RIGHT_OFFSET) != 0 ) + entity->sprite = itemModel(stats[PLAYER_NUM]->weapon); + if ( itemCategory(stats[PLAYER_NUM]->weapon) == SPELLBOOK ) { - // successfully set sprite for the human model + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; } } - if ( (!PLAYER_ARMBENDED && showEquipment) || (insectoidLevitating && PLAYER_ATTACK == 0 && PLAYER_ATTACKTIME == 0) ) + + if ( my->isInvisible() && stats[PLAYER_NUM]->weapon ) { - entity->sprite += 2 * (stats[PLAYER_NUM]->weapon != NULL); + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = true; + } + } - if ( stats[PLAYER_NUM]->weapon == nullptr - && insectoidLevitating ) + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) + { + entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) { - entity->sprite += 2; + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); } } - if ( multiplayer == SERVER ) + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + } + } + my->handleHumanoidWeaponLimb(entity, weaponarm); + + if ( entity->sprite >= items[STEEL_FLAIL].index && entity->sprite <= items[STEEL_FLAIL].index + 2 ) + { + // slight GENIUS flag fix for clipping into camera + entity->sizex = 5; + entity->sizey = 5; + } + else + { + entity->sizex = 4; + entity->sizey = 4; + } + break; + // shield + case 7: + if ( multiplayer != CLIENT ) + { + entity->flags[INVISIBLE_DITHER] = false; + if ( swimming ) + { + entity->flags[INVISIBLE] = true; + } + else + { + if ( stats[PLAYER_NUM]->shield == NULL ) { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + entity->flags[INVISIBLE] = true; + entity->sprite = 0; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(stats[PLAYER_NUM]->shield); + if ( itemTypeIsQuiver(stats[PLAYER_NUM]->shield->type) ) { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) + if ( itemTypeIsQuiver(stats[PLAYER_NUM]->shield->type) ) { - serverUpdateEntityBodypart(my, bodypart); + entity->handleQuiverThirdPersonModel(*stats[PLAYER_NUM], my->sprite); } } } + + if ( my->isInvisible() && stats[PLAYER_NUM]->shield ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = true; + } } - my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_RIGHTARM); - node_t* tempNode = list_Node(&my->children, 6); - if ( tempNode ) + if ( multiplayer == SERVER ) { - Entity* weapon = (Entity*)tempNode->element; - if ( (weapon->flags[INVISIBLE] && !weapon->flags[INVISIBLE_DITHER]) || PLAYER_ARMBENDED || playerRace == CREATURE_IMP ) + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) { - if ( playerRace == INCUBUS || playerRace == SUCCUBUS ) + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) { - entity->focalx = limbs[playerRace][4][0] - 0.25; - entity->focaly = limbs[playerRace][4][1] - 0.25; - entity->focalz = limbs[playerRace][4][2]; + entity->skill[10] = entity->sprite; + updateBodypart = true; } - else + if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) + { + entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + { + updateBodypart = true; + } + if ( updateBodypart ) { - entity->focalx = limbs[playerRace][4][0]; // 0 - entity->focaly = limbs[playerRace][4][1]; // 0 - entity->focalz = limbs[playerRace][4][2]; // 1.5 + serverUpdateEntityBodypart(my, bodypart); } } - else + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + } + } + my->handleHumanoidShieldLimb(entity, shieldarm); + break; + // cloak + case 8: + entity->focalx = limbs[playerRace][8][0]; + entity->focaly = limbs[playerRace][8][1]; + entity->focalz = limbs[playerRace][8][2]; + entity->scalex = 1.01; + entity->scaley = 1.01; + if ( multiplayer != CLIENT ) + { + entity->flags[INVISIBLE_DITHER] = false; + if ( stats[PLAYER_NUM]->cloak == NULL ) + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(stats[PLAYER_NUM]->cloak); + } + + if ( my->isInvisible() && stats[PLAYER_NUM]->cloak ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = true; + } + + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) { - if ( playerRace == INCUBUS || playerRace == SUCCUBUS ) + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) { - entity->focalx = limbs[playerRace][4][0]; - entity->focaly = limbs[playerRace][4][1]; - entity->focalz = limbs[playerRace][4][2]; + entity->skill[10] = entity->sprite; + updateBodypart = true; } - else if ( playerRace == AUTOMATON ) + if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) { - entity->focalx = limbs[playerRace][4][0] + 1.5; // 1 - entity->focaly = limbs[playerRace][4][1] + 0.25; // 0 - entity->focalz = limbs[playerRace][4][2] - 1; // 1 + entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); + updateBodypart = true; } - else + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) { - entity->focalx = limbs[playerRace][4][0] + 0.75; - entity->focaly = limbs[playerRace][4][1]; - entity->focalz = limbs[playerRace][4][2] - 0.75; + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); } } } - entity->yaw += PLAYER_WEAPONYAW; - break; } - // left arm - case 5: + else { - entity->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; - if ( multiplayer != CLIENT ) + if ( entity->sprite <= 0 ) { - if ( stats[PLAYER_NUM]->gloves == NULL || !showEquipment ) - { - entity->setDefaultPlayerModel(PLAYER_NUM, playerRace, LIMB_HUMANOID_LEFTARM); - } - else + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + } + } + + if ( entity->sprite == items[CLOAK_BACKPACK].index ) + { + // human + if ( playerRace == HUMAN || playerRace == VAMPIRE ) + { + entity->focaly = limbs[playerRace][8][1] + 0.25; + entity->focalz = limbs[playerRace][8][2] - 0.3; + } + else if ( playerRace == SUCCUBUS || playerRace == INCUBUS ) + { + // succubus/incubus + entity->focaly = limbs[playerRace][8][1] + 0.25; + entity->focalz = limbs[playerRace][8][2] - 0.7; + } + else if ( playerRace == SKELETON ) + { + entity->focaly = limbs[playerRace][8][1] + 0.25; + entity->focalz = limbs[playerRace][8][2] - 0.5; + } + else if ( playerRace == AUTOMATON ) + { + entity->focaly = limbs[playerRace][8][1] - 0.25; + entity->focalz = limbs[playerRace][8][2] - 0.5; + } + else if ( playerRace == GOATMAN || playerRace == INSECTOID || playerRace == GOBLIN ) + { + entity->focaly = limbs[playerRace][8][1] - 0.25; + entity->focalz = limbs[playerRace][8][2] - 0.5; + } + + entity->scalex = 0.99; + entity->scaley = 0.99; + } + entity->x -= cos(my->yaw); + entity->y -= sin(my->yaw); + entity->yaw += PI / 2; + my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_CLOAK); + break; + // helm + case 9: + { + helmet = entity; + entity->focalx = limbs[playerRace][9][0]; // 0 + entity->focaly = limbs[playerRace][9][1]; // 0 + entity->focalz = limbs[playerRace][9][2]; // -1.75 + entity->pitch = my->pitch; + entity->roll = 0; + static int lastHelmSprite[MAXPLAYERS] = { 0 }; + bool tryShake = false; + if ( entity->skill[1] == 1 ) + { + tryShake = true; + entity->skill[1] = 0; + } + if ( multiplayer != CLIENT ) + { + entity->sprite = itemModel(stats[PLAYER_NUM]->helmet); + entity->flags[INVISIBLE_DITHER] = false; + if ( stats[PLAYER_NUM]->helmet == NULL ) + { + if ( playerRace == MYCONID && stats[PLAYER_NUM]->getEffectActive(EFF_GROWTH) >= 2 ) { - if ( setGloveSprite(stats[PLAYER_NUM], entity, SPRITE_GLOVE_LEFT_OFFSET) != 0 ) + entity->sprite = 2003 + std::min(2, (stats[PLAYER_NUM]->getEffectActive(EFF_GROWTH) - 2)); + entity->flags[INVISIBLE] = false; + if ( my->isInvisible() ) { - // successfully set sprite for the human model + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = true; } } - if ( showEquipment ) + else if ( playerRace == DRYAD && stats[PLAYER_NUM]->getEffectActive(EFF_GROWTH) >= 2 ) { - bool bendArm = false; - if ( insectoidLevitating ) + if ( my->sprite == 1964 || my->sprite == 1993 ) { - bendArm = true; + entity->sprite = 1974 + std::min(2, (stats[PLAYER_NUM]->getEffectActive(EFF_GROWTH) - 2)) * 2; + } + else + { + entity->sprite = 1973 + std::min(2, (stats[PLAYER_NUM]->getEffectActive(EFF_GROWTH) - 2)) * 2; } - if ( stats[PLAYER_NUM]->shield != NULL ) + entity->flags[INVISIBLE] = false; + if ( my->isInvisible() ) { - if ( itemCategory(stats[PLAYER_NUM]->shield) == SPELLBOOK ) - { - bendArm = false; - } - else - { - bendArm = true; - } + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = true; } - entity->sprite += 2 * (bendArm); } - if ( multiplayer == SERVER ) + else { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + entity->flags[INVISIBLE] = true; + } + } + else + { + entity->flags[INVISIBLE] = false; + } + + if ( my->isInvisible() && stats[PLAYER_NUM]->helmet ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = true; + } + + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) - { - serverUpdateEntityBodypart(my, bodypart); - } + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) + { + entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); } } } - my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_LEFTARM); + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + } + } + + if ( entity->sprite >= 1973 && entity->sprite <= 1978 ) + { + entity->focalx = limbs[DRYAD][15][0]; + entity->focaly = limbs[DRYAD][15][1]; + entity->focalz = limbs[DRYAD][15][2]; + entity->x += limbs[DRYAD][16][0] * cos(my->yaw + PI / 2) + limbs[DRYAD][16][1] * cos(my->yaw); + entity->y += limbs[DRYAD][16][0] * sin(my->yaw + PI / 2) + limbs[DRYAD][16][1] * sin(my->yaw); + entity->z += limbs[DRYAD][16][2]; + + if ( additionalLimb ) + { + entity->scalez = 1.008 + 0.05 * sin(additionalLimb->fskill[0]); + entity->pitch -= 0.05 * sin(std::max(0.0, additionalLimb->fskill[0]) / 10); + entity->z -= 0.05 * sin(additionalLimb->fskill[0]); + } + + entity->pitch += (entity->fskill[2]) * (PI / 64) * sin(entity->fskill[1]); + entity->roll = (entity->fskill[2]) * (PI / 64) * sin(entity->fskill[1] + PI / 2); - if ( weaponarm && !showEquipment && - (PLAYER_ATTACK == MONSTER_POSE_SPECIAL_WINDUP1 - || PLAYER_ATTACK == MONSTER_POSE_SPECIAL_WINDUP2 - || PLAYER_ATTACK == PLAYER_POSE_GOLEM_SMASH)) + real_t bobScale = 0.08 * entity->skill[0]; + entity->scalex = 1.01 + bobScale * sin(entity->fskill[3]); + entity->scaley = 1.01 + bobScale * sin(entity->fskill[3]); + if ( additionalLimb ) { - // special swing - copy the right arm movements. - entity->pitch = weaponarm->pitch; - entity->roll = -weaponarm->roll; + entity->scalez = 1.008 + 0.05 * sin(additionalLimb->fskill[0]) - bobScale * sin(entity->fskill[3]); } else { - entity->roll = 0.f; + entity->scalez = 1.01 - bobScale * sin(entity->fskill[3]); } - node_t* tempNode = list_Node(&my->children, 7); - if ( tempNode ) + if ( lastHelmSprite[PLAYER_NUM] != entity->sprite || tryShake ) { - Entity* shield = (Entity*)tempNode->element; - bool bendArm = true; - if ( shield->flags[INVISIBLE] && !shield->flags[INVISIBLE_DITHER] ) - { - bendArm = false; - if ( insectoidLevitating ) - { - bendArm = true; - } - } - else if ( shield->sprite >= items[SPELLBOOK_LIGHT].index - && shield->sprite < (items[SPELLBOOK_LIGHT].index + items[SPELLBOOK_LIGHT].variations) ) - { - bendArm = false; - } - else if ( playerRace == CREATURE_IMP ) + if ( !intro ) { - bendArm = false; + playSoundEntityLocal(my, 754 + local_rng.rand() % 2, 156); } - - if ( !bendArm ) + + entity->skill[0] = 3; + + entity->fskill[3] = 0.0; + entity->fskill[1] = 2 * PI * 0.0; + entity->fskill[2] = 0.0; + } + ignoreHelmetOffsetFromMask = true; + + if ( entity->skill[0] > 0 ) + { + entity->fskill[2] = std::min(1.0, std::max(0.05, entity->fskill[2] * 1.15)); + if ( entity->skill[0] == 4 ) // circle first { - if ( playerRace == INCUBUS || playerRace == SUCCUBUS ) + if ( entity->fskill[1] >= 2 * PI ) { - entity->focalx = limbs[playerRace][5][0] - 0.25; - entity->focaly = limbs[playerRace][5][1] + 0.25; - entity->focalz = limbs[playerRace][5][2]; - } - else - { - entity->focalx = limbs[playerRace][5][0]; // 0 - entity->focaly = limbs[playerRace][5][1]; // 0 - entity->focalz = limbs[playerRace][5][2]; // 1.5 + entity->skill[0]--; } } else { - if ( playerRace == INCUBUS || playerRace == SUCCUBUS ) - { - entity->focalx = limbs[playerRace][5][0]; - entity->focaly = limbs[playerRace][5][1]; - entity->focalz = limbs[playerRace][5][2]; - } - else if ( playerRace == AUTOMATON ) - { - entity->focalx = limbs[playerRace][5][0] + 1.5; // 1 - entity->focaly = limbs[playerRace][5][1] - 0.25; // 0 - entity->focalz = limbs[playerRace][5][2] - 1; // 1 - } - else + entity->fskill[3] += 0.25; + if ( entity->fskill[3] >= 2 * PI ) { - entity->focalx = limbs[playerRace][5][0] + 0.75; - entity->focaly = limbs[playerRace][5][1]; - entity->focalz = limbs[playerRace][5][2] - 0.75; + entity->fskill[3] -= 2 * PI; + entity->skill[0]--; } } } - if ( multiplayer != CLIENT ) + else { - real_t prevYaw = PLAYER_SHIELDYAW; - if ( stats[PLAYER_NUM]->defending ) - { - PLAYER_SHIELDYAW = PI / 5; - } - else - { - PLAYER_SHIELDYAW = 0; - } - if ( prevYaw != PLAYER_SHIELDYAW || ticks % 200 == 0 ) - { - serverUpdateEntityFSkill(players[PLAYER_NUM]->entity, 8); - } + entity->fskill[2] *= 0.95; } - entity->yaw += PLAYER_SHIELDYAW; - break; + entity->fskill[1] += 0.125; } - // weapon - case 6: - if ( multiplayer != CLIENT ) + else if ( entity->sprite == 2003 || entity->sprite == 2004 || entity->sprite == 2005 ) + { + entity->focalx = limbs[MYCONID][15][0]; + entity->focaly = limbs[MYCONID][15][1]; + entity->focalz = limbs[MYCONID][15][2]; + entity->x += limbs[MYCONID][16][0] * cos(my->yaw + PI / 2) + limbs[MYCONID][16][1] * cos(my->yaw); + entity->y += limbs[MYCONID][16][0] * sin(my->yaw + PI / 2) + limbs[MYCONID][16][1] * sin(my->yaw); + entity->z += limbs[MYCONID][16][2] - 0.01; + + if ( entity->sprite == 2004 ) { - entity->flags[INVISIBLE_DITHER] = false; - if ( swimming ) - { - entity->flags[INVISIBLE] = true; - } - else + entity->focalz -= .25; + } + + entity->pitch += (entity->fskill[2]) * (PI / 64) * sin(entity->fskill[1]); + entity->roll = (entity->fskill[2]) * (PI / 64) * sin(entity->fskill[1] + PI / 2); + + real_t bobScale = 0.08 * entity->skill[0]; + entity->scalex = 1.01 + bobScale * sin(entity->fskill[3]); + entity->scaley = 1.01 + bobScale * sin(entity->fskill[3]); + entity->scalez = 1.01 - bobScale * sin(entity->fskill[3]); + + if ( lastHelmSprite[PLAYER_NUM] != entity->sprite || tryShake ) + { + if ( !intro ) { - if ( stats[PLAYER_NUM]->weapon == NULL ) + if ( entity->sprite == 2003 ) { - entity->flags[INVISIBLE] = true; - entity->sprite = 0; + playSoundEntityLocal(my, 714, 64); } - else + else if ( entity->sprite == 2004 ) { - entity->sprite = itemModel(stats[PLAYER_NUM]->weapon); - if ( itemCategory(stats[PLAYER_NUM]->weapon) == SPELLBOOK ) - { - entity->flags[INVISIBLE] = true; - } - else - { - entity->flags[INVISIBLE] = false; - } + playSoundEntityLocal(my, 715, 64); } - - if ( my->isInvisible() && stats[PLAYER_NUM]->weapon ) + else if ( entity->sprite == 2005 ) { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = true; + playSoundEntityLocal(my, 716, 64); } } - if ( multiplayer == SERVER ) + entity->skill[0] = 3; + + entity->fskill[3] = 0.0; + entity->fskill[1] = 2 * PI * 0.0; + entity->fskill[2] = 0.0; + } + ignoreHelmetOffsetFromMask = true; + + if ( entity->skill[0] > 0 ) + { + entity->fskill[2] = std::min(1.0, std::max(0.05, entity->fskill[2] * 1.15)); + if ( entity->skill[0] == 4 ) // circle first { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + if ( entity->fskill[1] >= 2 * PI ) { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) - { - entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) - { - serverUpdateEntityBodypart(my, bodypart); - } + entity->skill[0]--; + } + } + else + { + entity->fskill[3] += 0.25; + if ( entity->fskill[3] >= 2 * PI ) + { + entity->fskill[3] -= 2 * PI; + entity->skill[0]--; } } } else { - if ( entity->sprite <= 0 ) - { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = false; - } + entity->fskill[2] *= 0.95; } - my->handleHumanoidWeaponLimb(entity, weaponarm); - break; - // shield - case 7: - if ( multiplayer != CLIENT ) + entity->fskill[1] += 0.125; + } + else + { + entity->skill[0] = 0; + entity->fskill[1] = 0.0; + entity->fskill[2] = 0.0; + my->setHelmetLimbOffset(entity); + } + lastHelmSprite[PLAYER_NUM] = entity->sprite; + break; + } + // mask + case 10: + { + entity->focalx = limbs[playerRace][10][0]; // 0 + entity->focaly = limbs[playerRace][10][1]; // 0 + entity->focalz = limbs[playerRace][10][2]; // .5 + entity->pitch = my->pitch; + entity->roll = PI / 2; + if ( multiplayer != CLIENT ) + { + entity->flags[INVISIBLE_DITHER] = false; + if ( stats[PLAYER_NUM]->mask == NULL ) { - entity->flags[INVISIBLE_DITHER] = false; - if ( swimming ) + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + + if ( my->isInvisible() && stats[PLAYER_NUM]->mask ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = true; + } + + if ( stats[PLAYER_NUM]->mask != NULL ) + { + if ( stats[PLAYER_NUM]->mask->type == TOOL_GLASSES ) { - entity->flags[INVISIBLE] = true; + entity->sprite = 165; // GlassesWorn.vox + } + else if ( stats[PLAYER_NUM]->mask->type == MONOCLE ) + { + entity->sprite = 1196; // MonocleWorn.vox } else { - if ( stats[PLAYER_NUM]->shield == NULL ) + entity->sprite = itemModel(stats[PLAYER_NUM]->mask); + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) { - entity->flags[INVISIBLE] = true; - entity->sprite = 0; + entity->skill[10] = entity->sprite; + updateBodypart = true; } - else + if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) { - entity->flags[INVISIBLE] = false; - entity->sprite = itemModel(stats[PLAYER_NUM]->shield); - if ( itemTypeIsQuiver(stats[PLAYER_NUM]->shield->type) ) - { - if ( itemTypeIsQuiver(stats[PLAYER_NUM]->shield->type) ) - { - entity->handleQuiverThirdPersonModel(*stats[PLAYER_NUM]); - } - } + entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); + updateBodypart = true; } - - if ( my->isInvisible() && stats[PLAYER_NUM]->shield ) + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = true; + updateBodypart = true; } - } - if ( multiplayer == SERVER ) - { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + if ( updateBodypart ) { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) - { - entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) - { - serverUpdateEntityBodypart(my, bodypart); - } + serverUpdateEntityBodypart(my, bodypart); } } } - else - { - if ( entity->sprite <= 0 ) - { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = false; - } - } - my->handleHumanoidShieldLimb(entity, shieldarm); - break; - // cloak - case 8: - entity->focalx = limbs[playerRace][8][0]; - entity->focaly = limbs[playerRace][8][1]; - entity->focalz = limbs[playerRace][8][2]; - entity->scalex = 1.01; - entity->scaley = 1.01; - if ( multiplayer != CLIENT ) + } + else + { + if ( entity->sprite <= 0 ) { + entity->flags[INVISIBLE] = true; entity->flags[INVISIBLE_DITHER] = false; - if ( stats[PLAYER_NUM]->cloak == NULL ) - { - entity->flags[INVISIBLE] = true; - } - else - { - entity->flags[INVISIBLE] = false; - entity->sprite = itemModel(stats[PLAYER_NUM]->cloak); - } + } + } + entity->scalex = 0.99; + entity->scaley = 0.99; + entity->scalez = 0.99; + + real_t helmscalex = 0.0; + real_t helmscaley = 0.0; + real_t helmscalez = 0.0; + if ( ignoreHelmetOffsetFromMask && helmet ) + { + helmscalex = helmet->scalex; + helmscaley = helmet->scaley; + helmscalez = helmet->scalez; + } - if ( my->isInvisible() && stats[PLAYER_NUM]->cloak ) + if ( EquipmentModelOffsets.modelOffsetExists(playerRace, entity->sprite, my->sprite) ) + { + my->setHelmetLimbOffset(entity); + my->setHelmetLimbOffsetWithMask(helmet, entity); + } + else if ( entity->sprite == items[MASK_SHAMAN].index ) + { + entity->roll = 0; + my->setHelmetLimbOffset(entity); + my->setHelmetLimbOffsetWithMask(helmet, entity); + } + else if ( entity->sprite == 165 || entity->sprite == 1196 ) + { + entity->focalx = limbs[playerRace][10][0] + .25; // .25 + entity->focaly = limbs[playerRace][10][1] - 2.5; // -2.25 + entity->focalz = limbs[playerRace][10][2]; // .5 + if ( entity->sprite == 1196 ) // MonocleWorn.vox + { + switch ( playerRace ) { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = true; + case INCUBUS: + entity->focalx -= .4; + break; + case SUCCUBUS: + entity->focalx -= .25; + break; + case GOBLIN: + entity->focalx -= .5; + entity->focalz -= .05; + break; + default: + break; } - - if ( multiplayer == SERVER ) + } + if ( helmet && !helmet->flags[INVISIBLE] && + (helmet->sprite == items[PUNISHER_HOOD].index + || helmet->sprite == 201 // hood purple + || helmet->sprite == items[HAT_HOOD_APPRENTICE].index) ) + { + switch ( playerRace ) { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) - { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) - { - entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) - { - serverUpdateEntityBodypart(my, bodypart); - } - } + case HUMAN: + case VAMPIRE: + case SHOPKEEPER: + case INSECTOID: + entity->focaly += 0.25; // lower glasses a bit. + break; + case INCUBUS: + case SUCCUBUS: + case AUTOMATON: + case GOBLIN: + case GOATMAN: + case SKELETON: + // no change. + break; + default: + break; } } - else + + if ( entity->sprite == 165 ) { - if ( entity->sprite <= 0 ) + switch ( playerRace ) { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = false; + case HUMAN: + case VAMPIRE: + entity->focalx += 0.15; + break; + default: + break; } } + } + else if ( playerRace == INCUBUS + && (entity->sprite == items[TOOL_BLINDFOLD].index + || entity->sprite == items[TOOL_BLINDFOLD_FOCUS].index + || entity->sprite == items[TOOL_BLINDFOLD_TELEPATHY].index) ) + { + entity->focalx = limbs[playerRace][10][0] + .35; // .35 + entity->focaly = limbs[playerRace][10][1] - 2.5; // -2 + entity->focalz = limbs[playerRace][10][2]; // .5 + } + else + { + entity->focalx = limbs[playerRace][10][0] + .35; // .35 + entity->focaly = limbs[playerRace][10][1] - 2; // -2 + entity->focalz = limbs[playerRace][10][2]; // .5 + } + + if ( ignoreHelmetOffsetFromMask && helmet ) + { + helmet->scalex = helmscalex; + helmet->scaley = helmscaley; + helmet->scalez = helmscalez; + } + break; + } + case 11: + additionalLimb = entity; + entity->focalx = limbs[playerRace][11][0]; + entity->focaly = limbs[playerRace][11][1]; + entity->focalz = limbs[playerRace][11][2]; + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; - if ( entity->sprite == items[CLOAK_BACKPACK].index ) + if ( playerRace == SALAMANDER ) { - // human - if ( playerRace == HUMAN || playerRace == VAMPIRE ) - { - entity->focaly = limbs[playerRace][8][1] + 0.25; - entity->focalz = limbs[playerRace][8][2] - 0.3; - } - else if ( playerRace == SUCCUBUS || playerRace == INCUBUS ) - { - // succubus/incubus - entity->focaly = limbs[playerRace][8][1] + 0.25; - entity->focalz = limbs[playerRace][8][2] - 0.7; - } - else if ( playerRace == SKELETON ) - { - entity->focaly = limbs[playerRace][8][1] + 0.25; - entity->focalz = limbs[playerRace][8][2] - 0.5; - } - else if ( playerRace == AUTOMATON ) + entity->focalx = limbs[SALAMANDER][11][0]; + entity->focaly = limbs[SALAMANDER][11][1]; + entity->focalz = limbs[SALAMANDER][11][2]; + entity->x += limbs[SALAMANDER][12][0] * cos(my->yaw + PI / 2) + limbs[SALAMANDER][12][1] * cos(my->yaw); + entity->y += limbs[SALAMANDER][12][0] * sin(my->yaw + PI / 2) + limbs[SALAMANDER][12][1] * sin(my->yaw); + entity->z += limbs[SALAMANDER][12][2]; + entity->pitch = 0.15; + + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + + entity->sprite = 2041; + switch ( my->sprite ) { - entity->focaly = limbs[playerRace][8][1] - 0.25; - entity->focalz = limbs[playerRace][8][2] - 0.5; + case 2014: + entity->sprite = 2041; + break; + case 2015: + entity->sprite = 2042; + break; + case 2016: + entity->sprite = 2043; + break; + case 2017: + entity->sprite = 2044; + break; + case 2018: + entity->sprite = 2045; + break; + case 2019: + entity->sprite = 2046; + break; + default: + break; } - else if ( playerRace == GOATMAN || playerRace == INSECTOID || playerRace == GOBLIN ) + + if ( entity->sprite == 2042 + || entity->sprite == 2044 + || entity->sprite == 2046 ) { - entity->focaly = limbs[playerRace][8][1] - 0.25; - entity->focalz = limbs[playerRace][8][2] - 0.5; + entity->focalx += 0.5; + entity->focalz -= 0.25; } - entity->scalex = 0.99; - entity->scaley = 0.99; - } - entity->x -= cos(my->yaw); - entity->y -= sin(my->yaw); - entity->yaw += PI / 2; - break; - // helm - case 9: - helmet = entity; - entity->focalx = limbs[playerRace][9][0]; // 0 - entity->focaly = limbs[playerRace][9][1]; // 0 - entity->focalz = limbs[playerRace][9][2]; // -1.75 - entity->pitch = my->pitch; - entity->roll = 0; - if ( multiplayer != CLIENT ) - { - entity->sprite = itemModel(stats[PLAYER_NUM]->helmet); - entity->flags[INVISIBLE_DITHER] = false; - if ( stats[PLAYER_NUM]->helmet == NULL ) + bool moving = false; + if ( fabs(PLAYER_VELX) > 0.1 || fabs(PLAYER_VELY) > 0.1 || insectoidLevitating ) { - entity->flags[INVISIBLE] = true; + moving = true; } - else + if ( entity->skill[0] == 0 ) { - entity->flags[INVISIBLE] = false; - } + if ( moving ) + { + entity->fskill[0] += std::min(dist * PLAYERWALKSPEED, 2.f * PLAYERWALKSPEED); // move proportional to move speed + } + else if ( PLAYER_ATTACK != 0 ) + { + entity->fskill[0] += PLAYERWALKSPEED; // move fixed speed when attacking if stationary + } + else + { + entity->fskill[0] += 0.01; // otherwise move slow idle + } - if ( my->isInvisible() && stats[PLAYER_NUM]->helmet ) - { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = true; + if ( entity->fskill[0] > PI / 3 || ((!moving || PLAYER_ATTACK != 0) && entity->fskill[0] > PI / 5) ) + { + // switch direction if angle too great, angle is shorter if attacking or stationary + entity->skill[0] = 1; + } } - - if ( multiplayer == SERVER ) + else // reverse of the above { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + if ( moving ) { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) - { - entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + if ( insectoidLevitating ) { - updateBodypart = true; + entity->fskill[0] -= std::min(std::max(0.15, dist * PLAYERWALKSPEED), 2.f * PLAYERWALKSPEED); } - if ( updateBodypart ) + else { - serverUpdateEntityBodypart(my, bodypart); + entity->fskill[0] -= std::min(dist * PLAYERWALKSPEED, 2.f * PLAYERWALKSPEED); } } + else if ( PLAYER_ATTACK != 0 ) + { + entity->fskill[0] -= PLAYERWALKSPEED; + } + else + { + entity->fskill[0] -= 0.007; + } + + if ( entity->fskill[0] < 0.0 ) + { + entity->skill[0] = 0; + entity->skill[1] = entity->skill[1] != 0 ? 0 : 1; + } } + //entity->yaw += -entity->fskill[0]; + real_t dir = entity->skill[1] == 0 ? 1 : -1; + entity->pitch += 0.5 * sin(entity->fskill[0]); + entity->roll = dir * -0.25 * sin(entity->fskill[0]); } - else - { - if ( entity->sprite <= 0 ) - { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = false; - } - } - my->setHelmetLimbOffset(entity); - break; - // mask - case 10: - entity->focalx = limbs[playerRace][10][0]; // 0 - entity->focaly = limbs[playerRace][10][1]; // 0 - entity->focalz = limbs[playerRace][10][2]; // .5 - entity->pitch = my->pitch; - entity->roll = PI / 2; - if ( multiplayer != CLIENT ) + if ( playerRace == DRYAD ) { - entity->flags[INVISIBLE_DITHER] = false; - if ( stats[PLAYER_NUM]->mask == NULL ) - { - entity->flags[INVISIBLE] = true; - } - else - { - entity->flags[INVISIBLE] = false; - } + entity->focalx = limbs[playerRace][11][0]; + entity->focaly = limbs[playerRace][11][1]; + entity->focalz = limbs[playerRace][11][2]; + entity->x += limbs[playerRace][12][0] * cos(my->yaw + PI / 2) + limbs[playerRace][12][1] * cos(my->yaw); + entity->y += limbs[playerRace][12][0] * sin(my->yaw + PI / 2) + limbs[playerRace][12][1] * sin(my->yaw); + entity->z += limbs[playerRace][12][2]; + + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; - if ( my->isInvisible() && stats[PLAYER_NUM]->mask ) + entity->sprite = (my->sprite == 1963 || my->sprite == 1992) ? 1979 : 1980; + + bool moving = false; + if ( fabs(PLAYER_VELX) > 0.1 || fabs(PLAYER_VELY) > 0.1 ) { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = true; + moving = true; } - if ( stats[PLAYER_NUM]->mask != NULL ) + if ( entity->skill[0] == 0 ) { - if ( stats[PLAYER_NUM]->mask->type == TOOL_GLASSES ) + if ( moving ) { - entity->sprite = 165; // GlassesWorn.vox + entity->fskill[0] += std::min(dist * PLAYERWALKSPEED, 0.35 * PLAYERWALKSPEED); // move proportional to move speed } - else if ( stats[PLAYER_NUM]->mask->type == MONOCLE ) + else if ( PLAYER_ATTACK != 0 ) { - entity->sprite = 1196; // MonocleWorn.vox + entity->fskill[0] += 0.5 * PLAYERWALKSPEED; // move fixed speed when attacking if stationary } else { - entity->sprite = itemModel(stats[PLAYER_NUM]->mask); + entity->fskill[0] += 0.01; // otherwise move slow idle + } + + if ( entity->fskill[0] > PI / 3 || ((!moving || PLAYER_ATTACK != 0) && entity->fskill[0] > PI / 5) ) + { + // switch direction if angle too great, angle is shorter if attacking or stationary + entity->skill[0] = 1; } } - if ( multiplayer == SERVER ) + else // reverse of the above { - // update sprites for clients - if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + if ( moving ) { - bool updateBodypart = false; - if ( entity->skill[10] != entity->sprite ) - { - entity->skill[10] = entity->sprite; - updateBodypart = true; - } - if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) - { - entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); - updateBodypart = true; - } - if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) - { - updateBodypart = true; - } - if ( updateBodypart ) - { - serverUpdateEntityBodypart(my, bodypart); - } + entity->fskill[0] -= std::min(dist * PLAYERWALKSPEED, 0.35 * PLAYERWALKSPEED); + } + else if ( PLAYER_ATTACK != 0 ) + { + entity->fskill[0] -= PLAYERWALKSPEED; + } + else + { + entity->fskill[0] -= 0.007; + } + + if ( entity->fskill[0] < -PI / 32 ) + { + entity->skill[0] = 0; } } + //entity->yaw += -entity->fskill[0] / 4; + entity->pitch = 0.0; + entity->pitch -= entity->fskill[0] / 8; + entity->z -= 0.25 * sin(entity->fskill[0] * 5); + entity->roll = -entity->fskill[0] / 8; + entity->scalez = 1.00 + 0.1 * sin(entity->fskill[0]); } - else + else if ( playerRace == MYCONID ) { - if ( entity->sprite <= 0 ) + entity->x += limbs[playerRace][12][0] * cos(my->yaw + PI / 2) + limbs[playerRace][12][1] * cos(my->yaw); + entity->y += limbs[playerRace][12][0] * sin(my->yaw + PI / 2) + limbs[playerRace][12][1] * sin(my->yaw); + entity->z += limbs[playerRace][12][2]; + + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + + entity->sprite = 2006; + + bool moving = false; + if ( fabs(PLAYER_VELX) > 0.1 || fabs(PLAYER_VELY) > 0.1 ) { - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = false; + moving = true; } - } - entity->scalex = 0.99; - entity->scaley = 0.99; - entity->scalez = 0.99; - if ( entity->sprite == 165 || entity->sprite == 1196 ) - { - entity->focalx = limbs[playerRace][10][0] + .25; // .25 - entity->focaly = limbs[playerRace][10][1] - 2.5; // -2.25 - entity->focalz = limbs[playerRace][10][2]; // .5 - if ( entity->sprite == 1196 ) // MonocleWorn.vox + + if ( entity->skill[0] == 0 ) { - switch ( playerRace ) + //if ( moving ) + //{ + // entity->fskill[0] += std::min(dist * PLAYERWALKSPEED, 2.f * PLAYERWALKSPEED); // move proportional to move speed + //} + //else if ( PLAYER_ATTACK != 0 ) + //{ + // entity->fskill[0] += PLAYERWALKSPEED; // move fixed speed when attacking if stationary + //} + //else { - case INCUBUS: - entity->focalx -= .4; - break; - case SUCCUBUS: - entity->focalx -= .25; - break; - case GOBLIN: - entity->focalx -= .5; - entity->focalz -= .05; - break; - default: - break; + entity->fskill[0] += 0.01; // otherwise move slow idle + } + + if ( entity->fskill[0] > PI / 3 || ((!moving || PLAYER_ATTACK != 0) && entity->fskill[0] > PI / 5) ) + { + // switch direction if angle too great, angle is shorter if attacking or stationary + entity->skill[0] = 1; } } - if ( helmet && !helmet->flags[INVISIBLE] && helmet->sprite == items[PUNISHER_HOOD].index ) + else // reverse of the above { - switch ( playerRace ) + /*if ( moving ) { - case HUMAN: - case VAMPIRE: - case SHOPKEEPER: - case INSECTOID: - entity->focaly += 0.25; // lower glasses a bit. - break; - case INCUBUS: - case SUCCUBUS: - case AUTOMATON: - case GOBLIN: - case GOATMAN: - case SKELETON: - // no change. - break; - default: - break; + entity->fskill[0] -= std::min(dist * PLAYERWALKSPEED, 2.f * PLAYERWALKSPEED); + } + else if ( PLAYER_ATTACK != 0 ) + { + entity->fskill[0] -= PLAYERWALKSPEED; + } + else*/ + { + entity->fskill[0] -= 0.007; + } + + if ( entity->fskill[0] < -PI / 32 ) + { + entity->skill[0] = 0; } } + entity->yaw += -entity->fskill[0] / 4; + //entity->pitch += entity->fskill[0] / 4; + entity->roll = -entity->fskill[0]; } - else if ( entity->sprite == items[MASK_SHAMAN].index ) - { - entity->roll = 0; - my->setHelmetLimbOffset(entity); - my->setHelmetLimbOffsetWithMask(helmet, entity); - } - else if ( EquipmentModelOffsets.modelOffsetExists(playerRace, entity->sprite) ) - { - my->setHelmetLimbOffset(entity); - my->setHelmetLimbOffsetWithMask(helmet, entity); - } - else if ( playerRace == INCUBUS - && (entity->sprite == items[TOOL_BLINDFOLD].index - || entity->sprite == items[TOOL_BLINDFOLD_FOCUS].index - || entity->sprite == items[TOOL_BLINDFOLD_TELEPATHY].index) ) - { - entity->focalx = limbs[playerRace][10][0] + .35; // .35 - entity->focaly = limbs[playerRace][10][1] - 2.5; // -2 - entity->focalz = limbs[playerRace][10][2]; // .5 - } - else - { - entity->focalx = limbs[playerRace][10][0] + .35; // .35 - entity->focaly = limbs[playerRace][10][1] - 2; // -2 - entity->focalz = limbs[playerRace][10][2]; // .5 - } - - break; - case 11: - additionalLimb = entity; - entity->focalx = limbs[playerRace][11][0]; - entity->focaly = limbs[playerRace][11][1]; - entity->focalz = limbs[playerRace][11][2]; - entity->flags[INVISIBLE] = true; - entity->flags[INVISIBLE_DITHER] = false; - if ( playerRace == INSECTOID || playerRace == CREATURE_IMP ) + else if ( playerRace == INSECTOID || playerRace == CREATURE_IMP ) { entity->flags[INVISIBLE] = my->flags[INVISIBLE]; entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; @@ -10048,6 +14307,8 @@ void actPlayer(Entity* my) } } entity->yaw += entity->fskill[0]; + entity->pitch = 0.0; + entity->roll = 0.0; } break; case 12: @@ -10056,7 +14317,53 @@ void actPlayer(Entity* my) entity->focalz = limbs[playerRace][12][2]; entity->flags[INVISIBLE] = true; entity->flags[INVISIBLE_DITHER] = false; - if ( playerRace == INSECTOID || playerRace == CREATURE_IMP ) + + if ( playerRace == DRYAD ) + { + entity->focalx = limbs[playerRace][13][0]; + entity->focaly = limbs[playerRace][13][1]; + entity->focalz = limbs[playerRace][13][2]; + entity->x += limbs[playerRace][14][0] * cos(my->yaw + PI / 2) + limbs[playerRace][14][1] * cos(my->yaw); + entity->y += limbs[playerRace][14][0] * sin(my->yaw + PI / 2) + limbs[playerRace][14][1] * sin(my->yaw); + entity->z += limbs[playerRace][14][2]; + + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + + entity->sprite = (my->sprite == 1963 || my->sprite == 1992) ? 1981 : 1982; + + if ( additionalLimb ) // follow the yaw of the previous limb. + { + //entity->yaw -= -additionalLimb->fskill[0] / 4; + entity->pitch = 0.0; + entity->pitch -= additionalLimb->fskill[0] / 8; + entity->z -= 0.25 * sin(additionalLimb->fskill[0] * 5); + entity->roll = additionalLimb->fskill[0] / 8; + entity->scalez = 1.00 + 0.1 * sin(additionalLimb->fskill[0]); + } + } + else if ( playerRace == MYCONID ) + { + entity->focalx = limbs[playerRace][13][0]; + entity->focaly = limbs[playerRace][13][1]; + entity->focalz = limbs[playerRace][13][2]; + entity->x += limbs[playerRace][14][0] * cos(my->yaw + PI / 2) + limbs[playerRace][14][1] * cos(my->yaw); + entity->y += limbs[playerRace][14][0] * sin(my->yaw + PI / 2) + limbs[playerRace][14][1] * sin(my->yaw); + entity->z += limbs[playerRace][14][2]; + + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + + entity->sprite = 2007; + + if ( additionalLimb ) // follow the yaw of the previous limb. + { + entity->yaw -= -additionalLimb->fskill[0] / 4; + //entity->pitch += additionalLimb->fskill[0] / 16; + entity->roll = additionalLimb->fskill[0]; + } + } + else if ( playerRace == INSECTOID || playerRace == CREATURE_IMP ) { entity->flags[INVISIBLE] = my->flags[INVISIBLE]; entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; @@ -10096,8 +14403,198 @@ void actPlayer(Entity* my) { entity->yaw -= additionalLimb->fskill[0]; } + entity->pitch = 0.0; + entity->roll = 0.0; } break; + case 13: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[playerRace][1][0]; + entity->focaly = limbs[playerRace][1][1]; + entity->focalz = limbs[playerRace][1][2]; + if ( multiplayer != CLIENT ) + { + entity->flags[INVISIBLE_DITHER] = false; + entity->sprite = 0; + if ( torsoCovering > 0 ) + { + entity->sprite = torsoCovering; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + else + { + entity->flags[INVISIBLE] = true; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)) ) + { + entity->skill[11] = ((entity->flags[INVISIBLE] ? 1 : 0) + (entity->flags[INVISIBLE_DITHER] ? 2 : 0)); + updateBodypart = true; + } + if ( PLAYER_ALIVETIME == TICKS_PER_SECOND + bodypart ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + } + } + my->setHumanoidLimbOffset(entity, playerRace, LIMB_HUMANOID_TORSO); + break; + case 14: // robe leg left + entity->scalex = 1.025; + entity->scaley = 1.025; + entity->scalez = 1.025; + entity->focalx = limbs[HUMAN][26][0]; + entity->focaly = limbs[HUMAN][26][1]; + entity->focalz = limbs[HUMAN][26][2]; + entity->x = torso->x; + entity->y = torso->y; + entity->z = torso->z; + entity->z += torso->focalz; + + if ( playerRace == MYCONID || playerRace == GOATMAN ) + { + entity->x += (-.2) * cos(my->yaw); + entity->y += (-.2) * sin(my->yaw); + } + entity->x += (limbs[HUMAN][28][0]) * cos(my->yaw) + limbs[HUMAN][28][1] * cos(my->yaw + PI / 2); + entity->y += (limbs[HUMAN][28][0]) * sin(my->yaw) + limbs[HUMAN][28][1] * sin(my->yaw + PI / 2); + entity->z += limbs[HUMAN][28][2]; + entity->flags[INVISIBLE_DITHER] = false; + entity->sprite = 0; + if ( torso && !(torso->flags[INVISIBLE] && !torso->flags[INVISIBLE_DITHER]) ) + { + if ( torso->sprite == items[ROBE_WIZARD].index ) + { + entity->sprite = 2137; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + else if ( torso->sprite == items[ROBE_CULTIST].index ) + { + entity->sprite = 2368; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + else if ( torso->sprite == items[ROBE_HEALER].index ) + { + entity->sprite = 2370; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + else if ( torso->sprite == items[ROBE_MONK].index ) + { + entity->sprite = 2372; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + } + /*else if ( torso && torso->sprite == items[ROBE_WIZARD].indexShort ) + { + entity->sprite = 2139; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + }*/ + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + } + entity->roll = 0.5 * abs(legleft->pitch); + entity->pitch = 0.0; + //entity->roll = 0.0; + //entity->pitch = legleft->pitch; + break; + case 15: // robe leg right + entity->scalex = 1.025; + entity->scaley = 1.025; + entity->scalez = 1.025; + entity->focalx = limbs[HUMAN][27][0]; + entity->focaly = limbs[HUMAN][27][1]; + entity->focalz = limbs[HUMAN][27][2]; + entity->x = torso->x; + entity->y = torso->y; + entity->z = torso->z; + entity->z += torso->focalz; + + if ( playerRace == MYCONID || playerRace == GOATMAN ) + { + entity->x += (-.2) * cos(my->yaw); + entity->y += (-.2) * sin(my->yaw); + } + entity->x += (limbs[HUMAN][29][0]) * cos(my->yaw) + limbs[HUMAN][29][1] * cos(my->yaw + PI / 2); + entity->y += (limbs[HUMAN][29][0]) * sin(my->yaw) + limbs[HUMAN][29][1] * sin(my->yaw + PI / 2); + entity->z += limbs[HUMAN][29][2]; + entity->flags[INVISIBLE_DITHER] = false; + entity->sprite = 0; + if ( torso && !(torso->flags[INVISIBLE] && !torso->flags[INVISIBLE_DITHER]) ) + { + if ( torso->sprite == items[ROBE_WIZARD].index ) + { + entity->sprite = 2138; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + else if ( torso->sprite == items[ROBE_CULTIST].index ) + { + entity->sprite = 2369; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + else if ( torso->sprite == items[ROBE_HEALER].index ) + { + entity->sprite = 2371; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + else if ( torso->sprite == items[ROBE_MONK].index ) + { + entity->sprite = 2373; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + } + } + /*else if ( torso && torso->sprite == items[ROBE_WIZARD].indexShort ) + { + entity->sprite = 2140; + entity->flags[INVISIBLE] = my->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; + }*/ + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + } + entity->roll = -0.5 * abs(legright->pitch); + entity->pitch = 0.0; + //entity->roll = 0.0; + //entity->pitch = legright->pitch; + break; default: break; } @@ -10105,14 +14602,13 @@ void actPlayer(Entity* my) if ( !showEquipment ) { - if ( bodypart >= 6 && bodypart <= 10 ) + if ( bodypart >= 6 && bodypart <= 10 || bodypart == 13 || bodypart == 14 || bodypart == 15 ) { entity->flags[INVISIBLE] = true; entity->flags[INVISIBLE_DITHER] = false; if ( playerRace == CREATURE_IMP && bodypart == 7 ) { - if ( entity->sprite >= items[SPELLBOOK_LIGHT].index - && entity->sprite < (items[SPELLBOOK_LIGHT].index + items[SPELLBOOK_LIGHT].variations) ) + if ( shieldSpriteAllowedImpForm(entity->sprite) ) { entity->flags[INVISIBLE] = my->flags[INVISIBLE]; // show spellbooks entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; @@ -10120,7 +14616,7 @@ void actPlayer(Entity* my) } else if ( playerRace == CREATURE_IMP && bodypart == 6 ) { - if ( entity->sprite >= 59 && entity->sprite < 64 ) + if ( weaponSpriteAllowedImpForm(entity->sprite) && !(PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2) ) { entity->flags[INVISIBLE] = my->flags[INVISIBLE]; // show magicstaffs entity->flags[INVISIBLE_DITHER] = entity->flags[INVISIBLE]; @@ -10171,6 +14667,26 @@ void actPlayer(Entity* my) } } +void Entity::playerShakeGrowthHelmet() +{ + if ( multiplayer == CLIENT ) { return; } + if ( behavior == &actPlayer ) + { + Stat* myStats = getStats(); + if ( myStats && (myStats->type == MYCONID || myStats->type == DRYAD) && !myStats->helmet && myStats->getEffectActive(EFF_GROWTH) > 1 ) + { + if ( node_t* node = list_Node(&children, 9) ) + { + if ( Entity* entity = (Entity*)node->element ) + { + entity->skill[1] = 1; + serverUpdateEntitySkill(entity, 1); + } + } + } + } +} + // client function void actPlayerLimb(Entity* my) { @@ -10225,15 +14741,51 @@ void actPlayerLimb(Entity* my) } } - if ( parent && parent->monsterEntityRenderAsTelepath == 1 ) + if ( parent && parent->monsterEntityRenderAsTelepath != 0 ) { - my->monsterEntityRenderAsTelepath = 1; + my->monsterEntityRenderAsTelepath = parent->monsterEntityRenderAsTelepath; } else { my->monsterEntityRenderAsTelepath = 0; } + if ( parent && parent->mistformGLRender > 0.05 ) + { + if ( my->skill[4] == 1 ) // shields + { + real_t modulus = fmod(parent->mistformGLRender, 1.0); + if ( modulus >= 0.05 && modulus < 0.15 ) // force shield + { + my->mistformGLRender = 0.5; + } + else if ( modulus >= 0.15 && modulus < 0.25 ) // reflector shield + { + my->mistformGLRender = 0.6; + } + else + { + my->mistformGLRender = parent->mistformGLRender; + } + } + else + { + if ( parent->mistformGLRender > 0.9 ) + { + my->mistformGLRender = parent->mistformGLRender; + } + else + { + my->mistformGLRender = 0.0; + } + } + } + else + { + my->mistformGLRender = 0.0; + } + + if (multiplayer != CLIENT) { return; @@ -10282,8 +14834,51 @@ void actPlayerLimb(Entity* my) players[my->skill[2]]->entity->skill[1] = 3; my->skill[4] = 1; // lets us know that this is indeed the off-hand item (shield) } + else if ( my->sprite == items[TOOL_FOCI_FIRE].index ) + { + players[my->skill[2]]->entity->skill[1] = 4; + my->skill[4] = 1; // lets us know that this is indeed the off-hand item (shield) + } + else if ( my->sprite == items[TOOL_FOCI_SNOW].index ) + { + players[my->skill[2]]->entity->skill[1] = 5; + my->skill[4] = 1; // lets us know that this is indeed the off-hand item (shield) + } + else if ( my->sprite == items[TOOL_FOCI_NEEDLES].index ) + { + players[my->skill[2]]->entity->skill[1] = 6; + my->skill[4] = 1; // lets us know that this is indeed the off-hand item (shield) + } + else if ( my->sprite == items[TOOL_FOCI_ARCS].index ) + { + players[my->skill[2]]->entity->skill[1] = 7; + my->skill[4] = 1; // lets us know that this is indeed the off-hand item (shield) + } + else if ( my->sprite == items[TOOL_FOCI_SAND].index ) + { + players[my->skill[2]]->entity->skill[1] = 8; + my->skill[4] = 1; // lets us know that this is indeed the off-hand item (shield) + } + else if ( my->sprite == items[TOOL_FOCI_DARK_LIFE].index + || my->sprite == items[TOOL_FOCI_DARK_RIFT].index + || my->sprite == items[TOOL_FOCI_DARK_SILENCE].index + || my->sprite == items[TOOL_FOCI_DARK_SUPPRESS].index + || my->sprite == items[TOOL_FOCI_DARK_VENGEANCE].index ) + { + players[my->skill[2]]->entity->skill[1] = 9; + my->skill[4] = 1; // lets us know that this is indeed the off-hand item (shield) + } + else if ( my->sprite == items[TOOL_FOCI_LIGHT_JUSTICE].index + || my->sprite == items[TOOL_FOCI_LIGHT_PEACE].index + || my->sprite == items[TOOL_FOCI_LIGHT_PROVIDENCE].index + || my->sprite == items[TOOL_FOCI_LIGHT_PURITY].index + || my->sprite == items[TOOL_FOCI_LIGHT_SANCTUARY].index ) + { + players[my->skill[2]]->entity->skill[1] = 10; + my->skill[4] = 1; // lets us know that this is indeed the off-hand item (shield) + } else { - // PLAYER_TORCH = 4 aka none + // PLAYER_TORCH = aka none if (my->skill[4]) { // we have to check skill[4] because if this logic runs for every limb, // the others will override the PLAYER_TORCH value set by the shield. @@ -10561,6 +15156,22 @@ bool Entity::isPlayerHeadSprite(const int sprite) case 1043: case 1044: case 1049: + case 1963: + case 1964: + case 1992: + case 1993: + case 1997: + case 1998: + case 2014: + case 2015: + case 2016: + case 2017: + case 2018: + case 2019: + case 2047: + case 2048: + case 2213: + case 2214: return true; break; default: @@ -10617,6 +15228,21 @@ Monster getMonsterFromPlayerRace(int playerRace) case RACE_IMP: return CREATURE_IMP; break; + case RACE_DRYAD: + return DRYAD; + break; + case RACE_MYCONID: + return MYCONID; + break; + case RACE_GREMLIN: + return GREMLIN; + break; + case RACE_SALAMANDER: + return SALAMANDER; + break; + case RACE_GNOME: + return GNOME; + break; default: return HUMAN; break; @@ -10629,7 +15255,7 @@ Monster Entity::getMonsterFromPlayerRace(int playerRace) return ::getMonsterFromPlayerRace(playerRace); } -void Entity::setDefaultPlayerModel(int playernum, Monster playerRace, int limbType) +void Entity::setDefaultPlayerModel(int playernum, Monster playerRace, int limbType, int headSprite) { if ( !players[playernum] || !players[playernum]->entity ) { @@ -10721,6 +15347,33 @@ void Entity::setDefaultPlayerModel(int playernum, Monster playerRace, int limbTy case TROLL: this->sprite = 818; break; + case DRYAD: + this->sprite = stats[playernum]->sex == FEMALE ? 1991 : 1989; + if ( headSprite == 1964 || headSprite == 1993 ) + { + this->sprite = stats[playernum]->sex == FEMALE ? 1994 : 1990; + } + break; + case MYCONID: + this->sprite = stats[playernum]->sex == MALE ? 2013 : 2012; + break; + case SALAMANDER: + this->sprite = 2038; + if ( headSprite == 2016 || headSprite == 2017 ) + { + this->sprite = 2039; + } + else if ( headSprite == 2018 || headSprite == 2019 ) + { + this->sprite = 2040; + } + break; + case GREMLIN: + this->sprite = stats[playernum]->sex == FEMALE ? 2062 : 2061; + break; + case GNOME: + this->sprite = stats[playernum]->sex == FEMALE ? 2216 : 2215; + break; default: break; } @@ -10798,6 +15451,33 @@ void Entity::setDefaultPlayerModel(int playernum, Monster playerRace, int limbTy case TROLL: this->sprite = 822; break; + case DRYAD: + this->sprite = stats[playernum]->sex == FEMALE ? 1988 : 1986; + if ( headSprite == 1964 || headSprite == 1993 ) + { + this->sprite = stats[playernum]->sex == FEMALE ? 1996 : 1987; + } + break; + case MYCONID: + this->sprite = stats[playernum]->sex == MALE ? 2011 : 2010; + break; + case SALAMANDER: + this->sprite = 2033; + if ( headSprite == 2016 || headSprite == 2017 ) + { + this->sprite = 2035; + } + else if ( headSprite == 2018 || headSprite == 2019 ) + { + this->sprite = 2037; + } + break; + case GREMLIN: + this->sprite = stats[playernum]->sex == FEMALE ? 2060 : 2058; + break; + case GNOME: + this->sprite = stats[playernum]->sex == FEMALE ? 2219 : 2217; + break; default: break; } @@ -10875,6 +15555,33 @@ void Entity::setDefaultPlayerModel(int playernum, Monster playerRace, int limbTy case TROLL: this->sprite = 821; break; + case DRYAD: + this->sprite = stats[playernum]->sex == FEMALE ? 1985 : 1983; + if ( headSprite == 1964 || headSprite == 1993 ) + { + this->sprite = stats[playernum]->sex == FEMALE ? 1995 : 1984; + } + break; + case MYCONID: + this->sprite = stats[playernum]->sex == MALE ? 2009 : 2008; + break; + case SALAMANDER: + this->sprite = 2032; + if ( headSprite == 2016 || headSprite == 2017 ) + { + this->sprite = 2034; + } + else if ( headSprite == 2018 || headSprite == 2019 ) + { + this->sprite = 2036; + } + break; + case GREMLIN: + this->sprite = stats[playernum]->sex == FEMALE ? 2059 : 2057; + break; + case GNOME: + this->sprite = stats[playernum]->sex == FEMALE ? 2220 : 2218; + break; default: break; } @@ -10938,6 +15645,33 @@ void Entity::setDefaultPlayerModel(int playernum, Monster playerRace, int limbTy case TROLL: this->sprite = 820; break; + case DRYAD: + this->sprite = 1966; + if ( headSprite == 1964 || headSprite == 1993 ) + { + this->sprite = 1970; + } + break; + case MYCONID: + this->sprite = 2000; + break; + case SALAMANDER: + this->sprite = 2021; + if ( headSprite == 2016 || headSprite == 2017 ) + { + this->sprite = 2025; + } + else if ( headSprite == 2018 || headSprite == 2019 ) + { + this->sprite = 2029; + } + break; + case GREMLIN: + this->sprite = stats[playernum]->sex == FEMALE ? 2054 : 2050; + break; + case GNOME: + this->sprite = 2221; + break; default: break; } @@ -11001,6 +15735,33 @@ void Entity::setDefaultPlayerModel(int playernum, Monster playerRace, int limbTy case TROLL: this->sprite = 819; break; + case DRYAD: + this->sprite = 1965; + if ( headSprite == 1964 || headSprite == 1993 ) + { + this->sprite = 1969; + } + break; + case MYCONID: + this->sprite = 1999; + break; + case SALAMANDER: + this->sprite = 2020; + if ( headSprite == 2016 || headSprite == 2017 ) + { + this->sprite = 2024; + } + else if ( headSprite == 2018 || headSprite == 2019 ) + { + this->sprite = 2028; + } + break; + case GREMLIN: + this->sprite = stats[playernum]->sex == FEMALE ? 2053 : 2049; + break; + case GNOME: + this->sprite = 2222; + break; default: break; } @@ -11023,7 +15784,7 @@ bool playerRequiresBloodToSustain(int player) { return true; } - if ( stats[player]->EFFECTS[EFF_VAMPIRICAURA] || client_classes[player] == CLASS_ACCURSED ) + if ( stats[player]->getEffectActive(EFF_VAMPIRICAURA) || client_classes[player] == CLASS_ACCURSED ) { return true; } @@ -11048,6 +15809,52 @@ void playerAnimateRat(Entity* my) my->scalex = 1.02; my->scaley = 1.02; my->scalez = 1.02; + + if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + // magic wiggle hands + if ( ticks % 10 == 0 ) + { + Entity* gib = spawnGib(my, 16); + gib->flags[INVISIBLE] = false; + gib->flags[SPRITE] = true; + gib->flags[NOUPDATE] = true; + gib->flags[UPDATENEEDED] = false; + gib->lightBonus = vec4(0.2f, 0.2f, 0.2f, 0.f); + gib->x += 4.5 * cos(my->yaw); + gib->y += 4.5 * sin(my->yaw); + gib->z = my->z - 2.0; + gib->scalex = 0.25f; //MAKE 'EM SMALL PLEASE! + gib->scaley = 0.25f; + gib->scalez = 0.25f; + gib->sprite = 16; //TODO: Originally. 22. 16 -- spark sprite instead? + gib->yaw = ((local_rng.rand() % 6) * 60) * PI / 180.0; + gib->pitch = (local_rng.rand() % 360) * PI / 180.0; + gib->roll = (local_rng.rand() % 360) * PI / 180.0; + gib->vel_x = cos(my->yaw) * .1; + gib->vel_y = sin(my->yaw) * .1; + gib->vel_z = -.15; + gib->fskill[3] = 0.01; + gib->fskill[4] = 0.01; // GIB_SHRINK + gib->skill[4] = 25; // GIB_LIFESPAN + gib->skill[11] = my->skill[2]; + gib->actGibDisableDrawForLocalPlayer = 1 + my->skill[2]; + } + + if ( PLAYER_ATTACKTIME >= my->playerCastTimeAnim ) + { + PLAYER_ATTACK = 0; + } + } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2 ) + { + doParticleEffectForTouchSpell(*my, my, RAT); + } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_CAST2 ) + { + my->playerCastTimeAnim = 0; + PLAYER_ATTACK = 0; + } continue; } Entity* entity = (Entity*)node->element; @@ -11115,7 +15922,10 @@ void playerAnimateRat(Entity* my) if ( PLAYER_ATTACKTIME >= 10 ) { - PLAYER_ATTACK = 0; + if ( !(PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 || PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2) ) + { + PLAYER_ATTACK = 0; + } } } @@ -11164,9 +15974,12 @@ void playerAnimateSpider(Entity* my) continue; } Entity* previous = NULL; // previous part - if ( bodypart > 11 ) + if ( bodypart > 12 ) { previous = (Entity*)node->prev->element; + // for GENIUS flag to draw into the camera + entity->sizex = 1; + entity->sizey = 1; } entity->x = my->x; entity->y = my->y; @@ -11252,6 +16065,108 @@ void playerAnimateSpider(Entity* my) } } } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + // magic wiggle hands + entity->skill[1] = 0; + PLAYER_ARMBENDED = 0; + PLAYER_WEAPONYAW = 0; + entity->roll = 0; + entity->pitch = 0; + entity->yaw = my->yaw; + + real_t circleAmount = (1 * PI / 8); + circleAmount *= 0.5; + real_t scaleDown = 1.0; + if ( (my->playerCastTimeAnim - PLAYER_ATTACKTIME) < 10 ) + { + // scale down + scaleDown = 1.0 - (10 - (my->playerCastTimeAnim - PLAYER_ATTACKTIME)) / 10.0; + } + + circleAmount *= scaleDown; + + real_t circleTime = 20.0; + entity->pitch = circleAmount * cos(2 * PI * (PLAYER_ATTACKTIME / (real_t)circleTime)); + entity->pitch -= scaleDown * (PI / 2) * std::min(1.0, (PLAYER_ATTACKTIME / (real_t)5)); + + if ( bodypart == 11 ) + { + entity->yaw -= circleAmount * sin(2 * PI * (PLAYER_ATTACKTIME / (real_t)circleTime)); + } + else + { + entity->yaw += circleAmount * sin(2 * PI * (PLAYER_ATTACKTIME / (real_t)circleTime)); + } + + if ( ticks % 10 == 0 ) + { + Entity* gib = spawnGib(entity, 16); + gib->flags[INVISIBLE] = false; + gib->flags[SPRITE] = true; + gib->flags[NOUPDATE] = true; + gib->flags[UPDATENEEDED] = false; + gib->lightBonus = vec4(0.2f, 0.2f, 0.2f, 0.f); + gib->x += 3.5 * cos(entity->yaw); + gib->y += 3.5 * sin(entity->yaw); + if ( bodypart == 11 ) + { + gib->x += 3.0 * cos(entity->yaw + PI / 2); + gib->y += 3.0 * sin(entity->yaw + PI / 2); + } + else + { + gib->x -= 3.0 * cos(entity->yaw + PI / 2); + gib->y -= 3.0 * sin(entity->yaw + PI / 2); + } + gib->z = entity->z - 2.0; + gib->scalex = 0.25f; //MAKE 'EM SMALL PLEASE! + gib->scaley = 0.25f; + gib->scalez = 0.25f; + gib->sprite = 16; //TODO: Originally. 22. 16 -- spark sprite instead? + gib->yaw = ((local_rng.rand() % 6) * 60) * PI / 180.0; + gib->pitch = (local_rng.rand() % 360) * PI / 180.0; + gib->roll = (local_rng.rand() % 360) * PI / 180.0; + gib->vel_x = cos(entity->yaw) * .1; + gib->vel_y = sin(entity->yaw) * .1; + gib->vel_z = -.15; + gib->fskill[3] = 0.01; + gib->fskill[4] = 0.01; // GIB_SHRINK + gib->skill[4] = 25; // GIB_LIFESPAN + gib->skill[11] = my->skill[2]; + gib->actGibDisableDrawForLocalPlayer = 1 + my->skill[2]; + } + + if ( PLAYER_ATTACKTIME >= my->playerCastTimeAnim ) + { + my->playerCastTimeAnim = 0; + entity->skill[1] = 0; + PLAYER_WEAPONYAW = 0; + entity->roll = 0; + PLAYER_ARMBENDED = 0; + PLAYER_ATTACK = 0; + } + } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_WINDUP2 ) + { + // touch spell intermission + my->playerCastTimeAnim = 0; + entity->skill[1] = 0; + PLAYER_ARMBENDED = 0; + PLAYER_WEAPONYAW = 0; + entity->roll = 0; + entity->pitch = 0; + entity->yaw = my->yaw; + + entity->pitch -= (PI / 2) * std::min(1.0, (PLAYER_ATTACKTIME / (real_t)5)); + + doParticleEffectForTouchSpell(*my, my, SPIDER); + } + else if ( PLAYER_ATTACK == MONSTER_POSE_MAGIC_CAST2 ) + { + my->playerCastTimeAnim = 0; + PLAYER_ATTACK = 0; + } } entity->flags[INVISIBLE] = my->flags[INVISIBLE]; diff --git a/src/actsink.cpp b/src/actsink.cpp index e14ac5885..bdd415e1f 100644 --- a/src/actsink.cpp +++ b/src/actsink.cpp @@ -122,15 +122,15 @@ void actSink(Entity* my) serverUpdateEntityFlag(players[i]->entity, BURNING); steamAchievementClient(i, "BARONY_ACH_HOT_SHOWER"); } - if ( stats[i] && stats[i]->EFFECTS[EFF_POLYMORPH] && (SINK_DISABLE_POLYMORPH_WASHING == 0) ) + if ( stats[i] && stats[i]->getEffectActive(EFF_POLYMORPH) && (SINK_DISABLE_POLYMORPH_WASHING == 0) ) { - if ( stats[i]->EFFECTS[EFF_POLYMORPH] ) + if ( stats[i]->getEffectActive(EFF_POLYMORPH) ) { players[i]->entity->setEffect(EFF_POLYMORPH, false, 0, true); players[i]->entity->effectPolymorph = 0; serverUpdateEntitySkill(players[i]->entity, 50); messagePlayer(i, MESSAGE_INTERACTION, Language::get(3192)); - if ( !stats[i]->EFFECTS[EFF_SHAPESHIFT] ) + if ( !stats[i]->getEffectActive(EFF_SHAPESHIFT) ) { messagePlayer(i, MESSAGE_INTERACTION, Language::get(3185)); } @@ -229,11 +229,37 @@ void actSink(Entity* my) playSoundEntity(players[i]->entity, 52, 64); if ( stats[i]->type != SKELETON ) { - stats[i]->HUNGER += 50; //Less nutrition than the refreshing fountain. - serverUpdateHunger(i); + if ( !((svFlags & SV_FLAG_HUNGER) && stats[i]->playerRace == RACE_INSECTOID && stats[i]->stat_appearance == 0) ) + { + stats[i]->HUNGER += 50; //Less nutrition than the refreshing fountain. + serverUpdateHunger(i); + } } - players[i]->entity->modHP(1); + players[i]->entity->modHP(2 + local_rng.rand() % 2); + + int mpAmount = players[i]->entity->modMP(1 + local_rng.rand() % 2); + players[i]->entity->playerInsectoidIncrementHungerToMP(mpAmount); Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_SINKS_HEALTH_RESTORED, "sink", 1); + + if ( stats[i]->type == DRYAD ) + { + if ( auto effectStrength = stats[i]->getEffectActive(EFF_GROWTH) ) + { + int chance = 5; + if ( (stats[i]->type == DRYAD && stats[i]->sex == FEMALE) ) + { + chance = 10; + } + if ( players[i]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_GROWTH, chance) ) + { + if ( stats[i]->getEffectActive(EFF_GROWTH) < 4 ) + { + players[i]->entity->setEffect(EFF_GROWTH, (Uint8)(std::min(4, effectStrength + 1)), 15 * TICKS_PER_SECOND, false); + messagePlayerColor(i, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6924)); + } + } + } + } } else { @@ -277,7 +303,7 @@ void actSink(Entity* my) messagePlayerColor(i, MESSAGE_STATUS, color, Language::get(3701)); playSoundEntity(players[i]->entity, 52, 64); stats[i]->HUNGER += 200; //Gain boiler - players[i]->entity->modMP(2); + players[i]->entity->modMP(4); serverUpdateHunger(i); break; } diff --git a/src/actspeartrap.cpp b/src/actspeartrap.cpp index f666d7d54..dc108cdcd 100644 --- a/src/actspeartrap.cpp +++ b/src/actspeartrap.cpp @@ -37,30 +37,37 @@ void actSpearTrap(Entity* my) { -#ifdef USE_FMOD - if ( SPEARTRAP_AMBIENCE == 0 ) + if ( my->actTrapSabotaged == 0 ) { +#ifdef USE_FMOD + if ( SPEARTRAP_AMBIENCE == 0 ) + { + SPEARTRAP_AMBIENCE--; + my->stopEntitySound(); + my->entity_sound = playSoundEntityLocal(my, 149, 64); + } + if ( my->entity_sound ) + { + bool playing = false; + my->entity_sound->isPlaying(&playing); + if ( !playing ) + { + my->entity_sound = nullptr; + } + } +#else SPEARTRAP_AMBIENCE--; - my->stopEntitySound(); - my->entity_sound = playSoundEntityLocal(my, 149, 64); - } - if ( my->entity_sound ) - { - bool playing = false; - my->entity_sound->isPlaying(&playing); - if ( !playing ) + if ( SPEARTRAP_AMBIENCE <= 0 ) { - my->entity_sound = nullptr; + SPEARTRAP_AMBIENCE = TICKS_PER_SECOND * 30; + playSoundEntityLocal(my, 149, 64); } +#endif } -#else - SPEARTRAP_AMBIENCE--; - if ( SPEARTRAP_AMBIENCE <= 0 ) + else { - SPEARTRAP_AMBIENCE = TICKS_PER_SECOND * 30; - playSoundEntityLocal( my, 149, 64 ); + my->stopEntitySound(); } -#endif if ( multiplayer != CLIENT ) { @@ -69,7 +76,7 @@ void actSpearTrap(Entity* my) return; } - if (my->skill[28] == 2) + if (my->skill[28] == 2 && my->actTrapSabotaged == 0 ) { // shoot out the spears if (!SPEARTRAP_STATUS ) @@ -143,6 +150,13 @@ void actSpearTrap(Entity* my) Stat* stats = entity->getStats(); if ( stats ) { + if ( stats->type == DUCK_SMALL && entityInsideEntity(my, entity) ) + { + if ( entity->monsterAttack == 0 ) + { + entity->attack(local_rng.rand() % 2 ? MONSTER_POSE_MELEE_WINDUP2 : MONSTER_POSE_MELEE_WINDUP3, 0, nullptr); + } + } if ( !entity->flags[PASSABLE] && entityInsideEntity(my, entity) ) { // do damage! @@ -182,7 +196,16 @@ void actSpearTrap(Entity* my) damage *= mult; } - if ( damage > 0 ) + if ( entity->onEntityTrapHitSacredPath(my) ) + { + if ( entity->behavior == &actPlayer ) + { + messagePlayerColor(entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + Language::get(6492)); + } + playSoundEntity(entity, 166, 128); + } + else if ( damage > 0 ) { playSoundEntity(entity, 28, 64); spawnGib(entity); diff --git a/src/actsprite.cpp b/src/actsprite.cpp index 4fb43a9fc..fbebcb9d1 100644 --- a/src/actsprite.cpp +++ b/src/actsprite.cpp @@ -34,12 +34,43 @@ #define SPRITE_FRAMES my->skill[1] #define SPRITE_ANIMSPEED my->skill[2] #define SPRITE_LIT my->skill[5] +#define SPRITE_ROTATE my->fskill[0] +#define SPRITE_CURRENT_ALPHA my->fskill[1] +#define SPRITE_ALPHA_VAR my->fskill[2] +#define SPRITE_ALPHA_ANIM_SIZE my->fskill[3] +#define SPRITE_CHECK_PARENT_EXISTS my->skill[8] void actSprite(Entity* my) { - if ( !my->skill[6] && SPRITE_LIT ) + if ( SPRITE_CHECK_PARENT_EXISTS > 0 ) { - my->skill[6] = 1; + if ( !uidToEntity(SPRITE_CHECK_PARENT_EXISTS) ) + { + if ( my->actSpriteUseCustomSurface != 0 ) + { + if ( auto fx = AOEIndicators_t::getIndicator(my->actSpriteUseCustomSurface) ) + { + fx->expired = true; + } + } + + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + if ( my->actSpriteFollowUID > 0 ) + { + if ( Entity* parent = uidToEntity(my->actSpriteFollowUID) ) + { + my->x = parent->x; + my->y = parent->y; + } + } + + if ( !my->actSpriteHasLightInit && SPRITE_LIT ) + { + my->actSpriteHasLightInit = 1; my->light = addLight(my->x / 16, my->y / 16, "explosion"); } else if ( !SPRITE_LIT ) @@ -64,6 +95,28 @@ void actSprite(Entity* my) } } } + + if ( SPRITE_ROTATE > 0.0001 ) + { + my->yaw += SPRITE_ROTATE; + } + if ( my->actSpritePitchRotate > 0.0001 ) + { + my->pitch += my->actSpritePitchRotate; + } + if ( SPRITE_ALPHA_VAR > 0.0001 ) + { + SPRITE_CURRENT_ALPHA = SPRITE_ALPHA_VAR + SPRITE_ALPHA_ANIM_SIZE * sin(2 * PI * (my->ticks % TICKS_PER_SECOND) / (real_t)(TICKS_PER_SECOND)); + } + if ( abs(my->vel_z) > 0.001 ) + { + my->z += my->vel_z; + } + if ( my->actSpriteVelXY != 0 ) + { + my->x += my->vel_x; + my->y += my->vel_y; + } } void actSpriteNametag(Entity* my) @@ -115,7 +168,7 @@ void actSpriteWorldTooltip(Entity* my) my->x = parent->x; my->y = parent->y; - if ( parent->behavior == &actDoor ) + if ( parent->behavior == &actDoor || parent->behavior == &actIronDoor ) { if ( parent->flags[PASSABLE] ) { diff --git a/src/actsummontrap.cpp b/src/actsummontrap.cpp index 155c6434f..5a8751c73 100644 --- a/src/actsummontrap.cpp +++ b/src/actsummontrap.cpp @@ -114,7 +114,18 @@ void actSummonTrap(Entity* my) { LICH, SHOPKEEPER, DEVIL, MIMIC, CRAB, BAT_SMALL, MINOTAUR, LICH_FIRE, LICH_ICE, NOTHING, - SENTRYBOT, SPELLBOT, GYROBOT, DUMMYBOT + SENTRYBOT, SPELLBOT, GYROBOT, DUMMYBOT, + REVENANT_SKULL, + MINIMIMIC, + MONSTER_ADORCISED_WEAPON, + FLAME_ELEMENTAL, + HOLOGRAM, + EARTH_ELEMENTAL, + DUCK_SMALL, + MONSTER_UNUSED_6, + MONSTER_UNUSED_7, + MONSTER_UNUSED_8, + MOTH_SMALL }; std::vector possibleTypes; @@ -174,6 +185,7 @@ void actSummonTrap(Entity* my) { monster->getStats()->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS] = 1; // disable champion normally. } + monster->getStats()->setAttribute("spawn_no_sleep", "1"); if ( foundTriggerEntity ) { diff --git a/src/actteleporter.cpp b/src/actteleporter.cpp index 1f0df753e..ecf634aab 100644 --- a/src/actteleporter.cpp +++ b/src/actteleporter.cpp @@ -47,6 +47,17 @@ void Entity::actTeleporter() createWorldUITooltip(); } + if ( teleporterDuration > 0 && multiplayer != CLIENT ) + { + --teleporterDuration; + if ( teleporterDuration <= 0 ) + { + this->removeLightField(); + list_RemoveNode(this->mynode); + return; + } + } + #ifdef USE_FMOD if ( teleporterAmbience == 0 ) { @@ -80,7 +91,27 @@ void Entity::actTeleporter() Entity* monsterInteracting = uidToEntity(this->interactedByMonster); if ( monsterInteracting ) { - monsterInteracting->teleporterMove(teleporterX, teleporterY, teleporterType); + if ( teleporterType == 3 ) + { + if ( monsterInteracting->teleport(teleporterX, teleporterY) ) + { + if ( Entity* caster = uidToEntity(this->parent) ) + { + if ( auto hitprops = getParticleEmitterHitProps(this->getUID(), this) ) + { + if ( hitprops->hits == 0 ) + { + magicOnSpellCastEvent(caster, caster, nullptr, SPELL_TUNNEL, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + ++hitprops->hits; + } + } + } + } + } + else + { + monsterInteracting->teleporterMove(teleporterX, teleporterY, teleporterType); + } this->clearMonsterInteract(); return; } @@ -106,7 +137,28 @@ void Entity::actTeleporter() default: break; } - Player::getPlayerInteractEntity(i)->teleporterMove(teleporterX, teleporterY, teleporterType); + if ( teleporterType == 3 ) + { + if ( Player::getPlayerInteractEntity(i)->teleport(teleporterX, teleporterY) ) + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6696)); + if ( Entity* caster = uidToEntity(this->parent) ) + { + if ( auto hitprops = getParticleEmitterHitProps(this->getUID(), this) ) + { + if ( hitprops->hits == 0 ) + { + magicOnSpellCastEvent(caster, caster, nullptr, SPELL_TUNNEL, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + ++hitprops->hits; + } + } + } + } + } + else + { + Player::getPlayerInteractEntity(i)->teleporterMove(teleporterX, teleporterY, teleporterType); + } return; } } @@ -126,4 +178,26 @@ void Entity::actTeleporter() sprite = 992 + ((this->ticks / 20) % 4) - 1; // animate through 992, 993, 994 } } + else if ( teleporterType == 3 ) + { + if ( !light ) + { + light = addLight(x / 16, y / 16, "portal_purple"); + } + + if ( ::ticks % 4 == 0 ) + { + sprite = teleporterStartFrame + teleporterCurrentFrame; + ++teleporterCurrentFrame; + if ( teleporterCurrentFrame >= teleporterNumFrames ) + { + teleporterCurrentFrame = 0; + } + } + + real_t increment = std::max(.05, (1.0 - scalex)) / 3.0; + scalex = std::min(1.0, scalex + increment); + scaley = std::min(1.0, scaley + increment); + scalez = std::min(1.0, scalez + increment); + } } diff --git a/src/actthrown.cpp b/src/actthrown.cpp index 805d131d8..c0672ea54 100644 --- a/src/actthrown.cpp +++ b/src/actthrown.cpp @@ -49,6 +49,96 @@ #define THROWN_BOOMERANG_STOP_Z my->skill[21] #define BOOMERANG_PARTICLE 977 +#define BOLAS_PARTICLE 1916 + +void onThrownLandingParticle(Entity* my) +{ + if ( my ) + { + int itemType = THROWN_TYPE; + if ( itemType >= WOODEN_SHIELD && itemType < NUMITEMS ) + { + if ( items[itemType].category == POTION ) + { + int gibsprite = -1; + if ( my->sprite >= 50 && my->sprite <= 58 ) + { + gibsprite = 1895 + my->sprite - 50; + } + if ( itemType == POTION_EMPTY ) + { + gibsprite = 1911; + } + if ( gibsprite >= 0 ) + { + for ( int i = 0; i < 5; ++i ) + { + if ( Entity* gib = spawnGib(hit.entity ? hit.entity : my, gibsprite) ) + { + gib->sprite = gibsprite; + if ( !hit.entity ) + { + gib->z = my->z; + } + serverSpawnGibForClient(gib); + } + } + } + } + else + { + switch ( itemType ) + { + case GREASE_BALL: + for ( int i = 0; i < 5; ++i ) + { + if ( Entity* gib = spawnGib(hit.entity ? hit.entity : my, 245) ) + { + gib->sprite = 245; + gib->flags[SPRITE] = true; + if ( !hit.entity ) + { + gib->z = my->z; + } + serverSpawnGibForClient(gib); + } + } + break; + case DUST_BALL: + for ( int i = 0; i < 5; ++i ) + { + if ( Entity* gib = spawnGib(hit.entity ? hit.entity : my, 1886) ) + { + gib->sprite = 1886; + if ( !hit.entity ) + { + gib->z = my->z; + } + serverSpawnGibForClient(gib); + } + } + break; + case SLOP_BALL: + for ( int i = 0; i < 5; ++i ) + { + if ( Entity* gib = spawnGib(hit.entity ? hit.entity : my, 1926) ) + { + gib->sprite = 1926; + if ( !hit.entity ) + { + gib->z = my->z; + } + serverSpawnGibForClient(gib); + } + } + break; + default: + break; + } + } + } + } +} void actThrown(Entity* my) { @@ -66,6 +156,8 @@ void actThrown(Entity* my) item = nullptr; } + my->removeLightField(); + if ( multiplayer == CLIENT ) { if ( THROWN_LIFE == 0 ) @@ -126,6 +218,81 @@ void actThrown(Entity* my) playSoundEntityLocal(my, 434 + local_rng.rand() % 10, 64); } } + + if ( my->sprite == items[GREASE_BALL].index ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 245, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(my->vel_y, my->vel_x); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->flags[BRIGHT] = true; + fx->pitch = PI / 2; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + fx->flags[SPRITE] = true; + } + } + else if ( my->sprite == items[DUST_BALL].index ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 1886, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(my->vel_y, my->vel_x); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; + fx->pitch = PI / 2; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + } + } + else if ( my->sprite == items[SLOP_BALL].index ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 1926, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(my->vel_y, my->vel_x); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; + fx->pitch = PI / 2; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + } + } + else if ( my->sprite == items[GEM_JEWEL].index ) + { + if ( my->ticks % 4 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 2410, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + fx->focalz = 0.25; + fx->vel_z = 0.04; + fx->fskill[0] = 0.04; + real_t dir = atan2(my->vel_y, my->vel_x); + if ( local_rng.rand() % 2 ) + { + dir += PI; + } + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + fx->vel_x = spd * 0.05 * cos(dir + PI / 2); + fx->vel_y = spd * 0.05 * sin(dir + PI / 2); + } + } + my->light = addLight(my->x / 16, my->y / 16, "jewel_yellow"); + } } else { @@ -144,6 +311,10 @@ void actThrown(Entity* my) playSoundEntityLocal(my, 434 + local_rng.rand() % 10, 64); } } + else if ( item->type == BOLAS ) + { + my->sprite = BOLAS_PARTICLE; + } free(item); item = nullptr; } @@ -161,6 +332,7 @@ void actThrown(Entity* my) { if ( my->ticks > (THROWN_LINGER + 1) ) { + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -238,11 +410,15 @@ void actThrown(Entity* my) /*THROWN_VELX = 0.f; THROWN_VELY = 0.f; THROWN_VELZ = 0.f;*/ - if ( type == BRONZE_TOMAHAWK || type == IRON_DAGGER ) + if ( type == BRONZE_TOMAHAWK || type == IRON_DAGGER || type == BONE_THROWING ) { // axe and dagger spin vertically my->pitch += 0.2; } + else if ( type == BLACKIRON_DART || type == SILVER_PLUMBATA ) + { + my->roll += 0.2; + } else { if ( type == BOOMERANG ) @@ -289,11 +465,114 @@ void actThrown(Entity* my) my->z += THROWN_VELZ; my->roll += 0.04; } + + if ( my->sprite == items[GREASE_BALL].index ) + { + if ( my->ticks % 5 == 0 ) + { + spawnGreasePuddleSpawner(my->parent == 0 ? nullptr : uidToEntity(my->parent), my->x, my->y, 30 * TICKS_PER_SECOND); + } + if ( Entity* fx = spawnMagicParticleCustom(my, 245, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(my->vel_y, my->vel_x); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->flags[BRIGHT] = true; + fx->pitch = PI / 2; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + fx->flags[SPRITE] = true; + } + } + else if ( my->sprite == items[DUST_BALL].index ) + { + if ( my->ticks == 1 ) + { + Uint32 lifetime = TICKS_PER_SECOND * 3; + Entity* spellTimer = createParticleTimer(my->parent == 0 ? nullptr : uidToEntity(my->parent), lifetime + TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SPORES_TRAIL; + spellTimer->particleTimerCountdownSprite = 248; + spellTimer->yaw = 0.0; + spellTimer->x = my->x; + spellTimer->y = my->y; + spellTimer->particleTimerVariable1 = 0; + spellTimer->particleTimerVariable2 = SPELL_MYCELIUM_SPORES; + spellTimer->particleTimerVariable4 = my->getUID(); + + my->thrownProjectileParticleTimerUID = spellTimer->getUID(); + particleTimerEffects.emplace(std::pair(spellTimer->getUID(), ParticleTimerEffect_t())); + } + if ( Entity* fx = spawnMagicParticleCustom(my, 1886, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(my->vel_y, my->vel_x); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + } + thrownItemUpdateSpellTrail(*my, my->x, my->y); + } + else if ( my->sprite == items[SLOP_BALL].index ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 1926, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(my->vel_y, my->vel_x); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + } + } + else if ( my->sprite == items[GEM_JEWEL].index ) + { + if ( my->ticks % 4 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 2410, 1.0, 1.0) ) + { + fx->ditheringDisabled = true; + fx->focalz = 0.25; + fx->vel_z = 0.04; + fx->fskill[0] = 0.04; + real_t dir = atan2(my->vel_y, my->vel_x); + if ( local_rng.rand() % 2 ) + { + dir += PI; + } + real_t spd = sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); + fx->vel_x = spd * 0.05 * cos(dir + PI / 2); + fx->vel_y = spd * 0.05 * sin(dir + PI / 2); + } + } + my->light = addLight(my->x / 16, my->y / 16, "jewel_yellow"); + } } else { if ( my->x >= 0 && my->y >= 0 && my->x < map.width << 4 && my->y < map.height << 4 ) { + if ( my->sprite == items[GREASE_BALL].index ) + { + spawnGreasePuddleSpawner(my->parent == 0 ? nullptr : uidToEntity(my->parent), my->x, my->y, 30 * TICKS_PER_SECOND); + } + if ( my->sprite == items[DUST_BALL].index ) + { + thrownItemUpdateSpellTrail(*my, my->x, my->y); + } + // landing on the ground. int index = (int)(my->y / 16)*MAPLAYERS + (int)(my->x / 16)*MAPLAYERS * map.height; if ( map.tiles[index] ) @@ -323,6 +602,15 @@ void actThrown(Entity* my) item->applyTinkeringCreation(parent, my); } free(item); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else if ( item && item->type == TOOL_DUCK ) + { + item->applyDuck(my->parent, my->x, my->y, nullptr, false); + free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -344,21 +632,37 @@ void actThrown(Entity* my) } playSoundEntity(my, 162, 64); free(item); + onThrownLandingParticle(my); + my->removeLightField(); list_RemoveNode(my->mynode); return; } else if ( specialMonster ) { free(item); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else if ( item && (item->type == DUST_BALL + || item->type == GREASE_BALL + || item->type == SLOP_BALL) ) + { + free(item); + onThrownLandingParticle(my); + playSoundEntity(my, 764, 64); + my->removeLightField(); list_RemoveNode(my->mynode); return; } - else if ( itemCategory(item) == GEM && (item->beatitude < 0 || local_rng.rand() % 5 == 0) ) + else if ( itemCategory(item) == GEM && (item->beatitude < 0 || local_rng.rand() % 5 == 0) + && item->type != GEM_JEWEL ) { // cursed gem, explode createParticleShatteredGem(my->x, my->y, 7.5, my->sprite, nullptr); serverSpawnMiscParticlesAtLocation(my->x, my->y, 7.5, PARTICLE_EFFECT_SHATTERED_GEM, my->sprite); free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -387,6 +691,7 @@ void actThrown(Entity* my) item->applyBomb(parent, item->type, Item::ItemBombPlacement::BOMB_FLOOR, Item::ItemBombFacingDirection::BOMB_UP, my, nullptr); } free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -411,6 +716,7 @@ void actThrown(Entity* my) return; } free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -457,12 +763,27 @@ void actThrown(Entity* my) entity->skill[13] = item->count; entity->skill[14] = item->appearance; entity->skill[15] = item->identified; + if ( item->type == GEM_JEWEL ) + { + entity->parent = my->parent; + } if ( itemCategory(item) == THROWN ) { //Hack to make monsters stop catching your shurikens and chakrams. entity->parent = my->parent; + if ( parent ) + { + if ( Stat* parentStats = parent->getStats() ) + { + if ( parentStats->getEffectActive(EFF_RETURN_ITEM) ) + { + entity->itemReturnUID = parent->getUID(); + } + } + } } free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -493,6 +814,16 @@ void actThrown(Entity* my) item->applyTinkeringCreation(parent, my); } free(item); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else if ( my->skill[10] == TOOL_DUCK ) + { + item = newItemFromEntity(my); + item->applyDuck(my->parent, my->x, my->y, nullptr, false); + free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -572,6 +903,7 @@ void actThrown(Entity* my) { free(item); } + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -605,6 +937,47 @@ void actThrown(Entity* my) } free(item); } + else if ( my->skill[10] >= WOODEN_SHIELD && my->skill[10] < NUMITEMS && items[my->skill[10]].category == THROWN ) + { + if ( parent ) + { + if ( Stat* parentStats = parent->getStats() ) + { + if ( parentStats->getEffectActive(EFF_RETURN_ITEM) ) + { + if ( Item* item = newItemFromEntity(my, true) ) + { + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. + entity->flags[INVISIBLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + entity->sizex = my->sizex; + entity->sizey = my->sizey; + entity->yaw = my->yaw; + entity->pitch = my->pitch; + entity->roll = my->roll; + entity->vel_x = THROWN_VELX; + entity->vel_y = THROWN_VELY; + entity->vel_z = my->vel_z; + entity->behavior = &actItem; + entity->skill[10] = item->type; + entity->skill[11] = item->status; + entity->skill[12] = item->beatitude; + entity->skill[13] = item->count; + entity->skill[14] = item->appearance; + entity->skill[15] = item->identified; + entity->itemReturnUID = parent->getUID(); + + free(item); + } + } + } + } + } + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -618,8 +991,61 @@ void actThrown(Entity* my) { return; } - double result = clipMove(&my->x, &my->y, THROWN_VELX, THROWN_VELY, my); - if ( processXYCollision && result != sqrt(THROWN_VELX * THROWN_VELX + THROWN_VELY * THROWN_VELY) ) + my->processEntityWind(); + + bool hitSomething = false; + real_t result = 0.0; + bool halfSpeedCheck = false; + static ConsoleVariable cvar_thrown_clip("/thrown_clip_test", true); + real_t speed = sqrt(THROWN_VELX * THROWN_VELX + THROWN_VELY * THROWN_VELY); + if ( speed > 4.0 ) // can clip through thin gates + { + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( auto it : entLists ) + { + if ( !*cvar_thrown_clip && (svFlags & SV_FLAG_CHEATS) ) + { + break; + } + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actGate || entity->behavior == &actDoor || entity->behavior == &actIronDoor ) + { + if ( entityDist(my, entity) <= speed ) + { + halfSpeedCheck = true; + break; + } + } + } + if ( halfSpeedCheck ) + { + break; + } + } + } + + if ( !halfSpeedCheck ) + { + result = clipMove(&my->x, &my->y, THROWN_VELX, THROWN_VELY, my); + hitSomething = result != sqrt(THROWN_VELX * THROWN_VELX + THROWN_VELY * THROWN_VELY); + } + else + { + real_t vel_x = THROWN_VELX / 2.0; + real_t vel_y = THROWN_VELY / 2.0; + real_t dist = clipMove(&my->x, &my->y, vel_x, vel_y, my); + result = dist; + hitSomething = dist != sqrt(vel_x * vel_x + vel_y * vel_y); + if ( !hitSomething ) + { + dist = clipMove(&my->x, &my->y, vel_x, vel_y, my); + result += dist; + hitSomething = dist != sqrt(vel_x * vel_x + vel_y * vel_y); + } + } + if ( processXYCollision && hitSomething ) { item = newItemFromEntity(my); if ( !item ) @@ -674,12 +1100,57 @@ void actThrown(Entity* my) if ( itemCategory(item) == THROWN || itemCategory(item) == GEM || itemCategory(item) == POTION ) { my->entityCheckIfTriggeredBomb(true); + if ( !hit.entity ) + { + my->entityCheckIfTriggeredWallButton(); + } } bool tryHitEntity = true; if ( itemIsThrowableTinkerTool(item) && !(item->type >= TOOL_BOMB && item->type <= TOOL_TELEPORT_BOMB) ) { tryHitEntity = false; } + if ( item->type == GEM_JEWEL ) + { + tryHitEntity = false; + if ( hit.entity != nullptr ) + { + if ( hit.entity->behavior == &actMonster && hit.entity->getStats() ) + { + Entity* parent = uidToEntity(my->parent); + if ( parent && parent->behavior == &actPlayer ) + { + if ( entityWantsJewel(item->status, *hit.entity, *hit.entity->getStats(), false) ) + { + if ( jewelItemRecruit(parent, hit.entity, item->status, nullptr) ) + { + free(item); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + } + } + } + } + + if ( my->sprite == items[GREASE_BALL].index ) + { + spawnGreasePuddleSpawner(my->parent == 0 ? nullptr : uidToEntity(my->parent), my->x, my->y, 30 * TICKS_PER_SECOND); + if ( hit.entity != nullptr && tryHitEntity ) + { + spawnGreasePuddleSpawner(my->parent == 0 ? nullptr : uidToEntity(my->parent), hit.entity->x, hit.entity->y, 30 * TICKS_PER_SECOND); + } + } + if ( my->sprite == items[DUST_BALL].index ) + { + thrownItemUpdateSpellTrail(*my, my->x, my->y); + if ( hit.entity != nullptr && tryHitEntity ) + { + thrownItemUpdateSpellTrail(*my, hit.entity->x, hit.entity->y); + } + } if ( hit.entity != nullptr && tryHitEntity ) { @@ -694,7 +1165,7 @@ void actThrown(Entity* my) if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(hit.entity) ) + if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) { friendlyHit = true; } @@ -705,13 +1176,15 @@ void actThrown(Entity* my) { item->applyBomb(parent, item->type, Item::ItemBombPlacement::BOMB_CHEST, Item::ItemBombFacingDirection::BOMB_UP, my, hit.entity); free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } - else if ( hit.entity->behavior == &actDoor ) + else if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) { item->applyBomb(parent, item->type, Item::ItemBombPlacement::BOMB_DOOR, Item::ItemBombFacingDirection::BOMB_UP, my, hit.entity); free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -720,6 +1193,7 @@ void actThrown(Entity* my) { item->applyBomb(parent, item->type, Item::ItemBombPlacement::BOMB_COLLIDER, Item::ItemBombFacingDirection::BOMB_UP, my, hit.entity); free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -730,6 +1204,7 @@ void actThrown(Entity* my) int oldHP = 0; oldHP = hit.entity->getHP(); int damage = (BASE_THROWN_DAMAGE + item->weaponGetAttack(parentStats)); + bool thrownTypeWeapon = false; if ( parentStats ) { if ( itemCategory(item) == POTION ) @@ -742,7 +1217,10 @@ void actThrown(Entity* my) } else { - if ( itemCategory(item) == THROWN ) + if ( itemCategory(item) == THROWN + && !(item->type == DUST_BALL + || item->type == SLOP_BALL + || item->type == GREASE_BALL) ) { int enemyAC = AC(hitstats); damage = my->thrownProjectilePower; @@ -761,6 +1239,8 @@ void actThrown(Entity* my) real_t targetACEffectiveness = Entity::getACEffectiveness(hit.entity, hitstats, hit.entity->behavior == &actPlayer, parent, parentStats, numBlessings); int attackAfterReductions = static_cast(std::max(0.0, ((damage * targetACEffectiveness - enemyAC))) + (1.0 - targetACEffectiveness) * damage); damage = attackAfterReductions; + + thrownTypeWeapon = true; } else { @@ -771,6 +1251,15 @@ void actThrown(Entity* my) } damage -= (AC(hit.entity->getStats()) * .5); } + + if ( hitstats && hitstats->getEffectActive(EFF_GUARD_BODY) ) + { + thaumSpellArmorProc(hit.entity, *hitstats, false, parent, EFF_GUARD_BODY); + } + if ( hitstats && hitstats->getEffectActive(EFF_DIVINE_GUARD) ) + { + thaumSpellArmorProc(hit.entity, *hitstats, false, parent, EFF_DIVINE_GUARD); + } } } if ( hitstats && !hitstats->defending ) @@ -796,6 +1285,9 @@ void actThrown(Entity* my) case FOOD_CREAMPIE: damage = 0; break; + case TOOL_DUCK: + damage = 1; + break; default: break; } @@ -810,12 +1302,66 @@ void actThrown(Entity* my) damage = std::min(10, damage); // impact damage is 10 max on allies. } + bool envenomWeapon = false; + if ( parent ) + { + Stat* parentStats = parent->getStats(); + if ( parentStats && parentStats->getEffectActive(EFF_ENVENOM_WEAPON) && hitstats ) + { + if ( local_rng.rand() % 2 == 0 ) + { + int envenomDamage = std::min( + getSpellDamageSecondaryFromID(SPELL_ENVENOM_WEAPON, parent, parentStats, parent), + getSpellDamageFromID(SPELL_ENVENOM_WEAPON, parent, parentStats, parent)); + + hit.entity->modHP(-envenomDamage); // do the damage + for ( int tmp = 0; tmp < 3; ++tmp ) + { + Entity* gib = spawnGib(hit.entity, 211); + serverSpawnGibForClient(gib); + } + if ( !hitstats->getEffectActive(EFF_POISONED) ) + { + envenomWeapon = true; + hitstats->setEffectActive(EFF_POISONED, 1); + + int duration = 160 * envenomDamage; + hitstats->EFFECTS_TIMERS[EFF_POISONED] = std::max(200, duration - hit.entity->getCON() * 20); + hitstats->poisonKiller = parent->getUID(); + if ( hit.entity->isEntityPlayer() ) + { + messagePlayerMonsterEvent(hit.entity->isEntityPlayer(), makeColorRGB(255, 0, 0), *parentStats, Language::get(6531), Language::get(6532), MSG_COMBAT); + serverUpdateEffects(hit.entity->isEntityPlayer()); + } + + if ( parent->behavior == &actPlayer ) + { + players[parent->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_ENVENOM_WEAPON, 50.0, 1.0, hit.entity); + } + } + } + } + } + char whatever[256] = ""; if ( !friendlyHit ) { Sint32 oldHP = hitstats->HP; hit.entity->modHP(-damage); + if ( hitstats ) + { + Sint32 damageTaken = oldHP - hitstats->HP; + if ( damageTaken > 0 ) + { + if ( hitstats->getEffectActive(EFF_DEFY_FLESH) ) + { + hit.entity->defyFleshProc(parent); + } + hit.entity->pinpointDamageProc(parent, damageTaken); + } + } + if ( hit.entity->behavior == &actPlayer ) { Compendium_t::Events_t::eventUpdateCodex(hit.entity->skill[2], Compendium_t::CPDM_HP_MOST_DMG_LOST_ONE_HIT, "hp", oldHP - hitstats->HP); @@ -861,7 +1407,8 @@ void actThrown(Entity* my) bool ignorePotion = false; bool wasPotion = itemCategory(item) == POTION; ItemType itemType = item->type; - bool wasConfused = (hitstats && hitstats->EFFECTS[EFF_CONFUSED]); + bool wasConfused = (hitstats && hitstats->getEffectActive(EFF_CONFUSED)); + Uint32 prevTarget = hit.entity->behavior == &actMonster ? hit.entity->monsterTarget : 0; bool healingPotion = false; if ( hitstats ) @@ -879,11 +1426,11 @@ void actThrown(Entity* my) case POTION_ICESTORM: case POTION_THUNDERSTORM: case POTION_POLYMORPH: + case POTION_WATER: ignorePotion = false; break; case POTION_EXTRAHEALING: case POTION_HEALING: - case POTION_WATER: case POTION_BOOZE: case POTION_JUICE: case POTION_CONFUSION: @@ -895,6 +1442,7 @@ void actThrown(Entity* my) case POTION_STRENGTH: case POTION_PARALYSIS: case FOOD_CREAMPIE: + case TOOL_DUCK: ignorePotion = true; break; default: @@ -947,7 +1495,7 @@ void actThrown(Entity* my) break; case POTION_BOOZE: item_PotionBooze(item, hit.entity, parent); - if ( parentStats && parentStats->EFFECTS[EFF_DRUNK] ) + if ( parentStats && parentStats->getEffectActive(EFF_DRUNK) ) { steamAchievementEntity(parent, "BARONY_ACH_CHEERS"); if ( hit.entity->behavior == &actMonster && parent && parent->behavior == &actPlayer ) @@ -1108,6 +1656,128 @@ void actThrown(Entity* my) item_PotionParalysis(item, hit.entity, parent); usedpotion = true; break; + case GREASE_BALL: + item_PotionGrease(item, hit.entity, parent); + //usedpotion = true; + break; + case DUST_BALL: + if ( hit.entity->setEffect(EFF_DUSTED, true, 5 * TICKS_PER_SECOND + 10, true) ) + { + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_STATUS, makeColorRGB(255, 0, 0), Language::get(6752)); + } + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6762), Language::get(6761), MSG_COMBAT); + } + if ( hit.entity->behavior == &actMonster ) + { + disableAlertBlindStatus = true; // don't aggro target. + } + } + break; + case SLOP_BALL: + { + bool wasBlind = false; + if ( hitstats ) + { + wasBlind = hitstats->getEffectActive(EFF_BLIND) > 0; + } + if ( hit.entity->setEffect(EFF_BLIND, true, 5 * TICKS_PER_SECOND, false) ) + { + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_STATUS, makeColorRGB(255, 0, 0), Language::get(6990)); + } + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(3878), Language::get(3879), MSG_COMBAT); + if ( !wasBlind ) + { + achievementObserver.addEntityAchievementTimer(parent, AchievementObserver::BARONY_ACH_FOOD_FIGHT, 5 * TICKS_PER_SECOND, false, 1); + achievementObserver.awardAchievementIfActive(parent->skill[2], parent, AchievementObserver::BARONY_ACH_FOOD_FIGHT); + } + } + if ( hit.entity->behavior == &actMonster ) + { + disableAlertBlindStatus = true; // don't aggro target. + } + } + break; + } + case BOLAS: + { + int duration = 3 * TICKS_PER_SECOND; + if ( parent && parentStats ) + { + duration += statGetPER(parentStats, parent) * 5; + duration += statGetDEX(parentStats, parent) * 5; + duration -= statGetSTR(hitstats, hit.entity) * 5; + duration -= statGetDEX(hitstats, hit.entity) * 5; + duration = std::max(1 * TICKS_PER_SECOND, duration); + if ( parent->behavior == &actPlayer ) + { + real_t charge = my->thrownProjectileCharge / 15.0; // 0-1 + duration *= (0.25 + 1.25 * charge); // 0.25-1.5 + } + } + if ( hit.entity->setEffect(EFF_ROOTED, true, duration, false) ) + { + achievementObserver.addEntityAchievementTimer(hit.entity, AchievementObserver::BARONY_ACH_THATS_A_WRAP, duration, true, 0); + + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_STATUS, makeColorRGB(255, 0, 0), Language::get(6763)); + } + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6764), Language::get(6765), MSG_COMBAT); + } + playSoundEntity(hit.entity, 763, 128); + } + else + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6766), Language::get(6767), MSG_COMBAT); + } + } + break; + } + case TOOL_DUCK: + { + // set disoriented and start a cooldown on being distracted. + if ( hit.entity->behavior == &actMonster && hitstats + && !hitstats->getEffectActive(EFF_DISORIENTED) + && !hitstats->getEffectActive(EFF_DISTRACTED_COOLDOWN) ) + { + if ( hit.entity->monsterReleaseAttackTarget() ) + { + hit.entity->monsterLookDir = hit.entity->yaw; + hit.entity->monsterLookDir += (PI - PI / 4 + (local_rng.rand() % 10) * PI / 40); + if ( hit.entity->monsterState == MONSTER_STATE_WAIT || hit.entity->monsterTarget == 0 ) + { + // not attacking, duration longer. + hit.entity->setEffect(EFF_DISORIENTED, true, TICKS_PER_SECOND * 2, false); + hit.entity->setEffect(EFF_DISTRACTED_COOLDOWN, true, TICKS_PER_SECOND * 3, false); + } + else + { + hit.entity->setEffect(EFF_DISORIENTED, true, TICKS_PER_SECOND * 1, false); + hit.entity->setEffect(EFF_DISTRACTED_COOLDOWN, true, TICKS_PER_SECOND * 3, false); + } + } + spawnFloatingSpriteMisc(134, hit.entity->x + (-4 + local_rng.rand() % 9) + cos(hit.entity->yaw) * 2, + hit.entity->y + (-4 + local_rng.rand() % 9) + sin(hit.entity->yaw) * 2, hit.entity->z + local_rng.rand() % 4); + } + usedpotion = true; + break; + } case FOOD_CREAMPIE: { skipMessage = true; @@ -1245,7 +1915,9 @@ void actThrown(Entity* my) if ( friendlyHit && !usedpotion ) { - if ( item && itemCategory(item) != POTION && item->type != BOOMERANG ) + if ( item && itemCategory(item) != POTION && item->type != BOOMERANG + && item->type != GREASE_BALL && item->type != DUST_BALL + && item->type != SLOP_BALL ) { Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. entity->flags[INVISIBLE] = true; @@ -1269,13 +1941,28 @@ void actThrown(Entity* my) entity->skill[13] = item->count; entity->skill[14] = item->appearance; entity->skill[15] = item->identified; + if ( item->type == GEM_JEWEL ) + { + entity->parent = my->parent; + } if ( itemCategory(item) == THROWN ) { //Hack to make monsters stop catching your shurikens and chakrams. entity->parent = my->parent; + if ( parent ) + { + if ( Stat* parentStats = parent->getStats() ) + { + if ( parentStats->getEffectActive(EFF_RETURN_ITEM) ) + { + entity->itemReturnUID = parent->getUID(); + } + } + } } } free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -1316,10 +2003,25 @@ void actThrown(Entity* my) doSkillIncrease = false; // no skill for killing/hurting other turrets. } } + else if ( hit.entity->behavior == &actMonster + && (hit.entity->monsterAllyGetPlayerLeader() || (hitstats && achievementObserver.checkUidIsFromPlayer(hitstats->leader_uid) >= 0)) + && parent && parent->behavior == &actPlayer ) + { + doSkillIncrease = false; // no level up on allies + } if ( hit.entity->behavior == &actPlayer && parent && parent->behavior == &actPlayer ) { doSkillIncrease = false; // no skill for killing/hurting players } + + if ( doSkillIncrease && parent && parent->behavior == &actPlayer ) + { + if ( parent->isInvisible() && parent->checkEnemy(hit.entity) ) + { + players[parent->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, 10.0, 1.0, hit.entity); + } + } + int chance = 5; if ( doSkillIncrease && (local_rng.rand() % chance == 0) && parent && parent->getStats() ) { @@ -1332,7 +2034,10 @@ void actThrown(Entity* my) } else { - if ( cat == THROWN ) + if ( cat == THROWN + && !(item && (item->type == GREASE_BALL + || item->type == DUST_BALL + || item->type == SLOP_BALL)) ) { playSoundEntity(hit.entity, 66, 64); //*tink* } @@ -1383,10 +2088,10 @@ void actThrown(Entity* my) bool doAlert = true; // fix for confuse potion aggro'ing monsters on impact. - if ( !wasConfused && hitstats && hitstats->EFFECTS[EFF_CONFUSED] && hit.entity->behavior == &actMonster && parent ) + if ( !wasConfused && hitstats && hitstats->getEffectActive(EFF_CONFUSED) && hit.entity->behavior == &actMonster && parent ) { doAlert = false; - if ( hit.entity->monsterTarget == parent->getUID() ) + if ( hit.entity->monsterTarget == parent->getUID() || prevTarget == parent->getUID() ) { hit.entity->monsterReleaseAttackTarget(); } @@ -1395,21 +2100,13 @@ void actThrown(Entity* my) // alert the monster if ( hit.entity->behavior == &actMonster && hitstats && parent != nullptr && doAlert ) { - bool alertTarget = true; + bool alertTarget = hit.entity->monsterAlertBeforeHit(parent); bool targetHealed = false; - if ( parent->behavior == &actMonster && parent->monsterAllyIndex != -1 ) - { - if ( hit.entity->behavior == &actMonster && hit.entity->monsterAllyIndex != -1 ) - { - // if a player ally + hit another ally, don't aggro back - alertTarget = false; - } - } if ( disableAlertBlindStatus ) { alertTarget = false; - if ( hitstats->EFFECTS[EFF_BLIND] ) + if ( hitstats->getEffectActive(EFF_BLIND) || hitstats->getEffectActive(EFF_DUSTED) ) { hit.entity->monsterReleaseAttackTarget(); } @@ -1513,6 +2210,11 @@ void actThrown(Entity* my) messagePlayerColor(hit.entity->skill[2], MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6088)); } } + if ( parent && parent->behavior == &actPlayer && envenomWeapon && hitstats && hitstats->HP > 0 ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6533), Language::get(6534), MSG_COMBAT); + } } else { @@ -1624,8 +2326,11 @@ void actThrown(Entity* my) hit.entity->colliderKillerUid = parent ? parent->getUID() : 0; if ( parent && parent->behavior == &actPlayer ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(hit.entity->getColliderOnBreakLangEntry()), - Language::get(hit.entity->getColliderLangName())); + if ( hit.entity->getColliderOnBreakLangEntry() != 0 ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(hit.entity->getColliderOnBreakLangEntry()), + Language::get(hit.entity->getColliderLangName())); + } if ( hit.entity->isColliderWall() ) { Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_BARRIER_DESTROYED, "breakable barriers", 1); @@ -1701,6 +2406,7 @@ void actThrown(Entity* my) } } free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; break; @@ -1716,10 +2422,12 @@ void actThrown(Entity* my) { free(item); } + onThrownLandingParticle(my); + my->removeLightField(); list_RemoveNode(my->mynode); return; } - else if ( item->type == FOOD_CREAMPIE ) + else if ( item && item->type == FOOD_CREAMPIE ) { if ( !usedpotion ) { @@ -1731,9 +2439,32 @@ void actThrown(Entity* my) } free(item); item = nullptr; + my->removeLightField(); list_RemoveNode(my->mynode); return; } + else if ( item && (item->type == DUST_BALL + || item->type == GREASE_BALL + || item->type == SLOP_BALL) ) + { + free(item); + item = nullptr; + onThrownLandingParticle(my); + playSoundEntity(my, 764, 64); + if ( hit.entity && + (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) ) + { + // become passable, go through creatures + //my->flags[NOCLIP_CREATURES] = true; + my->collisionIgnoreTargets.insert(hit.entity->getUID()); + } + else + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } else if ( itemCategory(item) == THROWN && (item->type == STEEL_CHAKRAM || item->type == CRYSTAL_SHURIKEN || (item->type == BOOMERANG && uidToEntity(my->parent))) && hit.entity == NULL ) @@ -1744,6 +2475,7 @@ void actThrown(Entity* my) { // boomerang always tink and return to owner. free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -1752,6 +2484,7 @@ void actThrown(Entity* my) { // boomerang always return to owner. free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -1759,6 +2492,18 @@ void actThrown(Entity* my) { // non-bomb tools will fall to the ground and get placed. } + else if ( item && item->type == GEM_JEWEL ) + { + // will fall to the ground and get placed. + } + else if ( item && item->type == TOOL_DUCK ) + { + item->applyDuck(my->parent, my->x, my->y, hit.entity, false); + free(item); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } else { bool dropItem = true; @@ -1779,6 +2524,21 @@ void actThrown(Entity* my) } } } + else if ( item && item->type == BOLAS ) + { + if ( hit.entity && !hit.entity->isInertMimic() && hit.entity->getStats() ) + { + dropItem = false; + int duration = 3 * TICKS_PER_SECOND; + if ( hit.entity->getStats()->getEffectActive(EFF_ROOTED) ) + { + duration = hit.entity->getStats()->EFFECTS_TIMERS[EFF_ROOTED]; + } + item->ownerUid = parent ? parent->getUID() : 0; + createParticleBolas(hit.entity, 1917, duration, item); + serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_BOLAS, 1917, 0, duration, 0); + } + } if ( dropItem ) { Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. @@ -1803,13 +2563,28 @@ void actThrown(Entity* my) entity->skill[13] = item->count; entity->skill[14] = item->appearance; entity->skill[15] = item->identified; + if ( item->type == GEM_JEWEL ) + { + entity->parent = my->parent; + } if ( itemCategory(item) == THROWN ) { //Hack to make monsters stop catching your shurikens and chakrams. entity->parent = my->parent; + if ( parent ) + { + if ( Stat* parentStats = parent->getStats() ) + { + if ( parentStats->getEffectActive(EFF_RETURN_ITEM) ) + { + entity->itemReturnUID = parent->getUID(); + } + } + } } } free(item); + my->removeLightField(); list_RemoveNode(my->mynode); return; } @@ -1857,3 +2632,46 @@ void actThrown(Entity* my) } } } + +void thrownItemUpdateSpellTrail(Entity& my, real_t _x, real_t _y) +{ + if ( my.sprite == items[DUST_BALL].index ) + { + auto findEffects = particleTimerEffects.find(my.thrownProjectileParticleTimerUID); + if ( findEffects != particleTimerEffects.end() ) + { + if ( auto spellTimer = uidToEntity(my.thrownProjectileParticleTimerUID) ) + { + int x = static_cast(_x) / 16; + int y = static_cast(_y) / 16; + bool freeSpot = true; + Uint32 lastTick = 1; + for ( auto& eff : findEffects->second.effectMap ) + { + if ( static_cast(eff.second.x) / 16 == x + && static_cast(eff.second.y) / 16 == y ) + { + freeSpot = false; + } + lastTick = std::max(eff.first, lastTick); + } + if ( freeSpot ) + { + auto& effect = findEffects->second.effectMap[std::max(spellTimer->ticks + 1, lastTick + 2)]; // insert x ticks beyond last effect + if ( findEffects->second.effectMap.size() == 1 ) + { + effect.firstEffect = true; + } + int spellID = spellTimer->particleTimerVariable2; + auto particleEffectType = (spellID == SPELL_MYCELIUM_BOMB || spellID == SPELL_MYCELIUM_SPORES) + ? ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM + : ParticleTimerEffect_t::EffectType::EFFECT_SPORES; + effect.effectType = particleEffectType; + effect.x = x * 16.0 + 8.0; + effect.y = y * 16.0 + 8.0; + effect.yaw = 0.0; + } + } + } + } +} \ No newline at end of file diff --git a/src/acttorch.cpp b/src/acttorch.cpp index 1c5b6f82d..4fbf1160b 100644 --- a/src/acttorch.cpp +++ b/src/acttorch.cpp @@ -393,7 +393,10 @@ void Entity::actLightSource() if ( !LIGHTSOURCE_LIGHT ) { const auto color = lightSourceBrightness / 255.f; - light = lightSphereShadow(0, x / 16, y / 16, lightSourceRadius, color, color, color, 0.5f); + float r = (color / 255.f) * (float)((lightSourceRGB & 0xFF)); + float g = (color / 255.f) * (float)((lightSourceRGB >> 8) & 0xFF); + float b = (color / 255.f) * (float)((lightSourceRGB >> 16) & 0xFF); + light = lightSphereShadow(0, x / 16, y / 16, lightSourceRadius, r, g, b, 0.f, 0.5f); LIGHTSOURCE_LIGHT = 1; } if ( lightSourceFlicker && flickerLights ) @@ -406,7 +409,10 @@ void Entity::actLightSource() if ( !light ) { const auto color = lightSourceBrightness / 255.f; - light = lightSphereShadow(0, x / 16, y / 16, lightSourceRadius, color, color, color, 0.5f); + float r = (color / 255.f) * (float)((lightSourceRGB & 0xFF)); + float g = (color / 255.f) * (float)((lightSourceRGB >> 8) & 0xFF); + float b = (color / 255.f) * (float)((lightSourceRGB >> 16) & 0xFF); + light = lightSphereShadow(0, x / 16, y / 16, lightSourceRadius, r, g, b, 0.f, 0.5f); } } @@ -418,14 +424,20 @@ void Entity::actLightSource() { removeLightField(); const auto color = lightSourceBrightness / 255.f; - light = lightSphereShadow(0, x / 16, y / 16, lightSourceRadius, color, color, color, 0.5f); + float r = (color / 255.f) * (float)((lightSourceRGB & 0xFF)); + float g = (color / 255.f) * (float)((lightSourceRGB >> 8) & 0xFF); + float b = (color / 255.f) * (float)((lightSourceRGB >> 16) & 0xFF); + light = lightSphereShadow(0, x / 16, y / 16, lightSourceRadius, r, g, b, 0.f, 0.5f); } else { removeLightField(); const auto brightness = std::max(lightSourceBrightness - 16, 0); - const auto color = lightSourceBrightness / 255.f; - light = lightSphereShadow(0, x / 16, y / 16, lightSourceRadius, color, color, color, 0.5f); + const auto color = brightness / 255.f; + float r = (color / 255.f) * (float)((lightSourceRGB & 0xFF)); + float g = (color / 255.f) * (float)((lightSourceRGB >> 8) & 0xFF); + float b = (color / 255.f) * (float)((lightSourceRGB >> 16) & 0xFF); + light = lightSphereShadow(0, x / 16, y / 16, lightSourceRadius, r, g, b, 0.f, 0.5f); } LIGHTSOURCE_FLICKER = 2 + local_rng.rand() % 7; } diff --git a/src/book.cpp b/src/book.cpp index af0b73f3e..506a38df6 100644 --- a/src/book.cpp +++ b/src/book.cpp @@ -122,7 +122,7 @@ bool BookParser_t::readCompiledBooks() File* fp = FileIO::open(compiledBooksPath.c_str(), "rb"); if ( fp ) { - char buf[MAX_FILE_LENGTH]; + static char buf[MAX_FILE_LENGTH]; int count = fp->read(buf, sizeof(buf[0]), sizeof(buf)); buf[count] = '\0'; rapidjson::StringStream is(buf); @@ -170,7 +170,7 @@ bool BookParser_t::booksRequireCompiling() File* fp = FileIO::open(compiledBooksPath.c_str(), "rb"); if ( fp ) { - char buf[MAX_FILE_LENGTH]; + static char buf[MAX_FILE_LENGTH]; int count = fp->read(buf, sizeof(buf[0]), sizeof(buf)); buf[count] = '\0'; rapidjson::StringStream is(buf); @@ -335,7 +335,12 @@ void BookParser_t::readBooksIntoTemp() char bookChar[PATH_MAX]; strncpy(bookChar, bookPath.c_str(), PATH_MAX - 1); - entry = readFile(bookChar); + char* tmp = readFile(bookChar); + if ( tmp ) + { + entry = tmp; + } + free(tmp); } } else @@ -584,7 +589,12 @@ void BookParser_t::createBook(std::string filename) char bookChar[PATH_MAX]; strncpy(bookChar, tempstr.c_str(), PATH_MAX - 1); - newBook.text = readFile(bookChar); + char* tmp = readFile(bookChar); + if ( tmp ) + { + newBook.text = tmp; + } + free(tmp); if ( newBook.text == "" ) { printlog( "error opening book \"%s\".\n", tempstr.c_str()); diff --git a/src/buttons.cpp b/src/buttons.cpp index 4d41e9fbb..788dfc42c 100644 --- a/src/buttons.cpp +++ b/src/buttons.cpp @@ -1753,6 +1753,95 @@ void buttonCloseSubwindow(button_t* my) strcpy(filename, oldfilename); } +void buttonOpenNextMap(button_t* my) +{ + buttonCloseSubwindow(my); + + updateMapNames(); + + std::string searchStr = filename; + auto find = searchStr.find(".lmp"); + if ( find == std::string::npos ) + { + searchStr += ".lmp"; + } + + for ( auto it = mapNames.begin(); it != mapNames.end(); ) + { + if ( (*it) == searchStr ) + { + ++it; + if ( it != mapNames.end() ) + { + if ( it->size() > 0 && it->front() != '.' ) + { + std::string f = *it; + auto find = f.find(".lmp"); + if ( find != std::string::npos ) + { + f.erase(find, strlen(".lmp")); + } + strcpy(filename, f.c_str()); + buttonOpenConfirm(my); + return; + } + } + } + else + { + ++it; + } + } + + strcpy(message, "Unable to open next map, end of list"); + messagetime = 60; // 60*50 ms = 3000 ms (3 seconds) +} + +void buttonOpenPrevMap(button_t* my) +{ + buttonCloseSubwindow(my); + + updateMapNames(); + + std::string searchStr = filename; + auto find = searchStr.find(".lmp"); + if ( find == std::string::npos ) + { + searchStr += ".lmp"; + } + + for ( auto it = mapNames.rbegin(); it != mapNames.rend(); ) + { + if ( (*it) == searchStr ) + { + ++it; + if ( it != mapNames.rend() ) + { + if ( it->size() > 0 && it->front() != '.' ) + { + std::string f = *it; + auto find = f.find(".lmp"); + if ( find != std::string::npos ) + { + f.erase(find, strlen(".lmp")); + } + strcpy(filename, f.c_str()); + buttonOpenConfirm(my); + return; + } + } + break; + } + else + { + ++it; + } + } + + strcpy(message, "Unable to open prev map, first of list"); + messagetime = 60; // 60*50 ms = 3000 ms (3 seconds) +} + void buttonSpriteProperties(button_t* my) { button_t* button; @@ -2004,6 +2093,7 @@ void buttonSpriteProperties(button_t* my) snprintf(spriteProperties[2], 5, "%d", static_cast(selectedEntity[0]->floorDecorationHeightOffset)); snprintf(spriteProperties[3], 5, "%d", static_cast(selectedEntity[0]->floorDecorationXOffset)); snprintf(spriteProperties[4], 5, "%d", static_cast(selectedEntity[0]->floorDecorationYOffset)); + snprintf(spriteProperties[5], 5, "%d", static_cast(selectedEntity[0]->floorDecorationDestroyIfNoWall)); char buf[256] = ""; int totalChars = 0; for ( int i = 8; i < 60; ++i ) @@ -2029,10 +2119,10 @@ void buttonSpriteProperties(button_t* my) { buf[totalChars] = '\0'; } - strncpy(spriteProperties[5], buf, 48); - strncpy(spriteProperties[6], buf + 48, 48); - strncpy(spriteProperties[7], buf + 96, 48); - strncpy(spriteProperties[8], buf + 144, 48); + strncpy(spriteProperties[6], buf, 48); + strncpy(spriteProperties[7], buf + 48, 48); + strncpy(spriteProperties[8], buf + 96, 48); + strncpy(spriteProperties[9], buf + 144, 48); inputstr = spriteProperties[0]; cursorflash = ticks; menuVisible = 0; @@ -2040,8 +2130,8 @@ void buttonSpriteProperties(button_t* my) newwindow = 15; subx1 = xres / 2 - 200; subx2 = xres / 2 + 200; - suby1 = yres / 2 - 180; - suby2 = yres / 2 + 180; + suby1 = yres / 2 - 190; + suby2 = yres / 2 + 190; strcpy(subtext, "Decoration Model Properties:"); break; } @@ -2070,6 +2160,9 @@ void buttonSpriteProperties(button_t* my) snprintf(spriteProperties[4], 3, "%d", static_cast(selectedEntity[0]->lightSourceRadius)); snprintf(spriteProperties[5], 2, "%d", static_cast(selectedEntity[0]->lightSourceFlicker)); snprintf(spriteProperties[6], 5, "%d", static_cast(selectedEntity[0]->lightSourceDelay)); + snprintf(spriteProperties[7], 4, "%d", static_cast((selectedEntity[0]->lightSourceRGB & 0xFF))); + snprintf(spriteProperties[8], 4, "%d", static_cast((selectedEntity[0]->lightSourceRGB >> 8) & 0xFF)); + snprintf(spriteProperties[9], 4, "%d", static_cast((selectedEntity[0]->lightSourceRGB >> 16) & 0xFF)); inputstr = spriteProperties[0]; cursorflash = ticks; menuVisible = 0; @@ -2280,6 +2373,22 @@ void buttonSpriteProperties(button_t* my) suby2 = yres / 2 + 100; strcpy(subtext, "Door Properties:"); break; + case 32: + snprintf(spriteProperties[0], 4, "%d", static_cast(selectedEntity[0]->doorUnlockWhenPowered)); + snprintf(spriteProperties[1], 4, "%d", static_cast(selectedEntity[0]->doorDisableLockpicks)); + snprintf(spriteProperties[2], 4, "%d", static_cast(selectedEntity[0]->doorDisableOpening)); + snprintf(spriteProperties[3], 4, "%d", static_cast(selectedEntity[0]->doorForceLockedUnlocked)); + inputstr = spriteProperties[0]; + cursorflash = ticks; + menuVisible = 0; + subwindow = 1; + newwindow = 36; + subx1 = xres / 2 - 170; + subx2 = xres / 2 + 170; + suby1 = yres / 2 - 100; + suby2 = yres / 2 + 100; + strcpy(subtext, "Iron Door Properties:"); + break; case 22: snprintf(spriteProperties[0], 4, "%d", static_cast(selectedEntity[0]->gateDisableOpening)); inputstr = spriteProperties[0]; @@ -2407,6 +2516,51 @@ void buttonSpriteProperties(button_t* my) suby2 = yres / 2 + 60; strcpy(subtext, "Pressure Plate Properties:"); break; + case 30: + snprintf(spriteProperties[0], 2, "%d", static_cast(selectedEntity[0]->wallLockMaterial)); + snprintf(spriteProperties[1], 2, "%d", static_cast(selectedEntity[0]->wallLockInvertPower)); + snprintf(spriteProperties[2], 2, "%d", static_cast(selectedEntity[0]->wallLockTurnable)); + snprintf(spriteProperties[3], 4, "%d", static_cast(selectedEntity[0]->wallLockPickable)); + snprintf(spriteProperties[4], 2, "%d", static_cast(selectedEntity[0]->wallLockPickableSkeletonKey)); + snprintf(spriteProperties[5], 2, "%d", static_cast(selectedEntity[0]->wallLockAutoGenKey)); + inputstr = spriteProperties[0]; + cursorflash = ticks; + menuVisible = 0; + subwindow = 1; + newwindow = 34; + subx1 = xres / 2 - 170; + subx2 = xres / 2 + 170; + suby1 = yres / 2 - 120; + suby2 = yres / 2 + 120; + strcpy(subtext, "Wall Lock Properties:"); + break; + case 31: + snprintf(spriteProperties[0], 2, "%d", static_cast(selectedEntity[0]->wallLockInvertPower)); + snprintf(spriteProperties[1], 4, "%d", static_cast(selectedEntity[0]->wallLockTimer)); + inputstr = spriteProperties[0]; + cursorflash = ticks; + menuVisible = 0; + subwindow = 1; + newwindow = 35; + subx1 = xres / 2 - 170; + subx2 = xres / 2 + 170; + suby1 = yres / 2 - 90; + suby2 = yres / 2 + 90; + strcpy(subtext, "Wall Button Properties:"); + break; + case 33: + snprintf(spriteProperties[0], 2, "%d", static_cast(selectedEntity[0]->skill[0])); + inputstr = spriteProperties[0]; + cursorflash = ticks; + menuVisible = 0; + subwindow = 1; + newwindow = 37; + subx1 = xres / 2 - 170; + subx2 = xres / 2 + 170; + suby1 = yres / 2 - 90; + suby2 = yres / 2 + 90; + strcpy(subtext, "Wind Properties:"); + break; default: strcpy(message, "No properties available for current sprite."); messagetime = 60; @@ -3270,6 +3424,7 @@ void buttonSpritePropertiesConfirm(button_t* my) selectedEntity[0]->floorDecorationHeightOffset = (Sint32)atoi(spriteProperties[2]); selectedEntity[0]->floorDecorationXOffset = (Sint32)atoi(spriteProperties[3]); selectedEntity[0]->floorDecorationYOffset = (Sint32)atoi(spriteProperties[4]); + selectedEntity[0]->floorDecorationDestroyIfNoWall = (Sint32)atoi(spriteProperties[5]); int totalChars = 0; char checkChr = 'a'; @@ -3288,23 +3443,23 @@ void buttonSpritePropertiesConfirm(button_t* my) { if ( totalChars >= 144 ) { - selectedEntity[0]->skill[i] |= (spriteProperties[8][totalChars - 144]) << (c * 8); - checkChr = spriteProperties[8][totalChars - 144]; + selectedEntity[0]->skill[i] |= (spriteProperties[9][totalChars - 144]) << (c * 8); + checkChr = spriteProperties[9][totalChars - 144]; } else if ( totalChars >= 96 ) { - selectedEntity[0]->skill[i] |= (spriteProperties[7][totalChars - 96]) << (c * 8); - checkChr = spriteProperties[7][totalChars - 96]; + selectedEntity[0]->skill[i] |= (spriteProperties[8][totalChars - 96]) << (c * 8); + checkChr = spriteProperties[8][totalChars - 96]; } else if ( totalChars >= 48 ) { - selectedEntity[0]->skill[i] |= (spriteProperties[6][totalChars - 48]) << (c * 8); - checkChr = spriteProperties[6][totalChars - 48]; + selectedEntity[0]->skill[i] |= (spriteProperties[7][totalChars - 48]) << (c * 8); + checkChr = spriteProperties[7][totalChars - 48]; } else { - selectedEntity[0]->skill[i] |= (spriteProperties[5][totalChars]) << (c * 8); - checkChr = spriteProperties[5][totalChars]; + selectedEntity[0]->skill[i] |= (spriteProperties[6][totalChars]) << (c * 8); + checkChr = spriteProperties[6][totalChars]; } if ( checkChr == '\0' ) { @@ -3333,6 +3488,10 @@ void buttonSpritePropertiesConfirm(button_t* my) selectedEntity[0]->lightSourceRadius = (Sint32)atoi(spriteProperties[4]); selectedEntity[0]->lightSourceFlicker = (Sint32)atoi(spriteProperties[5]); selectedEntity[0]->lightSourceDelay = (Sint32)atoi(spriteProperties[6]); + selectedEntity[0]->lightSourceRGB = 0; + selectedEntity[0]->lightSourceRGB |= std::max(0, std::min(255, (Sint32)atoi(spriteProperties[7]))); + selectedEntity[0]->lightSourceRGB |= std::max(0, std::min(255, (Sint32)atoi(spriteProperties[8]))) << 8; + selectedEntity[0]->lightSourceRGB |= std::max(0, std::min(255, (Sint32)atoi(spriteProperties[9]))) << 16; break; case 16: // text source { @@ -3482,6 +3641,12 @@ void buttonSpritePropertiesConfirm(button_t* my) selectedEntity[0]->doorDisableLockpicks = (Sint32)atoi(spriteProperties[1]); selectedEntity[0]->doorDisableOpening = (Sint32)atoi(spriteProperties[2]); break; + case 32: + selectedEntity[0]->doorUnlockWhenPowered = (Sint32)atoi(spriteProperties[0]); + selectedEntity[0]->doorDisableLockpicks = (Sint32)atoi(spriteProperties[1]); + selectedEntity[0]->doorDisableOpening = (Sint32)atoi(spriteProperties[2]); + selectedEntity[0]->doorForceLockedUnlocked = (Sint32)atoi(spriteProperties[3]); + break; case 22: // gates selectedEntity[0]->gateDisableOpening = (Sint32)atoi(spriteProperties[0]); break; @@ -3526,6 +3691,21 @@ void buttonSpritePropertiesConfirm(button_t* my) case 29: // pressure plate selectedEntity[0]->pressurePlateTriggerType = (Sint32)atoi(spriteProperties[0]); break; + case 30: + selectedEntity[0]->wallLockMaterial = (Sint32)atoi(spriteProperties[0]); + selectedEntity[0]->wallLockInvertPower = (Sint32)atoi(spriteProperties[1]); + selectedEntity[0]->wallLockTurnable = (Sint32)atoi(spriteProperties[2]); + selectedEntity[0]->wallLockPickable = (Sint32)atoi(spriteProperties[3]); + selectedEntity[0]->wallLockPickableSkeletonKey = (Sint32)atoi(spriteProperties[4]); + selectedEntity[0]->wallLockAutoGenKey = (Sint32)atoi(spriteProperties[5]); + break; + case 31: + selectedEntity[0]->wallLockInvertPower = (Sint32)atoi(spriteProperties[0]); + selectedEntity[0]->wallLockTimer = (Sint32)atoi(spriteProperties[1]); + break; + case 33: + selectedEntity[0]->skill[0] = (Sint32)atoi(spriteProperties[0]); + break; default: break; } diff --git a/src/charclass.cpp b/src/charclass.cpp index 9d093cb7c..02b025d3b 100644 --- a/src/charclass.cpp +++ b/src/charclass.cpp @@ -50,7 +50,7 @@ void initClassStats(const int classnum, void* myStats) stat->MP -= 10; // skills - stat->setProficiency(PRO_SWIMMING, 25); + //stat->setProficiency(PRO_LEGACY_SWIMMING, 25); stat->setProficiency(PRO_SHIELD, 25); stat->setProficiency(PRO_AXE, 50); stat->setProficiency(PRO_MACE, 25); @@ -94,9 +94,11 @@ void initClassStats(const int classnum, void* myStats) stat->MP += 10; // skills - stat->setProficiency(PRO_SPELLCASTING, 50); - stat->setProficiency(PRO_MAGIC, 25); - stat->setProficiency(PRO_SWIMMING, 25); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 50); + //stat->setProficiency(PRO_LEGACY_MAGIC, 25); + //stat->setProficiency(PRO_LEGACY_SWIMMING, 25); + stat->setProficiency(PRO_THAUMATURGY, 50); + stat->setProficiency(PRO_MYSTICISM, 25); stat->setProficiency(PRO_POLEARM, 25); stat->setProficiency(PRO_ALCHEMY, 30); stat->setProficiency(PRO_APPRAISAL, 10); @@ -142,7 +144,7 @@ void initClassStats(const int classnum, void* myStats) // skills stat->setProficiency(PRO_STEALTH, 25); - stat->setProficiency(PRO_SWIMMING, 50); + //stat->setProficiency(PRO_LEGACY_SWIMMING, 50); stat->setProficiency(PRO_POLEARM, 25); stat->setProficiency(PRO_RANGED, 25); stat->setProficiency(PRO_TRADING, 25); @@ -160,9 +162,10 @@ void initClassStats(const int classnum, void* myStats) // skills stat->setProficiency(PRO_MACE, 25); - stat->setProficiency(PRO_SWIMMING, 25); - stat->setProficiency(PRO_MAGIC, 25); - stat->setProficiency(PRO_SPELLCASTING, 25); + //stat->setProficiency(PRO_LEGACY_SWIMMING, 25); + //stat->setProficiency(PRO_LEGACY_MAGIC, 25); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 25); + stat->setProficiency(PRO_THAUMATURGY, 40); stat->setProficiency(PRO_LEADERSHIP, 20); stat->setProficiency(PRO_ALCHEMY, 20); stat->setProficiency(PRO_SHIELD, 10); @@ -204,8 +207,10 @@ void initClassStats(const int classnum, void* myStats) // skills stat->setProficiency(PRO_POLEARM, 25); - stat->setProficiency(PRO_SPELLCASTING, 50); - stat->setProficiency(PRO_MAGIC, 50); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 50); + //stat->setProficiency(PRO_LEGACY_MAGIC, 50); + stat->setProficiency(PRO_SORCERY, 50); + stat->setProficiency(PRO_THAUMATURGY, 15); stat->setProficiency(PRO_ALCHEMY, 10); stat->setProficiency(PRO_APPRAISAL, 10); } @@ -226,8 +231,10 @@ void initClassStats(const int classnum, void* myStats) stat->MP += 10; // skills - stat->setProficiency(PRO_MAGIC, 25); - stat->setProficiency(PRO_SPELLCASTING, 50); + //stat->setProficiency(PRO_LEGACY_MAGIC, 25); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 50); + stat->setProficiency(PRO_SORCERY, 30); + //stat->setProficiency(PRO_MYSTICISM, 20); stat->setProficiency(PRO_STEALTH, 25); stat->setProficiency(PRO_LOCKPICKING, 25); stat->setProficiency(PRO_RANGED, 25); @@ -246,8 +253,10 @@ void initClassStats(const int classnum, void* myStats) stat->setProficiency(PRO_LOCKPICKING, 25); stat->setProficiency(PRO_TRADING, 25); stat->setProficiency(PRO_LEADERSHIP, 20); - stat->setProficiency(PRO_MAGIC, 25); - stat->setProficiency(PRO_SPELLCASTING, 25); + //stat->setProficiency(PRO_LEGACY_MAGIC, 25); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 25); + stat->setProficiency(PRO_MYSTICISM, 25); + stat->setProficiency(PRO_SORCERY, 10); stat->setProficiency(PRO_ALCHEMY, 10); stat->setProficiency(PRO_RANGED, 20); stat->setProficiency(PRO_STEALTH, 10); @@ -268,8 +277,9 @@ void initClassStats(const int classnum, void* myStats) stat->setProficiency(PRO_MACE, 10); stat->setProficiency(PRO_SHIELD, 10); stat->setProficiency(PRO_STEALTH, 40); - stat->setProficiency(PRO_SPELLCASTING, 40); - stat->setProficiency(PRO_MAGIC, 40); + stat->setProficiency(PRO_MYSTICISM, 40); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 40); + //stat->setProficiency(PRO_LEGACY_MAGIC, 40); stat->setProficiency(PRO_RANGED, 20); stat->setProficiency(PRO_ALCHEMY, 20); } @@ -286,6 +296,9 @@ void initClassStats(const int classnum, void* myStats) stat->MAXHP += 5; stat->HP += 5; + stat->MAXMP -= 10; + stat->MP -= 10; + // skills stat->setProficiency(PRO_STEALTH, 60); stat->setProficiency(PRO_SWORD, 60); @@ -305,12 +318,13 @@ void initClassStats(const int classnum, void* myStats) // skills stat->setProficiency(PRO_SHIELD, 40); - stat->setProficiency(PRO_SPELLCASTING, 20); stat->setProficiency(PRO_LEADERSHIP, 10); - stat->setProficiency(PRO_MAGIC, 10); + stat->setProficiency(PRO_THAUMATURGY, 25); + //stat->setProficiency(PRO_LEGACY_MAGIC, 10); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 20); stat->setProficiency(PRO_UNARMED, 50); stat->setProficiency(PRO_ALCHEMY, 20); - stat->setProficiency(PRO_SWIMMING, 10); + //stat->setProficiency(PRO_LEGACY_SWIMMING, 10); } // start DLC else if ( classnum == CLASS_CONJURER ) @@ -327,8 +341,10 @@ void initClassStats(const int classnum, void* myStats) stat->MP += 15; // skills - stat->setProficiency(PRO_MAGIC, 40); - stat->setProficiency(PRO_SPELLCASTING, 40); + //stat->setProficiency(PRO_LEGACY_MAGIC, 40); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 40); + stat->setProficiency(PRO_MYSTICISM, 40); + stat->setProficiency(PRO_THAUMATURGY, 15); stat->setProficiency(PRO_STEALTH, 20); stat->setProficiency(PRO_RANGED, 20); stat->setProficiency(PRO_LEADERSHIP, 40); @@ -337,7 +353,7 @@ void initClassStats(const int classnum, void* myStats) else if ( classnum == CLASS_ACCURSED ) { // attributes - stat->INT += 10; + stat->INT += 5; stat->STR -= 2; stat->CON -= 2; stat->DEX -= 3; @@ -349,8 +365,10 @@ void initClassStats(const int classnum, void* myStats) stat->MP += 10; // skills - stat->setProficiency(PRO_MAGIC, 70); - stat->setProficiency(PRO_SPELLCASTING, 40); + //stat->setProficiency(PRO_LEGACY_MAGIC, 70); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 40); + stat->setProficiency(PRO_SORCERY, 60); + stat->setProficiency(PRO_MYSTICISM, 20); stat->setProficiency(PRO_STEALTH, 40); stat->setProficiency(PRO_APPRAISAL, 20); stat->setProficiency(PRO_UNARMED, 40); @@ -371,8 +389,10 @@ void initClassStats(const int classnum, void* myStats) stat->MP += 10; // skills - stat->setProficiency(PRO_MAGIC, 60); - stat->setProficiency(PRO_SPELLCASTING, 40); + stat->setProficiency(PRO_MYSTICISM, 60); + stat->setProficiency(PRO_SORCERY, 25); + //stat->setProficiency(PRO_LEGACY_MAGIC, 60); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 40); stat->setProficiency(PRO_POLEARM, 20); stat->setProficiency(PRO_LEADERSHIP, 60); } @@ -418,8 +438,9 @@ void initClassStats(const int classnum, void* myStats) stat->MP += 10; // skills - stat->setProficiency(PRO_SPELLCASTING, 40); - stat->setProficiency(PRO_MAGIC, 40); + stat->setProficiency(PRO_MYSTICISM, 40); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 40); + //stat->setProficiency(PRO_LEGACY_MAGIC, 40); stat->setProficiency(PRO_UNARMED, 10); stat->setProficiency(PRO_ALCHEMY, 10); stat->setProficiency(PRO_STEALTH, 10); @@ -443,8 +464,10 @@ void initClassStats(const int classnum, void* myStats) stat->MP += 10;*/ // skills - stat->setProficiency(PRO_SPELLCASTING, 40); - stat->setProficiency(PRO_MAGIC, 20); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 40); + //stat->setProficiency(PRO_LEGACY_MAGIC, 20); + stat->setProficiency(PRO_SORCERY, 20); + stat->setProficiency(PRO_MYSTICISM, 20); stat->setProficiency(PRO_RANGED, 25); stat->setProficiency(PRO_AXE, 25); /*stat->setProficiency(PRO_SHIELD, 40); @@ -468,10 +491,10 @@ void initClassStats(const int classnum, void* myStats) stat->MP -= 10; // skills - stat->setProficiency(PRO_SPELLCASTING, 10); + //stat->setProficiency(PRO_LEGACY_SPELLCASTING, 10); stat->setProficiency(PRO_APPRAISAL, 20); stat->setProficiency(PRO_STEALTH, 25); - stat->setProficiency(PRO_SWIMMING, 50); + //stat->setProficiency(PRO_LEGACY_SWIMMING, 50); stat->setProficiency(PRO_RANGED, 50); stat->setProficiency(PRO_LOCKPICKING, 10); } @@ -496,6 +519,118 @@ void initClassStats(const int classnum, void* myStats) stat->setProficiency(PRO_ALCHEMY, 10); stat->setProficiency(PRO_TRADING, 10); } + else if ( classnum == CLASS_BARD ) + { + stat->STR += -2; + stat->DEX += 1; + stat->CON += -2; + stat->INT += 0; + stat->PER += 0; + stat->CHR += 3; + + stat->MAXHP -= 5; + stat->HP -= 5; + + stat->MAXMP += 5; + stat->MP += 5; + + stat->GOLD = 200; + + stat->setProficiency(PRO_SWORD, 25); + stat->setProficiency(PRO_APPRAISAL, 50); + stat->setProficiency(PRO_MYSTICISM, 15); + stat->setProficiency(PRO_LEADERSHIP, 15); + stat->setProficiency(PRO_STEALTH, 15); + stat->setProficiency(PRO_TRADING, 15); + } + else if ( classnum == CLASS_SAPPER ) + { + stat->STR += 1; + stat->DEX += 1; + stat->CON += -3; + stat->INT += -2; + stat->PER += 0; + stat->CHR += -3; + + //stat->MAXHP += 5; + //stat->HP += 5; + + stat->MAXMP -= 5; + stat->MP -= 5; + + stat->setProficiency(PRO_MACE, 50); + stat->setProficiency(PRO_SORCERY, 15); + stat->setProficiency(PRO_RANGED, 15); + stat->setProficiency(PRO_STEALTH, 25); + stat->setProficiency(PRO_ALCHEMY, 25); + } + else if ( classnum == CLASS_SCION ) + { + // attributes + stat->STR -= 2; + stat->INT += 3; + stat->PER += 1; + stat->DEX -= 2; + stat->CHR -= 1; + + stat->MAXHP -= 10; + stat->HP -= 10; + stat->MAXMP += 20; + stat->MP += 20; + stat->GOLD = 250; + + // skills + stat->setProficiency(PRO_SORCERY, 40); + stat->setProficiency(PRO_MYSTICISM, 40); + stat->setProficiency(PRO_THAUMATURGY, 40); + stat->setProficiency(PRO_ALCHEMY, 10); + stat->setProficiency(PRO_APPRAISAL, 10); + } + else if ( classnum == CLASS_HERMIT ) + { + // attributes + stat->INT += 1; + stat->CON += 2; + stat->STR -= 2; + stat->DEX -= 2; + stat->CHR += 1; + + //stat->MAXHP -= 10; + //stat->HP -= 10; + //stat->MAXMP += 20; + //stat->MP += 20; + //stat->GOLD = 250; + + // skills + stat->setProficiency(PRO_MYSTICISM, 40); + stat->setProficiency(PRO_SORCERY, 15); + stat->setProficiency(PRO_MACE, 25); + stat->setProficiency(PRO_STEALTH, 25); + stat->setProficiency(PRO_ALCHEMY, 10); + } + else if ( classnum == CLASS_PALADIN ) + { + // attributes + stat->CON += 0; + stat->STR += 1; + stat->DEX -= 1; + stat->INT += 1; + stat->PER -= 1; + stat->CHR += 1; + + //stat->MAXHP -= 10; + //stat->HP -= 10; + stat->MAXMP -= 5; + stat->MP -= 5; + //stat->GOLD = 250; + + // skills + stat->setProficiency(PRO_SWORD, 50); + stat->setProficiency(PRO_THAUMATURGY, 25); + stat->setProficiency(PRO_APPRAISAL, 25); + stat->setProficiency(PRO_SHIELD, 10); + stat->setProficiency(PRO_LEADERSHIP, 10); + } if ( gameModeManager.currentSession.challengeRun.isActive() ) { @@ -1602,19 +1737,6 @@ void initClass(const int player) } free(item); - // spellbook of light - item = newItem(SPELLBOOK_LIGHT, WORN, 0, 1, 7, true, nullptr); - item2 = itemPickup(player, item); - if ( players[player]->hotbar.useHotbarFaceMenu ) - { - hotbar[8].item = item2->uid; - } - else - { - hotbar[9].item = item2->uid; - } - free(item); - // scroll item = newItem(SCROLL_CHARGING, EXCELLENT, 0, 2, 1, true, nullptr); item2 = itemPickup(player, item); @@ -2102,7 +2224,7 @@ void initClass(const int player) if ( isLocalPlayer ) { // slow book - item = newItem(SPELLBOOK_SLOW, WORN, 0, 1, 8, true, nullptr); + item = newItem(SPELLBOOK_POISON, DECREPIT, 0, 1, 8, true, nullptr); item2 = itemPickup(player, item); hotbar[9].item = item2->uid; free(item); @@ -2248,7 +2370,7 @@ void initClass(const int player) if ( isLocalPlayer ) { // spear - item = newItem(IRON_SPEAR, SERVICABLE, curseItems ? -1 : 1, 1, 1, true, nullptr); + item = newItem(MAGICSTAFF_COLD, SERVICABLE, 0, 1, 1, true, nullptr); item2 = itemPickup(player, item); hotbar[1].item = item2->uid; free(item); @@ -2266,10 +2388,10 @@ void initClass(const int player) free(item); // cold spellbook - item = newItem(SPELLBOOK_COLD, SERVICABLE, 0, 1, 4, true, nullptr); + /*item = newItem(SPELLBOOK_COLD, SERVICABLE, 0, 1, 4, true, nullptr); item2 = itemPickup(player, item); hotbar[8].item = item2->uid; - free(item); + free(item);*/ // charm monster spellbook item = newItem(SPELLBOOK_CHARM_MONSTER, DECREPIT, 0, 1, 8, true, nullptr); @@ -2472,7 +2594,7 @@ void initClass(const int player) return; } - item = newItem(TOOL_WHIP, EXCELLENT, 0, 1, 0, true, nullptr); + item = newItem(TOOL_WHIP, WORN, 0, 1, 0, true, nullptr); if ( isLocalPlayer ) { item2 = itemPickup(player, item); @@ -2764,61 +2886,53 @@ void initClass(const int player) free(item); } } + else if ( client_classes[player] == CLASS_BARD ) + { + initClassStats(client_classes[player], stats[player]); - stats[player]->OLDHP = stats[player]->HP; + if ( !isLocalPlayer && multiplayer == CLIENT && intro == false ) { + // don't do anything crazy with items on players we don't own + return; + } - if ( stats[player]->stat_appearance == 0 && stats[player]->playerRace == RACE_GOATMAN ) - { - stats[player]->EFFECTS[EFF_ASLEEP] = true; - stats[player]->EFFECTS_TIMERS[EFF_ASLEEP] = -1; + item = newItem(RAPIER, WORN, 0, 1, 0, true, nullptr); if ( isLocalPlayer ) { - // extra booze for hangover :) - item = newItem(POTION_BOOZE, EXCELLENT, 0, 3, 2, true, nullptr); item2 = itemPickup(player, item); + useItem(item2, player); + hotbar[0].item = item2->uid; free(item); } - } - else - { - stats[player]->EFFECTS[EFF_ASLEEP] = false; - stats[player]->EFFECTS_TIMERS[EFF_ASLEEP] = 0; - } - - if ( stats[player]->stat_appearance == 0 && stats[player]->playerRace == RACE_AUTOMATON ) - { - //stats[player]->HUNGER = 150; - } + else + { + useItem(item, player); + } - if ( stats[player]->stat_appearance == 0 - && client_classes[player] <= CLASS_MONK - && stats[player]->playerRace != RACE_HUMAN ) - { + item = newItem(BANDIT_BREASTPIECE, EXCELLENT, 0, 1, 1, true, nullptr); if ( isLocalPlayer ) { - // bonus polymorph potions - item = newItem(POTION_POLYMORPH, EXCELLENT, 0, 2, 0, true, nullptr); item2 = itemPickup(player, item); + useItem(item2, player); free(item); } - } - if ( stats[player]->stat_appearance == 0 - && client_classes[player] >= CLASS_CONJURER - && client_classes[player] <= CLASS_HUNTER - && stats[player]->playerRace != RACE_HUMAN ) - { + else + { + useItem(item, player); + } + + item = newItem(HAT_PLUMED_CAP, EXCELLENT, 0, 1, 1, true, nullptr); if ( isLocalPlayer ) { - // bonus polymorph potions - item = newItem(POTION_POLYMORPH, EXCELLENT, 0, 3, 0, true, nullptr); item2 = itemPickup(player, item); + useItem(item2, player); free(item); } - } + else + { + useItem(item, player); + } - /*if ( svFlags & SV_FLAG_LIFESAVING ) - { - item = newItem(AMULET_LIFESAVING, WORN, 0, 1, 0, true, nullptr); + item = newItem(LOAFERS, SERVICABLE, 0, 1, 0, true, nullptr); if ( isLocalPlayer ) { item2 = itemPickup(player, item); @@ -2829,10 +2943,8 @@ void initClass(const int player) { useItem(item, player); } - }*/ - if ( gameModeManager.currentSession.challengeRun.isActive(GameModeManager_t::CurrentSession_t::ChallengeRun_t::CHEVENT_BFG) ) - { - item = newItem(HEAVY_CROSSBOW, EXCELLENT, curseItems ? -99 : 99, 1, 0, true, nullptr); + + item = newItem(player % 2 == 0 ? INSTRUMENT_FLUTE : INSTRUMENT_LYRE, EXCELLENT, 0, 1, 0, true, nullptr); if ( isLocalPlayer ) { item2 = itemPickup(player, item); @@ -2843,91 +2955,668 @@ void initClass(const int player) { useItem(item, player); } + + if ( isLocalPlayer ) + { + item = newItem(player % 2 == 1 ? INSTRUMENT_FLUTE : INSTRUMENT_LYRE, EXCELLENT, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(FOOD_FISH, EXCELLENT, 0, 2, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(POTION_RESTOREMAGIC, EXCELLENT, 0, 1, 1, true, nullptr); + item2 = itemPickup(player, item); + free(item); + } } - if ( isLocalPlayer ) + else if ( client_classes[player] == CLASS_SAPPER ) { - if ( stats[player]->playerRace == RACE_VAMPIRE && stats[player]->stat_appearance == 0 ) - { - addSpell(SPELL_LEVITATION, player, true); - addSpell(SPELL_BLEED, player, true); + initClassStats(client_classes[player], stats[player]); + + if ( !isLocalPlayer && multiplayer == CLIENT && intro == false ) { + // don't do anything crazy with items on players we don't own + return; } - else if ( stats[player]->playerRace == RACE_SUCCUBUS && stats[player]->stat_appearance == 0 ) + + item = newItem(STEEL_FLAIL, SERVICABLE, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) { - addSpell(SPELL_TELEPORTATION, player, true); - addSpell(SPELL_SELF_POLYMORPH, player, true); + item2 = itemPickup(player, item); + useItem(item2, player); + hotbar[0].item = item2->uid; + free(item); } - else if ( stats[player]->playerRace == RACE_INSECTOID && stats[player]->stat_appearance == 0 ) + else { - addSpell(SPELL_FLUTTER, player, true); - addSpell(SPELL_DASH, player, true); - addSpell(SPELL_ACID_SPRAY, player, true); + useItem(item, player); } - else if ( stats[player]->playerRace == RACE_INCUBUS && stats[player]->stat_appearance == 0 ) + + item = newItem(AMULET_BURNINGRESIST, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) { - addSpell(SPELL_TELEPORTATION, player, true); - addSpell(SPELL_SHADOW_TAG, player, true); + item2 = itemPickup(player, item); + useItem(item2, player); + hotbar[0].item = item2->uid; + free(item); } - else if ( stats[player]->playerRace == RACE_AUTOMATON && stats[player]->stat_appearance == 0 ) + else { - addSpell(SPELL_SALVAGE, player, true); + useItem(item, player); } - if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 0 ) + item = newItem(TOOL_FRYING_PAN, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) { - bool learned = false; - if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 0 ) - { - ItemType potion = POTION_WATER; - learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); - } - if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 20 ) - { - ItemType potion = POTION_JUICE; - learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); - potion = POTION_BOOZE; - learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); - } - if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 40 ) - { - ItemType potion = POTION_ACID; - learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); - } - if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 60 ) - { - ItemType potion = POTION_INVISIBILITY; - learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); - potion = POTION_POLYMORPH; - learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); - } + item2 = itemPickup(player, item); + useItem(item2, player); + hotbar[4].item = item2->uid; + free(item); } - - if ( client_classes[player] == CLASS_SHAMAN ) + else { - addSpell(SPELL_RAT_FORM, player, true); - addSpell(SPELL_SPIDER_FORM, player, true); - addSpell(SPELL_TROLL_FORM, player, true); - addSpell(SPELL_IMP_FORM, player, true); - addSpell(SPELL_REVERT_FORM, player, true); - - addSpell(SPELL_DETECT_FOOD, player, true); - addSpell(SPELL_SPEED, player, true); - addSpell(SPELL_POISON, player, true); - addSpell(SPELL_SPRAY_WEB, player, true); - addSpell(SPELL_STRIKE, player, true); - addSpell(SPELL_FEAR, player, true); - addSpell(SPELL_LIGHTNING, player, true); - addSpell(SPELL_CONFUSE, player, true); - addSpell(SPELL_TROLLS_BLOOD, player, true); - addSpell(SPELL_AMPLIFY_MAGIC, player, true); + useItem(item, player); } - else if ( client_classes[player] == CLASS_PUNISHER ) + + item = newItem(SHAWL, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) { - addSpell(SPELL_TELEPULL, player, true); - addSpell(SPELL_DEMON_ILLUSION, player, true); + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); } - else if ( client_classes[player] == CLASS_CONJURER ) + else { - addSpell(SPELL_SUMMON, player, true); + useItem(item, player); + } + + item = newItem(HOOD_TEAL, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(CLEAT_BOOTS, WORN, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(GLOVES, WORN, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(MASK_EYEPATCH, WORN, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + if ( isLocalPlayer ) + { + item = newItem(BOLAS, SERVICABLE, 0, 3, 0, true, nullptr); + item2 = itemPickup(player, item); + hotbar[2].item = item2->uid; + free(item); + + item = newItem(TOOL_TOWEL, SERVICABLE, 0, 3, 0, true, nullptr); + item2 = itemPickup(player, item); + hotbar[5].item = item2->uid; + free(item); + + item = newItem(FOOD_MEAT, DECREPIT, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(FOOD_BREAD, DECREPIT, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + } + } + else if ( client_classes[player] == CLASS_SCION ) + { + initClassStats(client_classes[player], stats[player]); + + if ( !isLocalPlayer && multiplayer == CLIENT && intro == false ) { + // don't do anything crazy with items on players we don't own + return; + } + + item = newItem(TOOL_FOCI_DARK_RIFT, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(MAGICSTAFF_SCEPTER, EXCELLENT, 0, 1, 50, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + /*item = newItem(HAT_CIRCLET_WISDOM, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + }*/ + + item = newItem(QUILTED_BOOTS, WORN, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(ROBE_WIZARD, SERVICABLE, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + if ( isLocalPlayer ) + { + item = newItem(FOOD_BREAD, SERVICABLE, 1, 2, 0, true, nullptr); + item2 = itemPickup(player, item); + hotbar[2].item = item2->uid; + free(item); + + item = newItem(POTION_RESTOREMAGIC, SERVICABLE, 0, 2, 0, true, nullptr); + item2 = itemPickup(player, item); + hotbar[5].item = item2->uid; + free(item); + + item = newItem(POTION_HEALING, SERVICABLE, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(KEY_SILVER, WORN, 0, 2, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + } + } + else if ( client_classes[player] == CLASS_HERMIT ) + { + initClassStats(client_classes[player], stats[player]); + + if ( !isLocalPlayer && multiplayer == CLIENT && intro == false ) { + // don't do anything crazy with items on players we don't own + return; + } + + item = newItem(SHILLELAGH_MACE, WORN, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + Uint32 color = ((uniqueGameKey + player) % MAXPLAYERS); + item = newItem(TOOL_DUCK, EXCELLENT, 0, 1, MAXPLAYERS * color + player, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(LEATHER_BOOTS, SERVICABLE, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(CLOAK_PROTECTION, SERVICABLE, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(AMULET_WATERBREATHING, SERVICABLE, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + if ( isLocalPlayer ) + { + item = newItem(FOOD_APPLE, SERVICABLE, 0, 3, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(FOOD_MEAT, EXCELLENT, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(POTION_RESTOREMAGIC, EXCELLENT, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(POTION_BOOZE, EXCELLENT, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(MASK_PIPE, WORN, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + } + } + else if ( client_classes[player] == CLASS_PALADIN ) + { + initClassStats(client_classes[player], stats[player]); + + if ( !isLocalPlayer && multiplayer == CLIENT && intro == false ) { + // don't do anything crazy with items on players we don't own + return; + } + + item = newItem(CHAIN_COIF, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(CHAIN_HAUBERK, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(CLAYMORE_SWORD, SERVICABLE, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + item = newItem(TOOL_FOCI_LIGHT_SANCTUARY, EXCELLENT, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + + if ( isLocalPlayer ) + { + item = newItem(FOOD_BREAD, SERVICABLE, 0, 2, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + + item = newItem(POTION_WATER, EXCELLENT, 4, 4, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + } + } + + stats[player]->OLDHP = stats[player]->HP; + + if ( stats[player]->stat_appearance == 0 && stats[player]->playerRace == RACE_GOATMAN ) + { + stats[player]->setEffectActive(EFF_ASLEEP, 1); + stats[player]->EFFECTS_TIMERS[EFF_ASLEEP] = -1; + if ( isLocalPlayer ) + { + // extra booze for hangover :) + item = newItem(POTION_BOOZE, EXCELLENT, 0, 3, 2, true, nullptr); + item2 = itemPickup(player, item); + free(item); + } + } + else + { + stats[player]->clearEffect(EFF_ASLEEP); + stats[player]->EFFECTS_TIMERS[EFF_ASLEEP] = 0; + } + + if ( stats[player]->stat_appearance == 0 + && (stats[player]->playerRace == RACE_DRYAD || stats[player]->playerRace == RACE_MYCONID) + && !stats[player]->helmet ) + { + stats[player]->setEffectActive(EFF_GROWTH, 3); + stats[player]->EFFECTS_TIMERS[EFF_GROWTH] = -1; + } + else + { + stats[player]->clearEffect(EFF_GROWTH); + stats[player]->EFFECTS_TIMERS[EFF_GROWTH] = 0; + } + + if ( stats[player]->stat_appearance == 0 && stats[player]->playerRace == RACE_AUTOMATON ) + { + //stats[player]->HUNGER = 150; + } + + if ( stats[player]->stat_appearance == 0 + && client_classes[player] <= CLASS_MONK + && stats[player]->playerRace != RACE_HUMAN ) + { + if ( isLocalPlayer ) + { + // bonus polymorph potions + if ( stats[player]->playerRace == RACE_DRYAD + || stats[player]->playerRace == RACE_MYCONID + || stats[player]->playerRace == RACE_GNOME + || stats[player]->playerRace == RACE_SALAMANDER ) + { + /*item = newItem(POTION_POLYMORPH, EXCELLENT, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item);*/ + } + else + { + item = newItem(POTION_POLYMORPH, EXCELLENT, 0, 2, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + } + } + } + if ( stats[player]->stat_appearance == 0 + && (client_classes[player] >= CLASS_CONJURER + && client_classes[player] <= CLASS_PALADIN) + && stats[player]->playerRace != RACE_HUMAN ) + { + if ( isLocalPlayer ) + { + if ( stats[player]->playerRace == RACE_DRYAD + || stats[player]->playerRace == RACE_MYCONID + || stats[player]->playerRace == RACE_GNOME + || stats[player]->playerRace == RACE_SALAMANDER ) + { + /*item = newItem(POTION_POLYMORPH, EXCELLENT, 0, 1, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item);*/ + } + else + { + // bonus polymorph potions + item = newItem(POTION_POLYMORPH, EXCELLENT, 0, 3, 0, true, nullptr); + item2 = itemPickup(player, item); + free(item); + } + } + } + + players[player]->mechanics.ducksInARow.clear(); + if ( client_classes[player] == CLASS_HERMIT ) + { + players[player]->mechanics.ducksInARow.push_back(std::make_pair(((uniqueGameKey + player) % MAXPLAYERS), 0)); + } + + if ( stats[player]->playerRace == RACE_SALAMANDER && stats[player]->stat_appearance == 0 ) + { + stats[player]->MP = stats[player]->MAXMP / 2; + } + + /*if ( svFlags & SV_FLAG_LIFESAVING ) + { + item = newItem(AMULET_LIFESAVING, WORN, 0, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + }*/ + if ( gameModeManager.currentSession.challengeRun.isActive(GameModeManager_t::CurrentSession_t::ChallengeRun_t::CHEVENT_BFG) ) + { + item = newItem(HEAVY_CROSSBOW, EXCELLENT, curseItems ? -99 : 99, 1, 0, true, nullptr); + if ( isLocalPlayer ) + { + item2 = itemPickup(player, item); + useItem(item2, player); + free(item); + } + else + { + useItem(item, player); + } + } + if ( isLocalPlayer ) + { + if ( stats[player]->playerRace == RACE_VAMPIRE && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_LEVITATION, player, true); + addSpell(SPELL_BLEED, player, true); + } + else if ( stats[player]->playerRace == RACE_SUCCUBUS && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_TELEPORTATION, player, true); + addSpell(SPELL_SELF_POLYMORPH, player, true); + } + else if ( stats[player]->playerRace == RACE_INSECTOID && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_FLUTTER, player, true); + addSpell(SPELL_DASH, player, true); + addSpell(SPELL_ACID_SPRAY, player, true); + } + else if ( stats[player]->playerRace == RACE_INCUBUS && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_TELEPORTATION, player, true); + addSpell(SPELL_SHADOW_TAG, player, true); + } + else if ( stats[player]->playerRace == RACE_AUTOMATON && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_SALVAGE, player, true); + } + else if ( stats[player]->playerRace == RACE_DRYAD && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_THORNS, player, true); + addSpell(SPELL_SHRUB, player, true); + } + else if ( stats[player]->playerRace == RACE_MYCONID && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_SPORES, player, true); + addSpell(SPELL_MUSHROOM, player, true); + } + else if ( stats[player]->playerRace == RACE_GREMLIN && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_DEFACE, player, true); + } + else if ( stats[player]->playerRace == RACE_SALAMANDER && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_BREATHE_FIRE, player, true); + } + else if ( stats[player]->playerRace == RACE_GNOME && stats[player]->stat_appearance == 0 ) + { + addSpell(SPELL_FORGE_JEWEL, player, true); + } + + if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 0 ) + { + bool learned = false; + if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 0 ) + { + ItemType potion = POTION_WATER; + learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); + } + if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 20 ) + { + ItemType potion = POTION_JUICE; + learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); + potion = POTION_BOOZE; + learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); + } + if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 40 ) + { + ItemType potion = POTION_ACID; + learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); + } + if ( stats[player]->getProficiency(PRO_ALCHEMY) >= 60 ) + { + ItemType potion = POTION_INVISIBILITY; + learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); + potion = POTION_POLYMORPH; + learned = GenericGUI[player].alchemyLearnRecipe(potion, false, false); + } + } + + if ( client_classes[player] == CLASS_SHAMAN ) + { + addSpell(SPELL_RAT_FORM, player, true); + addSpell(SPELL_SPIDER_FORM, player, true); + addSpell(SPELL_TROLL_FORM, player, true); + addSpell(SPELL_IMP_FORM, player, true); + addSpell(SPELL_REVERT_FORM, player, true); + + addSpell(SPELL_DETECT_FOOD, player, true); + addSpell(SPELL_SPEED, player, true); + addSpell(SPELL_POISON, player, true); + addSpell(SPELL_SPRAY_WEB, player, true); + addSpell(SPELL_STRIKE, player, true); + addSpell(SPELL_FEAR, player, true); + addSpell(SPELL_LIGHTNING, player, true); + addSpell(SPELL_CONFUSE, player, true); + addSpell(SPELL_TROLLS_BLOOD, player, true); + addSpell(SPELL_AMPLIFY_MAGIC, player, true); + } + else if ( client_classes[player] == CLASS_PUNISHER ) + { + addSpell(SPELL_TELEPULL, player, true); + addSpell(SPELL_DEMON_ILLUSION, player, true); + } + else if ( client_classes[player] == CLASS_CONJURER ) + { + addSpell(SPELL_SUMMON, player, true); + } + else if ( client_classes[player] == CLASS_MESMER ) + { + addSpell(SPELL_COMMAND, player, true); + } + else if ( client_classes[player] == CLASS_BARD ) + { + addSpell(SPELL_ALTER_INSTRUMENT, player, true); + } + else if ( client_classes[player] == CLASS_SAPPER ) + { + addSpell(SPELL_BOOBY_TRAP, player, true); + } + else if ( client_classes[player] == CLASS_ARCANIST ) + { + addSpell(SPELL_WINDGATE, player, true); + } + else if ( client_classes[player] == CLASS_SCION ) + { + addSpell(SPELL_EARTH_ELEMENTAL, player, true); + addSpell(SPELL_TELEKINESIS, player, true); + addSpell(SPELL_BLESS_FOOD, player, true); + } + else if ( client_classes[player] == CLASS_HERMIT ) + { + addSpell(SPELL_MAGICIANS_ARMOR, player, true); + addSpell(SPELL_PROJECT_SPIRIT, player, true); + addSpell(SPELL_DEEP_SHADE, player, true); + } + else if ( client_classes[player] == CLASS_PALADIN ) + { + addSpell(SPELL_DIVINE_ZEAL, player, true); } //printlog("spell size: %d", list_Size(&spellList)); diff --git a/src/collision.cpp b/src/collision.cpp index b09fc7567..18206f896 100644 --- a/src/collision.cpp +++ b/src/collision.cpp @@ -214,11 +214,13 @@ Entity* entityClicked(bool* clickedOnGUI, bool clickCheckOverride, int player, E if ( !entity && !mute_player_monster_sounds && !clickCheckOverride && clicktype != ENTITY_CLICK_CALLOUT ) { - if ( players[player] && players[player]->entity && players[player]->movement.monsterEmoteGimpTimer == 0 ) + if ( players[player] && players[player]->entity && players[player]->movement.monsterEmoteGimpTimer == 0 + && !players[player]->ghost.isActive() ) { players[player]->movement.monsterEmoteGimpTimer = TICKS_PER_SECOND * 5; int sfx = 0; int line = 0; + int vol = 92; switch ( stats[player]->type ) { case SKELETON: @@ -289,6 +291,21 @@ Entity* entityClicked(bool* clickedOnGUI, bool clickCheckOverride, int player, E case CREATURE_IMP: sfx = 198 + local_rng.rand() % 3; break; + case GNOME: + sfx = 835 + local_rng.rand() % 8; + break; + case DRYAD: + sfx = 828 + local_rng.rand() % 4; + break; + case GREMLIN: + sfx = 843 + local_rng.rand() % 3; + break; + case SALAMANDER: + sfx = 846 + local_rng.rand() % 3; + break; + case MYCONID: + sfx = 832 + local_rng.rand() % 3; + break; default: sfx = 0; break; @@ -299,18 +316,19 @@ Entity* entityClicked(bool* clickedOnGUI, bool clickCheckOverride, int player, E { if ( multiplayer == CLIENT ) { - playSound(sfx, 92); + playSound(sfx, vol); strcpy((char*)net_packet->data, "EMOT"); net_packet->data[4] = player; SDLNet_Write16(sfx, &net_packet->data[5]); + net_packet->data[7] = vol; net_packet->address.host = net_server.host; net_packet->address.port = net_server.port; - net_packet->len = 7; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, 0); } else if ( multiplayer != CLIENT ) { - playSound(sfx, 92); + playSound(sfx, vol); for ( int c = 1; c < MAXPLAYERS; ++c ) { if ( !client_disconnected[c] && !players[c]->isLocalPlayer() ) @@ -318,7 +336,7 @@ Entity* entityClicked(bool* clickedOnGUI, bool clickCheckOverride, int player, E strcpy((char*)net_packet->data, "SNEL"); SDLNet_Write16(sfx, &net_packet->data[4]); SDLNet_Write32((Uint32)players[player]->entity->getUID(), &net_packet->data[6]); - SDLNet_Write16(92, &net_packet->data[10]); + SDLNet_Write16(vol, &net_packet->data[10]); net_packet->address.host = net_clients[c - 1].host; net_packet->address.port = net_clients[c - 1].port; net_packet->len = 12; @@ -369,7 +387,17 @@ bool entityInsideTile(Entity* entity, int x, int y, int z, bool checkSafeTiles) { if ( !checkSafeTiles && !map.tiles[z + y * MAPLAYERS + x * MAPLAYERS * map.height] ) { - if ( entity->behavior != &actDeathGhost && !(entity->behavior == &actMonster && entity->getStats() && entity->getStats()->type == BAT_SMALL) ) + if ( entity->behavior != &actDeathGhost + && !(entity->behavior == &actMonster + && entity->getStats() + && (entity->getStats()->type == BAT_SMALL + || entity->getStats()->type == REVENANT_SKULL + || entity->getStats()->type == MONSTER_ADORCISED_WEAPON + || entity->getStats()->type == HOLOGRAM + || entity->getStats()->type == MOTH_SMALL + || entity->getStats()->type == FLAME_ELEMENTAL + || entity->getStats()->type == EARTH_ELEMENTAL + || entity->getStats()->type == DUCK_SMALL)) ) { return true; } @@ -450,6 +478,19 @@ bool entityInsideSomething(Entity* entity) } } + Monster type = NOTHING; + if ( entity->behavior == &actMonster ) + { + type = entity->getMonsterTypeFromSprite(); + if ( type == NOTHING ) + { + if ( entity->getStats() ) + { + type = entity->getStats()->type; + } + } + } + // test against entities std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(entity, 2); for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) @@ -462,13 +503,33 @@ bool entityInsideSomething(Entity* entity) { continue; } - if ( entity->behavior == &actDeathGhost || entity->getMonsterTypeFromSprite() == BAT_SMALL ) + if ( entity->behavior == &actDeathGhost + || (entity->behavior == &actMonster && + (type == BAT_SMALL + || type == REVENANT_SKULL + || type == MONSTER_ADORCISED_WEAPON + || type == MOTH_SMALL + || type == HOLOGRAM + || type == FLAME_ELEMENTAL + || type == EARTH_ELEMENTAL)) ) { if ( testEntity->behavior == &actMonster || testEntity->behavior == &actPlayer || (testEntity->isDamageableCollider() && (testEntity->colliderHasCollision & EditorEntityData_t::COLLIDER_COLLISION_FLAG_NPC)) ) { continue; } + if ( type == EARTH_ELEMENTAL && entity->getStats() && entity->getStats()->getAttribute("earth_elemental_spawn") == "terrain_spawn_override" ) + { + if ( testEntity->behavior == &actFurniture || testEntity->behavior == &actDoor + || testEntity->behavior == &actChest ) + { + continue; + } + } + } + if ( type == DUCK_SMALL && (entity->behavior == &actPlayer || entity->behavior == &actMonster) ) + { + continue; } if ( entityInsideEntity(entity, testEntity) ) { @@ -490,8 +551,8 @@ bool useSmallCollision(Entity& my, Stat& myStats, Entity& your, Stat& yourStats) || your.getUID() == myStats.leader_uid || (myStats.leader_uid != 0 && myStats.leader_uid == yourStats.leader_uid) || (my.behavior == &actPlayer && your.behavior == &actPlayer) - || (my.behavior == &actPlayer && your.monsterAllyGetPlayerLeader()) - || (your.behavior == &actPlayer && my.monsterAllyGetPlayerLeader()) ) + || (my.behavior == &actPlayer && (your.monsterAllyGetPlayerLeader() || achievementObserver.checkUidIsFromPlayer(yourStats.leader_uid) >= 0)) + || (your.behavior == &actPlayer && (my.monsterAllyGetPlayerLeader() || achievementObserver.checkUidIsFromPlayer(myStats.leader_uid) >= 0)) ) { return true; } @@ -503,7 +564,14 @@ bool Entity::collisionProjectileMiss(Entity* parent, Entity* projectile) { if ( multiplayer == CLIENT ) { return false; } if ( !projectile ) { return false; } - if ( hit.entity ) { return false; } // we hit something in clipMove already + if ( hit.entity ) + { + if ( behavior == &actBell || behavior == &actGreasePuddleSpawner ) + { + return true; // always miss this + } + return false; // we hit something in clipMove already + } if ( (Sint32)getUID() < 0 ) { return false; @@ -516,7 +584,7 @@ bool Entity::collisionProjectileMiss(Entity* parent, Entity* projectile) } } - if ( behavior == &actBell ) + if ( behavior == &actBell || behavior == &actGreasePuddleSpawner ) { if ( !flags[BURNING] ) { @@ -526,15 +594,21 @@ bool Entity::collisionProjectileMiss(Entity* parent, Entity* projectile) { if ( spell_t* spell = (spell_t*)projectile->children.first->element ) { - if ( spell->ID == SPELL_FIREBALL || spell->ID == SPELL_SLIME_FIRE ) + if ( spell->ID == SPELL_FIREBALL || spell->ID == SPELL_SLIME_FIRE + || spell->ID == SPELL_FLAMES || spell->ID == SPELL_METEOR + || spell->ID == SPELL_BREATHE_FIRE + || spell->ID == SPELL_FOCI_FIRE || spell->ID == SPELL_METEOR_SHOWER ) { - SetEntityOnFire(); + SetEntityOnFire(parent); if ( parent && flags[BURNING] ) { - skill[13] = parent->getUID(); // burning inflicted by for bell - if ( parent->behavior == &actPlayer ) + if ( behavior == &actBell ) { - messagePlayer(parent->skill[2], MESSAGE_INTERACTION, Language::get(6297)); + skill[13] = parent->getUID(); // burning inflicted by for bell + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_INTERACTION, Language::get(6297)); + } } } } @@ -546,22 +620,29 @@ bool Entity::collisionProjectileMiss(Entity* parent, Entity* projectile) SetEntityOnFire(parent); if ( parent && flags[BURNING] ) { - skill[13] = parent->getUID(); // burning inflicted by for bell - if ( parent->behavior == &actPlayer ) + if ( behavior == &actBell ) { - messagePlayer(parent->skill[2], MESSAGE_INTERACTION, Language::get(6297)); + skill[13] = parent->getUID(); // burning inflicted by for bell + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_INTERACTION, Language::get(6297)); + } } } } else if ( projectile->flags[BURNING] && (projectile->behavior == &actMonster || projectile->behavior == &actPlayer) ) { + bool prevBurning = this->flags[BURNING]; SetEntityOnFire(projectile); if ( flags[BURNING] ) { - skill[13] = projectile->getUID(); // burning inflicted by for bell - if ( projectile->behavior == &actPlayer ) + if ( behavior == &actBell ) { - messagePlayer(projectile->skill[2], MESSAGE_INTERACTION, Language::get(6297)); + skill[13] = projectile->getUID(); // burning inflicted by for bell + if ( projectile->behavior == &actPlayer && !prevBurning ) + { + messagePlayer(projectile->skill[2], MESSAGE_INTERACTION, Language::get(6297)); + } } } } @@ -577,10 +658,121 @@ bool Entity::collisionProjectileMiss(Entity* parent, Entity* projectile) } if ( Stat* myStats = getStats() ) { - if ( myStats->type == BAT_SMALL || myStats->EFFECTS[EFF_AGILITY] ) + if ( parent && (parent->behavior == &actPlayer || parent->behavior == &actMonster) ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) && (projectile->behavior == &actMagicMissile || projectile->behavior == &actArrow) ) + { + if ( parent->checkFriend(this) && parent->friendlyFireProtection(this) ) + { + projectile->collisionIgnoreTargets.insert(getUID()); + return true; + } + } + } + + if ( (projectile->behavior == &actThrown || projectile->behavior == &actArrow) + && myStats->getEffectActive(EFF_MAGICIANS_ARMOR) + && !(!(svFlags & SV_FLAG_FRIENDLYFIRE) && parent && parent->checkFriend(this) && parent->friendlyFireProtection(this)) /*dont apply to friendly fire */ ) + { + Uint8 effectStrength = myStats->getEffectActive(EFF_MAGICIANS_ARMOR); + int duration = myStats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR]; + if ( effectStrength == 1 ) + { + if ( myStats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR] > 0 ) + { + myStats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR] = 1; + } + } + else if ( effectStrength > 1 ) + { + --effectStrength; + myStats->setEffectValueUnsafe(EFF_MAGICIANS_ARMOR, effectStrength); + this->setEffect(EFF_MAGICIANS_ARMOR, effectStrength, myStats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR], true); + } + + magicOnSpellCastEvent(this, this, parent, SPELL_MAGICIANS_ARMOR, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + if ( this->behavior == &actPlayer ) + { + steamStatisticUpdateClient(this->skill[2], STEAM_STAT_DOESNT_COUNT, STEAM_STAT_INT, 1); + } + + if ( projectile->collisionIgnoreTargets.find(getUID()) == projectile->collisionIgnoreTargets.end() ) + { + projectile->collisionIgnoreTargets.insert(getUID()); + if ( (parent && parent->behavior == &actPlayer) || (parent && parent->behavior == &actMonster && parent->monsterAllyGetPlayerLeader()) + || this->behavior == &actPlayer || this->monsterAllyGetPlayerLeader() + || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0 ) + { + spawnDamageGib(this, 0, DamageGib::DMG_GUARD, DamageGibDisplayType::DMG_GIB_GUARD, true); + } + + if ( this->behavior == &actPlayer ) + { + if ( projectile->behavior == &actArrow ) + { + if ( projectile->sprite == 167 ) + { + // bolt + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6472), Language::get(6292)); + } + else if ( projectile->sprite == 78 ) + { + // rock + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6472), Language::get(6293)); + } + else + { + // arrow + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6472), Language::get(6291)); + } + } + else if ( projectile->behavior == &actThrown ) + { + if ( projectile->skill[10] >= 0 && projectile->skill[10] < NUMITEMS ) + { + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6471), items[projectile->skill[10]].getUnidentifiedName()); + } + else + { + // generic "projectile" + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6471), Language::get(6296)); + } + } + else + { + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6465)); + } + } + if ( parent && parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], makeColorRGB(255, 255, 255), + *myStats, Language::get(6468), Language::get(6469), MSG_COMBAT); // %s guards the attack + } + //playSoundEntity(this, 166, 128); + + Entity* fx = createParticleAestheticOrbit(this, 1817, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE); + fx->x = this->x; + fx->y = this->y; + fx->z = this->z; + real_t tangent = atan2(projectile->y - this->y, projectile->x - this->x); + fx->x += 4.0 * cos(tangent); + fx->y += 4.0 * sin(tangent); + fx->yaw = tangent; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 0; + serverSpawnMiscParticlesAtLocation(fx->x, fx->y, fx->z, PARTICLE_EFFECT_NULL_PARTICLE, 1817, 0, fx->yaw * 256.0); + } + return true; + } + + if ( myStats->type == BAT_SMALL || myStats->getEffectActive(EFF_AGILITY) || myStats->getEffectActive(EFF_ENSEMBLE_LUTE) + || mistFormDodge(true, parent) + || !monsterIsTargetable(true) + || (myStats->type == DRYAD && myStats->sex == FEMALE && behavior == &actPlayer) + || (parent && parent->getStats() && parent->getStats()->getEffectActive(EFF_BLIND)) ) { bool miss = false; - if ( myStats->type == BAT_SMALL && isUntargetableBat() ) + if ( !monsterIsTargetable(true) ) { projectile->collisionIgnoreTargets.insert(getUID()); return true; @@ -603,9 +795,10 @@ bool Entity::collisionProjectileMiss(Entity* parent, Entity* projectile) } else { - if ( monsterState == MONSTER_STATE_WAIT + if ( (monsterState == MONSTER_STATE_WAIT || monsterState == MONSTER_STATE_PATH - || (monsterState == MONSTER_STATE_HUNT && uidToEntity(monsterTarget) == nullptr) ) + || (monsterState == MONSTER_STATE_HUNT && uidToEntity(monsterTarget) == nullptr)) + && !myStats->getEffectActive(EFF_ROOTED) ) { // unaware monster, get backstab damage. backstab = true; @@ -620,23 +813,68 @@ bool Entity::collisionProjectileMiss(Entity* parent, Entity* projectile) } bool accuracyBonus = projectile->behavior == &actMagicMissile && myStats->type == BAT_SMALL; - if ( backstab ) + if ( mistFormDodge(false, parent) ) + { + miss = true; + } + else if ( backstab ) { miss = false; + + if ( myStats->type == DRYAD && myStats->sex == FEMALE && behavior == &actPlayer ) + { + int baseChance = 5; + miss = players[this->skill[2]]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_EVASION, baseChance); + } } else { - int baseChance = myStats->type == BAT_SMALL ? 6 : 3; - if ( accuracyBonus ) + int baseChance = 0; + if ( myStats->type == BAT_SMALL ) + { + baseChance = std::max(baseChance, 60); + } + if ( myStats->getEffectActive(EFF_AGILITY) ) + { + baseChance = std::max(baseChance, 30); + } + if ( myStats->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_TIER) > 0.001 ) { - baseChance -= 2; + baseChance = std::max(baseChance, static_cast(myStats->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_TIER))); } - if ( flanking ) + if ( myStats->type == DRYAD && myStats->sex == FEMALE && behavior == &actPlayer ) { - baseChance -= 2; + baseChance = std::max(baseChance, 5); + } + if ( parent && parent->getStats() && parent->getStats()->getEffectActive(EFF_BLIND) ) + { + baseChance = std::max(baseChance, 75); + } + if ( baseChance <= 0 ) + { + miss = false; + } + else + { + if ( accuracyBonus ) + { + baseChance -= 20; + } + if ( flanking ) + { + baseChance -= 20; + } + + baseChance = std::max(10, baseChance); + if ( this->behavior == &actPlayer ) + { + miss = players[this->skill[2]]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_EVASION, baseChance); + } + else + { + miss = local_rng.rand() % 100 < baseChance; + } } - baseChance = std::max(1, baseChance); - miss = local_rng.rand() % 10 < baseChance; } if ( miss ) @@ -647,13 +885,18 @@ bool Entity::collisionProjectileMiss(Entity* parent, Entity* projectile) if ( (parent && parent->behavior == &actPlayer) || (parent && parent->behavior == &actMonster && parent->monsterAllyGetPlayerLeader()) || (behavior == &actPlayer) - || (behavior == &actMonster && monsterAllyGetPlayerLeader()) ) + || (behavior == &actMonster && monsterAllyGetPlayerLeader()) + || achievementObserver.checkUidIsFromPlayer(myStats->leader_uid) >= 0 ) { spawnDamageGib(this, 0, DamageGib::DMG_MISS, DamageGibDisplayType::DMG_GIB_MISS, true); } if ( behavior == &actPlayer ) { + if ( myStats->type == DRYAD ) + { + this->playerShakeGrowthHelmet(); + } if ( projectile->behavior == &actMagicMissile ) { messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6287), Language::get(6295)); @@ -755,6 +998,8 @@ int barony_clear(real_t tx, real_t ty, Entity* my) bool reduceCollisionSize = false; bool tryReduceCollisionSize = false; bool projectileAttack = false; + bool parentDodgeChance = false; + bool friendlyFireCheck = false; Entity* parent = nullptr; Stat* parentStats = nullptr; if ( my ) @@ -762,6 +1007,10 @@ int barony_clear(real_t tx, real_t ty, Entity* my) if ( my->behavior != &actPlayer && my->behavior != &actMonster ) { levitating = true; + if ( my->behavior == &actLeafPile || my->behavior == &actLeafParticle ) + { + levitating = false; + } } if ( multiplayer != CLIENT ) { @@ -770,7 +1019,29 @@ int barony_clear(real_t tx, real_t ty, Entity* my) projectileAttack = true; if ( parent = uidToEntity(my->parent) ) { - if ( my->behavior == &actThrown ) + if ( Stat* tmpStats = parent->getStats() ) + { + parentDodgeChance = tmpStats->getEffectActive(EFF_BLIND); + + if ( parent && parent->behavior == &actPlayer ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( !(my->behavior == &actMagicMissile + && (my->actmagicTinkerTrapFriendlyFire == 1 || my->actmagicAllowFriendlyFireHit == 1)) + && (my->behavior == &actMagicMissile || my->behavior == &actArrow) ) + { + friendlyFireCheck = true; + } + } + } + } + + if ( my->behavior == &actMagicMissile && my->sprite == 2407 ) // holy beam targets allies too + { + tryReduceCollisionSize = false; + } + else if ( my->behavior == &actThrown ) { tryReduceCollisionSize = true; if ( Item* item = newItemFromEntity(my) ) @@ -809,7 +1080,7 @@ int barony_clear(real_t tx, real_t ty, Entity* my) { if ( x >= 0 && y >= 0 && x < map.width && y < map.height ) { - if ( x == 1 || (x == map.width - 1) || y == 1 || (y == map.height - 1) ) + if ( x == 0 || (x == map.width - 1) || y == 0 || (y == map.height - 1) ) { // collides with map edges only hit.x = x * 16 + 8; @@ -822,7 +1093,7 @@ int barony_clear(real_t tx, real_t ty, Entity* my) } } } - if ( my && my->behavior == &actMagiclightMoving ) + if ( my && (my->behavior == &actMagiclightMoving || (my->behavior == &actMonster && my->getMonsterTypeFromSprite() == DUCK_SMALL)) ) { return 1; // no other collision } @@ -893,17 +1164,23 @@ int barony_clear(real_t tx, real_t ty, Entity* my) entityDodgeChance = false; if ( entity->flags[PASSABLE] ) { - if ( my->behavior == &actBoulder && (entity->behavior == &actMonster && entity->sprite == 886) ) + if ( my->behavior == &actBoulder && (entity->behavior == &actMonster + && (entity->sprite == 886 + || entity->sprite == 1797)) ) { - // 886 is gyrobot, as they are passable, force collision here. + // 886/1797 is gyrobot/spirit weapon, as they are passable, force collision here. } - else if ( entity->sprite == 1478 + else if ( (entity->sprite == 1478 || entity->sprite == 1786) && (projectileAttack || (my && my->flags[BURNING] && (my->behavior == &actMonster || my->behavior == &actPlayer))) && multiplayer != CLIENT ) { // bell rope, check for burning entityDodgeChance = true; } + else if ( (entity->behavior == &actParticleDemesneDoor || entity->sprite == 1809) && (type == VAMPIRE || (my && my->behavior == &actPlayer && stats && stats->type == VAMPIRE)) ) + { + // hard collision + } else { continue; @@ -920,15 +1197,33 @@ int barony_clear(real_t tx, real_t ty, Entity* my) continue; } if ( entity->isDamageableCollider() && (entity->colliderHasCollision & EditorEntityData_t::COLLIDER_COLLISION_FLAG_NPC) - && ((my->behavior == &actMonster && (type == GYROBOT || type == BAT_SMALL)) || my->behavior == &actDeathGhost) ) + && ((my->behavior == &actMonster + && (type == GYROBOT + || type == BAT_SMALL + || type == REVENANT_SKULL + || type == MONSTER_ADORCISED_WEAPON + || type == MOTH_SMALL + || type == HOLOGRAM + || type == FLAME_ELEMENTAL + || entity->isColliderPathableMonster(type) + )) || my->behavior == &actDeathGhost) ) { continue; } - if ( entity->behavior == &actFurniture && type == BAT_SMALL ) + if ( projectileAttack && entity->isDamageableCollider() && entity->colliderSpellEvent > 0 ) + { + entityDodgeChance = true; + } + if ( entity->behavior == &actFurniture + && (type == BAT_SMALL + || type == REVENANT_SKULL + || type == MONSTER_ADORCISED_WEAPON + || type == MOTH_SMALL + || type == FLAME_ELEMENTAL) ) { continue; } - if ( entity->getMonsterTypeFromSprite() == BAT_SMALL ) + if ( entity->behavior == &actMonster && !entity->monsterIsTargetable(true) ) { if ( my->behavior == &actBoulder ) { @@ -951,6 +1246,22 @@ int barony_clear(real_t tx, real_t ty, Entity* my) continue; } } + if ( entity->behavior == &actMonster && + (entity->getMonsterTypeFromSprite() == REVENANT_SKULL || entity->getMonsterTypeFromSprite() == MONSTER_ADORCISED_WEAPON + || entity->getMonsterTypeFromSprite() == FLAME_ELEMENTAL || entity->getMonsterTypeFromSprite() == MOTH_SMALL) ) + { + if ( my->behavior == &actBoulder ) + { + // force collision here. + } + else if ( projectileAttack ) + { + } + else + { + continue; + } + } if ( (my->behavior == &actMonster || my->behavior == &actBoulder) && entity->behavior == &actDoorFrame ) { continue; // monsters don't have hard collision with door frames @@ -966,13 +1277,39 @@ int barony_clear(real_t tx, real_t ty, Entity* my) { continue; } + if ( type == EARTH_ELEMENTAL || type == MONSTER_ADORCISED_WEAPON ) + { + if ( stats && stats->getEffectActive(EFF_KNOCKBACK) + && (entity->behavior == &actMonster || entity->behavior == &actPlayer) ) + { + continue; + } + else if ( type == EARTH_ELEMENTAL && entity->behavior == &actChest && entityInsideEntity(entity, my) ) + { + continue; + } + } Stat* myStats = stats; //my->getStats(); //SEB <<< Stat* yourStats = entity->getStats(); if ( my->behavior == &actPlayer && entity->behavior == &actPlayer ) { continue; } - if ( projectileAttack && yourStats && yourStats->EFFECTS[EFF_AGILITY] ) + if ( projectileAttack && my->behavior == &actMagicMissile + && (my->sprite == 2191 || my->sprite == 2364 || my->sprite == 2407) ) // scepter blast/blood waves/holy beam phases entities + { + if ( my->collisionIgnoreTargets.find(entity->getUID()) != my->collisionIgnoreTargets.end() ) + { + continue; + } + } + if ( projectileAttack + && (yourStats && (parentDodgeChance + || yourStats->getEffectActive(EFF_AGILITY) + || yourStats->getEffectActive(EFF_ENSEMBLE_LUTE) + || entity->mistFormDodge(true, parent) + || (yourStats->type == DRYAD && yourStats->sex == FEMALE && entity->behavior == &actPlayer) + || (yourStats->getEffectActive(EFF_MAGICIANS_ARMOR) && (my->behavior == &actThrown || my->behavior == &actArrow)))) ) { entityDodgeChance = true; } @@ -1026,6 +1363,14 @@ int barony_clear(real_t tx, real_t ty, Entity* my) { continue; } + if ( yourStats->type == SHOPKEEPER && yourStats->MISC_FLAGS[STAT_FLAG_MYSTERIOUS_SHOPKEEP] > 0 ) + { + continue; + } + if ( yourStats->type == EARTH_ELEMENTAL && entityInsideEntity(my, entity) ) + { + continue; + } } if ( (myStats->type == HUMAN || my->flags[USERFLAG2]) && (yourStats->type == HUMAN || entity->flags[USERFLAG2]) ) { @@ -1034,13 +1379,20 @@ int barony_clear(real_t tx, real_t ty, Entity* my) } else if ( multiplayer != CLIENT ) { - if ( entityDodgeChance ) + if ( entityDodgeChance || (projectileAttack && my->collisionIgnoreTargets.size()) ) { if ( my->collisionIgnoreTargets.find(entity->getUID()) != my->collisionIgnoreTargets.end() ) { continue; } } + if ( projectileAttack && friendlyFireCheck ) + { + if ( parent && parent->checkFriend(entity) && parent->friendlyFireProtection(entity) ) + { + continue; + } + } if ( tryReduceCollisionSize ) { if ( my->behavior == &actMagicMissile && my->actmagicSpray == 1 ) @@ -1119,6 +1471,19 @@ int barony_clear(real_t tx, real_t ty, Entity* my) exmin += xoffset; exmax += xoffset; } + else if ( projectileAttack && (entity->behavior == &actGate || entity->behavior == &actDoor || entity->behavior == &actIronDoor) ) + { + if ( entity->sizex == 1 ) + { + exmin -= 0.01; + exmax += 0.01; + } + if ( entity->sizey == 1 ) + { + eymin -= 0.01; + eymax += 0.01; + } + } if ( (entity->sizex > 0) && ((txmin >= exmin && txmin < exmax) || (txmax >= exmin && txmax < exmax) || (txmin <= exmin && txmax > exmax)) ) { if ( (entity->sizey > 0) && ((tymin >= eymin && tymin < eymax) || (tymax >= eymin && tymax < eymax) || (tymin <= eymin && tymax > eymax)) ) @@ -1134,6 +1499,38 @@ int barony_clear(real_t tx, real_t ty, Entity* my) } } + if ( my && my->behavior == &actMagicMissile ) + { + if ( my->actmagicIsVertical == MAGIC_ISVERTICAL_XYZ ) + { + if ( my->sprite == 2209 || my->sprite == 233 ) + { + int tilex = entity->x / 16; + int tiley = entity->y / 16; + if ( tilex >= 0 && tilex < map.width && tiley >= 0 && tiley < map.height ) + { + if ( !map.tiles[(MAPLAYERS - 1) + tiley * MAPLAYERS + tilex * MAPLAYERS * map.height] ) + { + if ( entity->behavior == &actMonster ) + { + if ( my->z <= -8.0 ) + { + return 1; + } + } + else if ( my->z <= -5 && + !(entity->behavior == &actGate + || entity->behavior == &actDoor + || entity->behavior == &actIronDoor) ) + { + return 1; + } + } + } + } + } + } + tx2 = std::max(txmin, exmin); ty2 = std::max(tymin, eymin); hit.x = tx2; @@ -1157,17 +1554,58 @@ int barony_clear(real_t tx, real_t ty, Entity* my) } } } + + //if ( myStats && myStats->getEffectActive(EFF_FLAME_CLOAK) ) + //{ + // int chance = local_rng.rand() % 100; + // if ( ticks % 50 == 0 && chance < myStats->getEffectActive(EFF_FLAME_CLOAK) ) + // { + // // try burn + // } + // else + // { + // // no fire + // dyrnwyn = true; + // } + //} + if ( !dyrnwyn ) { bool previouslyOnFire = hit.entity->flags[BURNING]; // Attempt to set the Entity on fire - hit.entity->SetEntityOnFire(); - - // If the Entity is now on fire, tell them - if ( hit.entity->flags[BURNING] && !previouslyOnFire ) + if ( hit.entity->SetEntityOnFire(my) ) { - messagePlayer(hit.entity->skill[2], MESSAGE_STATUS, Language::get(590)); // "You suddenly catch fire!" + if ( myStats && yourStats ) + { + if ( myStats->getEffectActive(EFF_FLAME_CLOAK) ) + { + hit.entity->char_fire = std::min(hit.entity->char_fire, + getSpellEffectDurationSecondaryFromID(SPELL_FLAME_CLOAK, my, nullptr, my)); + } + yourStats->burningInflictedBy = my->getUID(); + + if ( my->behavior == &actPlayer ) + { + bool alertTarget = hit.entity->monsterAlertBeforeHit(my); + + // alert the monster! + if ( hit.entity->monsterState != MONSTER_STATE_ATTACK && (yourStats->type < LICH || yourStats->type >= SHOPKEEPER) ) + { + if ( alertTarget ) + { + hit.entity->monsterAcquireAttackTarget(*my, MONSTER_STATE_PATH, true); + } + } + + // alert other monsters too + /*if ( alertTarget ) + { + hit.entity->alertAlliesOnBeingHit(my); + }*/ + hit.entity->updateEntityOnHit(my, alertTarget); + } + } } } } @@ -1185,17 +1623,58 @@ int barony_clear(real_t tx, real_t ty, Entity* my) } } } + + //if ( yourStats && yourStats->getEffectActive(EFF_FLAME_CLOAK) ) + //{ + // int chance = local_rng.rand() % 100; + // if ( ticks % 50 == 0 && chance < yourStats->getEffectActive(EFF_FLAME_CLOAK) ) + // { + // // try burn + // } + // else + // { + // // no fire + // dyrnwyn = true; + // } + //} + if ( !dyrnwyn ) { - bool previouslyOnFire = hit.entity->flags[BURNING]; + bool previouslyOnFire = my->flags[BURNING]; // Attempt to set the Entity on fire - hit.entity->SetEntityOnFire(); - - // If the Entity is now on fire, tell them - if ( hit.entity->flags[BURNING] && !previouslyOnFire ) + if ( my->SetEntityOnFire(hit.entity) ) { - messagePlayer(hit.entity->skill[2], MESSAGE_STATUS, Language::get(590)); // "You suddenly catch fire!" + if ( myStats && yourStats ) + { + if ( yourStats->getEffectActive(EFF_FLAME_CLOAK) ) + { + my->char_fire = std::min(my->char_fire, + getSpellEffectDurationSecondaryFromID(SPELL_FLAME_CLOAK, hit.entity, nullptr, hit.entity)); + } + myStats->burningInflictedBy = hit.entity->getUID(); + + if ( hit.entity->behavior == &actPlayer ) + { + bool alertTarget = my->monsterAlertBeforeHit(hit.entity); + + // alert the monster! + if ( my->monsterState != MONSTER_STATE_ATTACK && (myStats->type < LICH || myStats->type >= SHOPKEEPER) ) + { + if ( alertTarget ) + { + my->monsterAcquireAttackTarget(*hit.entity, MONSTER_STATE_PATH, true); + } + } + + // alert other monsters too + /*if ( alertTarget ) + { + my->alertAlliesOnBeingHit(hit.entity); + }*/ + my->updateEntityOnHit(hit.entity, alertTarget); + } + } } } } @@ -1226,6 +1705,7 @@ real_t clipMove(real_t* x, real_t* y, real_t vx, real_t vy, Entity* my) // move x and y tx = *x + vx; ty = *y + vy; + if (barony_clear(tx, ty, my)) { *x = tx; @@ -1234,6 +1714,7 @@ real_t clipMove(real_t* x, real_t* y, real_t vx, real_t vy, Entity* my) return sqrt(vx * vx + vy * vy); } + hit_t prevHit = hit; // only move x tx = *x + vx; ty = *y; @@ -1245,6 +1726,7 @@ real_t clipMove(real_t* x, real_t* y, real_t vx, real_t vy, Entity* my) return fabs(vx); } + prevHit = hit; // only move y tx = *x; ty = *y + vy; @@ -1255,6 +1737,13 @@ real_t clipMove(real_t* x, real_t* y, real_t vx, real_t vy, Entity* my) hit.side = HORIZONTAL; return fabs(vy); } + + if ( my && my->behavior == &actPlayer && prevHit.entity && prevHit.entity->behavior == &actDoorFrame && !hit.entity ) + { + // allow players to hit doorways when next to map tile instead (otherwise gets overwritten here) + hit = prevHit; + } + hit.side = 0; return 0; } @@ -1269,7 +1758,7 @@ real_t clipMove(real_t* x, real_t* y, real_t vx, real_t vy, Entity* my) -------------------------------------------------------------------------------*/ -Entity* findEntityInLine( Entity* my, real_t x1, real_t y1, real_t angle, int entities, Entity* target ) +Entity* findEntityInLine( Entity* my, real_t x1, real_t y1, real_t angle, int entities, Entity* target, list_t* entityListToUse) { Entity* result = NULL; node_t* node; @@ -1374,6 +1863,12 @@ Entity* findEntityInLine( Entity* my, real_t x1, real_t y1, real_t angle, int en } } + if ( entityListToUse ) + { + entLists.clear(); + entLists.push_back(entityListToUse); + } + bool adjust = false; if ( angle >= PI / 2 && angle < 3 * (PI / 2) ) { @@ -1395,7 +1890,12 @@ Entity* findEntityInLine( Entity* my, real_t x1, real_t y1, real_t angle, int en bool ignoreFurniture = my && my->behavior == &actMonster && myStats && (myStats->type == SHOPKEEPER || myStats->type == MINOTAUR - || myStats->type == BAT_SMALL); + || myStats->type == BAT_SMALL + || myStats->type == REVENANT_SKULL + || myStats->type == MONSTER_ADORCISED_WEAPON + || myStats->type == MOTH_SMALL + || myStats->type == FLAME_ELEMENTAL + ); for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) { @@ -1404,17 +1904,35 @@ Entity* findEntityInLine( Entity* my, real_t x1, real_t y1, real_t angle, int en { Entity* entity = (Entity*)node->element; if ( (entity != target && target != nullptr) || entity->flags[PASSABLE] || entity == my - || ((entities == LINETRACE_IGNORE_ENTITIES) && + || ((entities & LINETRACE_IGNORE_ENTITIES) && ( (!entity->flags[BLOCKSIGHT] && entity->behavior != &actMonster) || (entity->behavior == &actMonster && (entity->flags[INVISIBLE] - && entity->sprite != 889 && entity->sprite != 1247 && entity->sprite != 1408) ) + && entity->sprite != 889 + && entity->sprite != 1247 + && entity->sprite != 1792 + && entity->sprite != 1794 + && entity->sprite != 1408 + && entity->sprite != 1796 + && entity->sprite != 1797 + && entity->sprite != 1803 + && entity->sprite != 1804 + && entity->sprite != 1819 + && entity->sprite != 1822 + && entity->sprite != 1871 + && entity->sprite != 1876 + && entity->sprite != 2225 + && entity->sprite != 2226 + && entity->sprite != 2231 + && entity->sprite != 2232 + && entity->sprite != 2237 + && entity->sprite != 2238) ) ) ) ) { - // if entities == LINETRACE_IGNORE_ENTITIES, then ignore entities that block sight. + // if entities & LINETRACE_IGNORE_ENTITIES, then ignore entities that block sight. // 16/11/19 - added exception to monsters. if monster, use the INVISIBLE flag to skip checking. - // 889/1247/1408 is dummybot/mimic/bat "invisible" AI entity. so it's invisible, need to make it shown here. + // 889/1247/1408 is dummybot/mimic/bat/revenant_skull/adorcised weapon/elemental/moth/duck "invisible" AI entity. so it's invisible, need to make it shown here. if ( entity->behavior == &actMonster && entity->sprite == 1408 ) { if ( (entity != target && target != nullptr) || entity == my || entity->flags[PASSABLE] ) @@ -1428,7 +1946,28 @@ Entity* findEntityInLine( Entity* my, real_t x1, real_t y1, real_t angle, int en } else { - continue; + if ( entities & LINETRACE_TELEKINESIS ) + { + if ( ((entity->behavior == &actIronDoor || entity->behavior == &actGate) && !entity->flags[PASSABLE]) ) + { + + } + else + { + continue; + } + } + else if ( (entities & LINETRACE_TOOLTIP_INTERACT) && my && my->behavior == &actSpriteWorldTooltip + && ((entity->behavior == &actIronDoor || entity->behavior == &actDoor) + && my->parent != entity->getUID() && target && target->behavior == &actPlayer && !entityInsideEntity(target, entity) + && !entity->flags[PASSABLE]) ) + { + // let doors block tooltips + } + else + { + continue; + } } } if ( entity->behavior == &actParticleTimer ) @@ -1442,7 +1981,7 @@ Entity* findEntityInLine( Entity* my, real_t x1, real_t y1, real_t angle, int en { continue; // see through furniture cause we'll bust it down } - if ( entity->isUntargetableBat() ) + if ( !entity->monsterIsTargetable(true) ) { continue; } @@ -1451,7 +1990,7 @@ Entity* findEntityInLine( Entity* my, real_t x1, real_t y1, real_t angle, int en int entitymapy = static_cast(entity->y) >> 4; real_t sizex = entity->sizex; real_t sizey = entity->sizey; - if ( entities == LINETRACE_ATK_CHECK_FRIENDLYFIRE && multiplayer != CLIENT ) + if ( (entities & LINETRACE_ATK_CHECK_FRIENDLYFIRE) && multiplayer != CLIENT ) { if ( (my->behavior == &actMonster || my->behavior == &actPlayer) && (entity->behavior == &actMonster || entity->behavior == &actPlayer) ) @@ -1729,7 +2268,12 @@ real_t lineTrace( Entity* my, real_t x1, real_t y1, real_t angle, real_t range, { ground = false; } - else if ( stats->type == SENTRYBOT || stats->type == SPELLBOT || stats->type == BAT_SMALL ) + else if ( stats->type == SENTRYBOT || stats->type == SPELLBOT || stats->type == BAT_SMALL + || stats->type == REVENANT_SKULL + || stats->type == MONSTER_ADORCISED_WEAPON + || stats->type == MOTH_SMALL + || stats->type == FLAME_ELEMENTAL + || stats->type == EARTH_ELEMENTAL ) { ground = false; } @@ -1749,7 +2293,7 @@ real_t lineTrace( Entity* my, real_t x1, real_t y1, real_t angle, real_t range, sizey = entity->sizey; yourStats = entity->getStats(); - if ( entities == LINETRACE_ATK_CHECK_FRIENDLYFIRE ) + if ( entities & LINETRACE_ATK_CHECK_FRIENDLYFIRE ) { if ( my && stats && (my->behavior == &actMonster || my->behavior == &actPlayer) && entity && (entity->behavior == &actMonster || entity->behavior == &actPlayer) && yourStats ) @@ -1819,7 +2363,7 @@ real_t lineTrace( Entity* my, real_t x1, real_t y1, real_t angle, real_t range, if ( entity ) { // debug particles. - if ( my && my->behavior == &actPlayer && entities == LINETRACE_ATK_CHECK_FRIENDLYFIRE ) + if ( my && my->behavior == &actPlayer && (entities & LINETRACE_ATK_CHECK_FRIENDLYFIRE) ) { static ConsoleVariable cvar_linetracedebug("/linetracedebug", false); if ( *cvar_linetracedebug ) @@ -1879,7 +2423,7 @@ real_t lineTrace( Entity* my, real_t x1, real_t y1, real_t angle, real_t range, return range; } -real_t lineTraceTarget(Entity* my, real_t x1, real_t y1, real_t angle, real_t range, int entities, bool ground, Entity* target) +real_t lineTraceTarget(Entity* my, real_t x1, real_t y1, real_t angle, real_t range, int entities, bool ground, Entity* target, list_t* entityListToUse) { int posx, posy; real_t fracx, fracy; @@ -1937,7 +2481,7 @@ real_t lineTraceTarget(Entity* my, real_t x1, real_t y1, real_t angle, real_t ra } d = 0; - Entity* entity = findEntityInLine(my, x1, y1, angle, entities, target); + Entity* entity = findEntityInLine(my, x1, y1, angle, entities, target, entityListToUse); bool isMonster = false; bool waterWalking = my && my->isWaterWalking(); @@ -2003,7 +2547,7 @@ real_t lineTraceTarget(Entity* my, real_t x1, real_t y1, real_t angle, real_t ra if ( entity ) { // debug particles. - if ( my && my->behavior == &actMonster && entities == 0 ) + if ( my && my->behavior == &actMonster && (entities == 0 || (entities & LINETRACE_ATK_CHECK_FRIENDLYFIRE)) ) { static ConsoleVariable cvar_linetracetargetdebug("/linetracetargetdebug", false); if ( *cvar_linetracetargetdebug ) @@ -2072,7 +2616,7 @@ real_t lineTraceTarget(Entity* my, real_t x1, real_t y1, real_t angle, real_t ra -------------------------------------------------------------------------------*/ -int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntityList, bool checkWalls, bool checkFloor) +int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntityList, bool checkWalls, bool checkFloor, bool checkEnemies) { node_t* node = nullptr; Entity* entity = nullptr; @@ -2128,6 +2672,7 @@ int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntity { // for map generation to detect if decorations have obstacles without entities being assigned actions std::vector entLists{ map.entities }; + bool ceilingTilesAllowed = !strncmp(map.filename, "fortress", 8); for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) { list_t* currentList = *it; @@ -2140,6 +2685,7 @@ int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntity || entity == target || entity->sprite == 8 // items || entity->sprite == 9 // gold + || (entity->sprite == 119 && ceilingTilesAllowed) // ceiling tiles || entity->behavior == &actDoor ) { continue; @@ -2165,7 +2711,9 @@ int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntity entity = (Entity*)node->element; //++entCheck; if ( !entity ) { continue; } - if ( entity->flags[PASSABLE] || entity == my || entity == target || entity->behavior == &actDoor ) + if ( entity->flags[PASSABLE] || entity == my || entity == target + || entity->behavior == &actDoor + || (entity->behavior == &actIronDoor && entity->doorLocked == 0) ) { continue; } @@ -2176,7 +2724,8 @@ int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntity if ( isMonster && my->getMonsterTypeFromSprite() == MINOTAUR && ((entity->isDamageableCollider() && (entity->colliderHasCollision & EditorEntityData_t::COLLIDER_COLLISION_FLAG_MINO)) - || entity->behavior == &::actDaedalusShrine) ) + || entity->behavior == &::actDaedalusShrine + || entity->behavior == &actIronDoor) ) { continue; } @@ -2185,11 +2734,22 @@ int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntity { continue; } - if ( my && my->behavior == &actDeathGhost && (entity->behavior == &actPlayer || entity->behavior == &actMonster) ) + else if ( my && my->behavior == &actMagiclightBall && !entity->flags[BLOCKSIGHT] ) + { + continue; + } + if ( my && (my->behavior == &actDeathGhost || !checkEnemies) && (entity->behavior == &actPlayer || entity->behavior == &actMonster) ) { continue; } - if ( entity->behavior == &actMonster && entity->getMonsterTypeFromSprite() == BAT_SMALL ) + if ( entity->behavior == &actMonster + && (entity->getMonsterTypeFromSprite() == BAT_SMALL + || entity->getMonsterTypeFromSprite() == REVENANT_SKULL + || entity->getMonsterTypeFromSprite() == MONSTER_ADORCISED_WEAPON + || entity->getMonsterTypeFromSprite() == HOLOGRAM + || entity->getMonsterTypeFromSprite() == MOTH_SMALL + || entity->getMonsterTypeFromSprite() == FLAME_ELEMENTAL + || entity->getMonsterTypeFromSprite() == EARTH_ELEMENTAL) ) { continue; } diff --git a/src/collision.hpp b/src/collision.hpp index 34b2db7a8..3b63dbb6d 100644 --- a/src/collision.hpp +++ b/src/collision.hpp @@ -13,6 +13,8 @@ #define LINETRACE_IGNORE_ENTITIES 1 #define LINETRACE_ATK_CHECK_FRIENDLYFIRE 2 +#define LINETRACE_TELEKINESIS 4 +#define LINETRACE_TOOLTIP_INTERACT 8 // function prototypes real_t entityDist(Entity* my, Entity* your); @@ -30,10 +32,10 @@ bool entityInsideEntity(Entity* entity1, Entity* entity2); bool entityInsideSomething(Entity* entity); int barony_clear(real_t tx, real_t ty, Entity* my); real_t clipMove(real_t* x, real_t* y, real_t vx, real_t vy, Entity* my); -Entity* findEntityInLine(Entity* my, real_t x1, real_t y1, real_t angle, int entities, Entity* target); +Entity* findEntityInLine(Entity* my, real_t x1, real_t y1, real_t angle, int entities, Entity* target, list_t* entityListToUse = nullptr); real_t lineTrace(Entity* my, real_t x1, real_t y1, real_t angle, real_t range, int entities, bool ground); -real_t lineTraceTarget(Entity* my, real_t x1, real_t y1, real_t angle, real_t range, int entities, bool ground, Entity* target); //If the linetrace function encounters the linetrace entity, it returns even if it's invisible or passable. -int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntityList = true, bool checkWalls = true, bool checkFloor = true); +real_t lineTraceTarget(Entity* my, real_t x1, real_t y1, real_t angle, real_t range, int entities, bool ground, Entity* target, list_t* entityListToUse = nullptr); //If the linetrace function encounters the linetrace entity, it returns even if it's invisible or passable. +int checkObstacle(long x, long y, Entity* my, Entity* target, bool useTileEntityList = true, bool checkWalls = true, bool checkFloor = true, bool checkEnemies = true); struct MonsterTrapIgnoreEntities_t { diff --git a/src/draw.cpp b/src/draw.cpp index f1f7945da..8a32122fd 100644 --- a/src/draw.cpp +++ b/src/draw.cpp @@ -150,199 +150,199 @@ static void buildSpriteShader( } void createCommonDrawResources() { - // framebuffer shader: - - framebuffer::mesh.init(); - + // framebuffer shader: + + framebuffer::mesh.init(); + static const char fb_vertex_glsl[] = "in vec3 iPosition;" "in vec2 iTexCoord;" - "out vec2 TexCoord;" + "out vec2 TexCoord;" "void main() {" "gl_Position = vec4(iPosition, 1.0);" - "TexCoord = iTexCoord;" + "TexCoord = iTexCoord;" "}"; static const char fb_fragment_glsl[] = - "in vec2 TexCoord;" + "in vec2 TexCoord;" "uniform sampler2D uTexture;" "uniform float uBrightness;" - "out vec4 FragColor;" + "out vec4 FragColor;" "void main() {" - "vec4 color = texture(uTexture, TexCoord);" + "vec4 color = texture(uTexture, TexCoord);" "FragColor = vec4(color.rgb * uBrightness, color.a);" "}"; - - framebuffer::shader.init("framebuffer"); - framebuffer::shader.compile(fb_vertex_glsl, sizeof(fb_vertex_glsl), Shader::Type::Vertex); - framebuffer::shader.compile(fb_fragment_glsl, sizeof(fb_fragment_glsl), Shader::Type::Fragment); - framebuffer::shader.bindAttribLocation("iPosition", 0); - framebuffer::shader.bindAttribLocation("iTexCoord", 1); - framebuffer::shader.link(); - framebuffer::shader.bind(); - GL_CHECK_ERR(glUniform1i(framebuffer::shader.uniform("uTexture"), 0)); - - static const char fb_hdr_fragment_glsl[] = - "in vec2 TexCoord;" - "uniform sampler2D uTexture;" - "uniform vec4 uBrightness;" - "uniform float uGamma;" - "uniform float uExposure;" - "out vec4 FragColor;" - - "void main() {" - "vec4 color = texture(uTexture, TexCoord);" - "vec3 mapped = color.rgb;" - - // reinhard tone-mapping - "mapped = vec3(1.0) - exp(-mapped * uExposure);" - - // another kind of reinhard tone mapping (pick one) - //"mapped = mapped * (uExposure / (1.0 + mapped / uExposure));" - // luma-based reinhard tone mapping (does not use exposure) - //"float luma = dot(mapped, vec3(0.2126, 0.7152, 0.0722));" - //"float toneMappedLuma = luma / (1.0 + luma);" - //"mapped = mapped * (toneMappedLuma / luma);" - - // additional tone-mapping examples - //https://www.shadertoy.com/view/lslGzl - - // gamma correction - "mapped = pow(mapped, vec3(1.0 / uGamma));" - - "FragColor = vec4(mapped, color.a) * uBrightness;" - "}"; - - framebuffer::hdrShader.init("hdr framebuffer"); - framebuffer::hdrShader.compile(fb_vertex_glsl, sizeof(fb_vertex_glsl), Shader::Type::Vertex); - framebuffer::hdrShader.compile(fb_hdr_fragment_glsl, sizeof(fb_hdr_fragment_glsl), Shader::Type::Fragment); - framebuffer::hdrShader.bindAttribLocation("iPosition", 0); - framebuffer::hdrShader.bindAttribLocation("iTexCoord", 1); - framebuffer::hdrShader.link(); - framebuffer::hdrShader.bind(); - GL_CHECK_ERR(glUniform1i(framebuffer::hdrShader.uniform("uTexture"), 0)); - - // create lightmap textures - for (int c = 0; c < MAXPLAYERS + 1; ++c) { - lightmapTexture[c] = new TempTexture(); - } - - // voxel shader: - - static const char vox_vertex_glsl[] = - "in vec3 iPosition;" - "in vec3 iColor;" - "in vec3 iNormal;" - "uniform mat4 uProj;" - "uniform mat4 uView;" - "uniform mat4 uModel;" - "out vec3 Color;" - "out vec4 WorldPos;" - "out vec3 Normal;" - - "void main() {" - "WorldPos = uModel * vec4(iPosition, 1.0);" - "gl_Position = uProj * uView * WorldPos;" - "Color = iColor;" - "Normal = (uModel * vec4(iNormal, 0.0)).xyz;" - "}"; + framebuffer::shader.init("framebuffer"); + framebuffer::shader.compile(fb_vertex_glsl, sizeof(fb_vertex_glsl), Shader::Type::Vertex); + framebuffer::shader.compile(fb_fragment_glsl, sizeof(fb_fragment_glsl), Shader::Type::Fragment); + framebuffer::shader.bindAttribLocation("iPosition", 0); + framebuffer::shader.bindAttribLocation("iTexCoord", 1); + framebuffer::shader.link(); + framebuffer::shader.bind(); + GL_CHECK_ERR(glUniform1i(framebuffer::shader.uniform("uTexture"), 0)); + + static const char fb_hdr_fragment_glsl[] = + "in vec2 TexCoord;" + "uniform sampler2D uTexture;" + "uniform vec4 uBrightness;" + "uniform float uGamma;" + "uniform float uExposure;" + "out vec4 FragColor;" - static const char vox_fragment_glsl[] = - "in vec3 Color;" - "in vec3 Normal;" - "in vec4 WorldPos;" - "uniform mat4 uColorRemap;" - "uniform vec4 uLightFactor;" - "uniform vec4 uLightColor;" - "uniform vec4 uColorAdd;" - "uniform vec4 uCameraPos;" - "uniform sampler2D uLightmap;" - "uniform vec2 uMapDims;" - "uniform float uFogDistance;" - "uniform vec4 uFogColor;" - "out vec4 FragColor;" - - "void main() {" - "vec3 Remapped =" - " (uColorRemap[0].rgb * Color.r)+" - " (uColorRemap[1].rgb * Color.g)+" - " (uColorRemap[2].rgb * Color.b);" - "vec2 TexCoord = WorldPos.xz / (uMapDims.xy * 32.0);" - "vec4 Lightmap = texture(uLightmap, TexCoord);" - "FragColor = vec4(Remapped, 1.0) * uLightFactor * (Lightmap + uLightColor) + uColorAdd;" - - "if (uFogDistance > 0.0) {" - "float dist = length(uCameraPos.xyz - WorldPos.xyz);" - "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" - "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" - "FragColor = vec4(mixed, FragColor.a);" - "}" - "}"; + "void main() {" + "vec4 color = texture(uTexture, TexCoord);" + "vec3 mapped = color.rgb;" + + // reinhard tone-mapping + "mapped = vec3(1.0) - exp(-mapped * uExposure);" + + // another kind of reinhard tone mapping (pick one) + //"mapped = mapped * (uExposure / (1.0 + mapped / uExposure));" + + // luma-based reinhard tone mapping (does not use exposure) + //"float luma = dot(mapped, vec3(0.2126, 0.7152, 0.0722));" + //"float toneMappedLuma = luma / (1.0 + luma);" + //"mapped = mapped * (toneMappedLuma / luma);" + + // additional tone-mapping examples + //https://www.shadertoy.com/view/lslGzl + + // gamma correction + "mapped = pow(mapped, vec3(1.0 / uGamma));" + + "FragColor = vec4(mapped, color.a) * uBrightness;" + "}"; + + framebuffer::hdrShader.init("hdr framebuffer"); + framebuffer::hdrShader.compile(fb_vertex_glsl, sizeof(fb_vertex_glsl), Shader::Type::Vertex); + framebuffer::hdrShader.compile(fb_hdr_fragment_glsl, sizeof(fb_hdr_fragment_glsl), Shader::Type::Fragment); + framebuffer::hdrShader.bindAttribLocation("iPosition", 0); + framebuffer::hdrShader.bindAttribLocation("iTexCoord", 1); + framebuffer::hdrShader.link(); + framebuffer::hdrShader.bind(); + GL_CHECK_ERR(glUniform1i(framebuffer::hdrShader.uniform("uTexture"), 0)); + + // create lightmap textures + for ( int c = 0; c < MAXPLAYERS + 1; ++c ) { + lightmapTexture[c] = new TempTexture(); + } + + // voxel shader: + + static const char vox_vertex_glsl[] = + "in vec3 iPosition;" + "in vec3 iColor;" + "in vec3 iNormal;" + "uniform mat4 uProj;" + "uniform mat4 uView;" + "uniform mat4 uModel;" + "out vec3 Color;" + "out vec4 WorldPos;" + "out vec3 Normal;" + + "void main() {" + "WorldPos = uModel * vec4(iPosition, 1.0);" + "gl_Position = uProj * uView * WorldPos;" + "Color = iColor;" + "Normal = (uModel * vec4(iNormal, 0.0)).xyz;" + "}"; + + static const char vox_fragment_glsl[] = + "in vec3 Color;" + "in vec3 Normal;" + "in vec4 WorldPos;" + "uniform mat4 uColorRemap;" + "uniform vec4 uLightFactor;" + "uniform vec4 uLightColor;" + "uniform vec4 uColorAdd;" + "uniform vec4 uCameraPos;" + "uniform sampler2D uLightmap;" + "uniform vec2 uMapDims;" + "uniform float uFogDistance;" + "uniform vec4 uFogColor;" + "out vec4 FragColor;" + + "void main() {" + "vec3 Remapped =" + " (uColorRemap[0].rgb * Color.r)+" + " (uColorRemap[1].rgb * Color.g)+" + " (uColorRemap[2].rgb * Color.b);" + "vec2 TexCoord = WorldPos.xz / (uMapDims.xy * 32.0);" + "vec4 Lightmap = texture(uLightmap, TexCoord);" + "FragColor = vec4(Remapped, 1.0) * uLightFactor * (Lightmap + uLightColor) + uColorAdd;" + + "if (uFogDistance > 0.0) {" + "float dist = length(uCameraPos.xyz - WorldPos.xyz);" + "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" + "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" + "FragColor = vec4(mixed, FragColor.a);" + "}" + "}"; buildVoxelShader(voxelShader, "voxelShader", true, vox_vertex_glsl, sizeof(vox_vertex_glsl), vox_fragment_glsl, sizeof(vox_fragment_glsl)); - - static const char vox_bright_fragment_glsl[] = - "in vec3 Color;" - "in vec3 Normal;" - "in vec4 WorldPos;" - "uniform mat4 uColorRemap;" - "uniform vec4 uLightFactor;" - "uniform vec4 uLightColor;" - "uniform vec4 uColorAdd;" - "uniform vec4 uCameraPos;" - "uniform float uFogDistance;" - "uniform vec4 uFogColor;" - "out vec4 FragColor;" - - "void main() {" - "vec3 Remapped =" - " (uColorRemap[0].rgb * Color.r)+" - " (uColorRemap[1].rgb * Color.g)+" - " (uColorRemap[2].rgb * Color.b);" - "FragColor = vec4(Remapped, 1.0) * uLightFactor * uLightColor + uColorAdd;" - - "if (uFogDistance > 0.0) {" - "float dist = length(uCameraPos.xyz - WorldPos.xyz);" - "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" - "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" - "FragColor = vec4(mixed, FragColor.a);" - "}" - "}"; - buildVoxelShader(voxelBrightShader, "voxelBrightShader", false, - vox_vertex_glsl, sizeof(vox_vertex_glsl), - vox_bright_fragment_glsl, sizeof(vox_bright_fragment_glsl)); + static const char vox_bright_fragment_glsl[] = + "in vec3 Color;" + "in vec3 Normal;" + "in vec4 WorldPos;" + "uniform mat4 uColorRemap;" + "uniform vec4 uLightFactor;" + "uniform vec4 uLightColor;" + "uniform vec4 uColorAdd;" + "uniform vec4 uCameraPos;" + "uniform float uFogDistance;" + "uniform vec4 uFogColor;" + "out vec4 FragColor;" + + "void main() {" + "vec3 Remapped =" + " (uColorRemap[0].rgb * Color.r)+" + " (uColorRemap[1].rgb * Color.g)+" + " (uColorRemap[2].rgb * Color.b);" + "FragColor = vec4(Remapped, 1.0) * uLightFactor * uLightColor + uColorAdd;" + + "if (uFogDistance > 0.0) {" + "float dist = length(uCameraPos.xyz - WorldPos.xyz);" + "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" + "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" + "FragColor = vec4(mixed, FragColor.a);" + "}" + "}"; + + buildVoxelShader(voxelBrightShader, "voxelBrightShader", false, + vox_vertex_glsl, sizeof(vox_vertex_glsl), + vox_bright_fragment_glsl, sizeof(vox_bright_fragment_glsl)); static const char vox_dithered_fragment_glsl[] = "in vec3 Color;" - "in vec3 Normal;" + "in vec3 Normal;" "in vec4 WorldPos;" - "uniform float uDitherAmount;" + "uniform float uDitherAmount;" "uniform mat4 uColorRemap;" - "uniform vec4 uLightFactor;" - "uniform vec4 uLightColor;" - "uniform vec4 uColorAdd;" - "uniform vec4 uCameraPos;" - "uniform sampler2D uLightmap;" + "uniform vec4 uLightFactor;" + "uniform vec4 uLightColor;" + "uniform vec4 uColorAdd;" + "uniform vec4 uCameraPos;" + "uniform sampler2D uLightmap;" "uniform vec2 uMapDims;" - "uniform float uFogDistance;" - "uniform vec4 uFogColor;" - "out vec4 FragColor;" - - "void dither(ivec2 pos, float amount) {" - "if (amount > 1.0) {" - "int d = int(amount) - 1;" - "if ((pos.x & d) == 0 && (pos.y & d) == 0) { discard; }" - "} else if (amount == 1.0) {" - "if (((pos.x + pos.y) & 1) == 0) { discard; }" - "} else if (amount < 1.0) {" - "int d = int(1.0 / amount) - 1;" - "if ((pos.x & d) != 0 || (pos.y & d) != 0) { discard; }" - "}" - "}" + "uniform float uFogDistance;" + "uniform vec4 uFogColor;" + "out vec4 FragColor;" + + "void dither(ivec2 pos, float amount) {" + "if (amount > 1.0) {" + "int d = int(amount) - 1;" + "if ((pos.x & d) == 0 && (pos.y & d) == 0) { discard; }" + "} else if (amount == 1.0) {" + "if (((pos.x + pos.y) & 1) == 0) { discard; }" + "} else if (amount < 1.0) {" + "int d = int(1.0 / amount) - 1;" + "if ((pos.x & d) != 0 || (pos.y & d) != 0) { discard; }" + "}" + "}" "void main() {" "dither(ivec2(gl_FragCoord), uDitherAmount);" @@ -351,15 +351,15 @@ void createCommonDrawResources() { " (uColorRemap[1].rgb * Color.g)+" " (uColorRemap[2].rgb * Color.b);" "vec2 TexCoord = WorldPos.xz / (uMapDims.xy * 32.0);" - "vec4 Lightmap = texture(uLightmap, TexCoord);" - "FragColor = vec4(Remapped, 1.0) * uLightFactor * (Lightmap + uLightColor) + uColorAdd;" - - "if (uFogDistance > 0.0) {" - "float dist = length(uCameraPos.xyz - WorldPos.xyz);" - "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" - "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" - "FragColor = vec4(mixed, FragColor.a);" - "}" + "vec4 Lightmap = texture(uLightmap, TexCoord);" + "FragColor = vec4(Remapped, 1.0) * uLightFactor * (Lightmap + uLightColor) + uColorAdd;" + + "if (uFogDistance > 0.0) {" + "float dist = length(uCameraPos.xyz - WorldPos.xyz);" + "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" + "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" + "FragColor = vec4(mixed, FragColor.a);" + "}" "}"; buildVoxelShader(voxelDitheredShader, "voxelDitheredShader", true, @@ -413,126 +413,126 @@ void createCommonDrawResources() { buildVoxelShader(voxelBrightDitheredShader, "voxelBrightDitheredShader", true, vox_vertex_glsl, sizeof(vox_vertex_glsl), vox_bright_dithered_fragment_glsl, sizeof(vox_bright_dithered_fragment_glsl)); - - // world shader: - - static const char world_vertex_glsl[] = - "in vec3 iPosition;" - "in vec2 iTexCoord;" - "in vec3 iColor;" - "uniform mat4 uProj;" - "uniform mat4 uView;" - "out vec2 TexCoord;" - "out vec3 Color;" - "out vec4 WorldPos;" - - "void main() {" - "WorldPos = vec4(iPosition, 1.0);" - "gl_Position = uProj * uView * WorldPos;" - "TexCoord = iTexCoord;" - "Color = iColor;" - "}"; - - static const char world_fragment_glsl[] = - "in vec2 TexCoord;" - "in vec3 Color;" - "in vec4 WorldPos;" - "uniform vec4 uLightFactor;" - "uniform sampler2D uTextures;" - "uniform sampler2D uLightmap;" - "uniform vec2 uMapDims;" - "uniform vec4 uCameraPos;" - "uniform float uFogDistance;" - "uniform vec4 uFogColor;" - "out vec4 FragColor;" - - "void main() {" - "vec2 LightCoord = WorldPos.xz / (uMapDims.xy * 32.0);" - "vec4 Lightmap = texture(uLightmap, LightCoord);" - "FragColor = texture(uTextures, TexCoord) * vec4(Color, 1.f) * uLightFactor * Lightmap;" - - "if (uFogDistance > 0.0) {" - "float dist = length(uCameraPos.xyz - WorldPos.xyz);" - "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" - "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" - "FragColor = vec4(mixed, FragColor.a);" - "}" - "}"; - - buildWorldShader(worldShader, "worldShader", true, - world_vertex_glsl, sizeof(world_vertex_glsl), - world_fragment_glsl, sizeof(world_fragment_glsl)); - - static const char world_dithered_fragment_glsl[] = - "in vec2 TexCoord;" - "in vec3 Color;" - "in vec4 WorldPos;" - "uniform float uDitherAmount;" - "uniform vec4 uLightFactor;" - "uniform sampler2D uTextures;" - "uniform sampler2D uLightmap;" - "uniform vec2 uMapDims;" - "uniform vec4 uCameraPos;" - "uniform float uFogDistance;" - "uniform vec4 uFogColor;" - "out vec4 FragColor;" - - "void dither(ivec2 pos, float amount) {" - "if (amount > 1.0) {" - "int d = int(amount) - 1;" - "if ((pos.x & d) == 0 && (pos.y & d) == 0) { discard; }" - "} else if (amount == 1.0) {" - "if (((pos.x + pos.y) & 1) == 0) { discard; }" - "} else if (amount < 1.0) {" - "int d = int(1.0 / amount) - 1;" - "if ((pos.x & d) != 0 || (pos.y & d) != 0) { discard; }" - "}" - "}" - - "void main() {" - "dither(ivec2(gl_FragCoord), uDitherAmount);" - "vec2 LightCoord = WorldPos.xz / (uMapDims.xy * 32.0);" - "vec4 Lightmap = texture(uLightmap, LightCoord);" - "FragColor = texture(uTextures, TexCoord) * vec4(Color, 1.f) * uLightFactor * Lightmap;" - - "if (uFogDistance > 0.0) {" - "float dist = length(uCameraPos.xyz - WorldPos.xyz);" - "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" - "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" - "FragColor = vec4(mixed, FragColor.a);" - "}" - "}"; - - buildWorldShader(worldDitheredShader, "worldDitheredShader", true, - world_vertex_glsl, sizeof(world_vertex_glsl), - world_dithered_fragment_glsl, sizeof(world_dithered_fragment_glsl)); - - static const char world_dark_fragment_glsl[] = - "in vec2 TexCoord;" - "in vec3 Color;" - "in vec4 WorldPos;" - "out vec4 FragColor;" - "void main() {" - "FragColor = vec4(0.0, 0.0, 0.0, 1.0);" - "}"; - - buildWorldShader(worldDarkShader, "worldDarkShader", false, - world_vertex_glsl, sizeof(world_vertex_glsl), - world_dark_fragment_glsl, sizeof(world_dark_fragment_glsl)); - - // sky shader: - - static const char sky_vertex_glsl[] = - "in vec3 iPosition;" - "in vec2 iTexCoord;" - "in vec4 iColor;" - "uniform mat4 uProj;" - "uniform mat4 uView;" - "uniform vec2 uScroll;" - "out vec2 TexCoord;" - "out vec2 Scroll;" - "out vec4 Color;" - + + // world shader: + + static const char world_vertex_glsl[] = + "in vec3 iPosition;" + "in vec2 iTexCoord;" + "in vec3 iColor;" + "uniform mat4 uProj;" + "uniform mat4 uView;" + "out vec2 TexCoord;" + "out vec3 Color;" + "out vec4 WorldPos;" + + "void main() {" + "WorldPos = vec4(iPosition, 1.0);" + "gl_Position = uProj * uView * WorldPos;" + "TexCoord = iTexCoord;" + "Color = iColor;" + "}"; + + static const char world_fragment_glsl[] = + "in vec2 TexCoord;" + "in vec3 Color;" + "in vec4 WorldPos;" + "uniform vec4 uLightFactor;" + "uniform sampler2D uTextures;" + "uniform sampler2D uLightmap;" + "uniform vec2 uMapDims;" + "uniform vec4 uCameraPos;" + "uniform float uFogDistance;" + "uniform vec4 uFogColor;" + "out vec4 FragColor;" + + "void main() {" + "vec2 LightCoord = WorldPos.xz / (uMapDims.xy * 32.0);" + "vec4 Lightmap = texture(uLightmap, LightCoord);" + "FragColor = texture(uTextures, TexCoord) * vec4(Color, 1.f) * uLightFactor * Lightmap;" + + "if (uFogDistance > 0.0) {" + "float dist = length(uCameraPos.xyz - WorldPos.xyz);" + "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" + "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" + "FragColor = vec4(mixed, FragColor.a);" + "}" + "}"; + + buildWorldShader(worldShader, "worldShader", true, + world_vertex_glsl, sizeof(world_vertex_glsl), + world_fragment_glsl, sizeof(world_fragment_glsl)); + + static const char world_dithered_fragment_glsl[] = + "in vec2 TexCoord;" + "in vec3 Color;" + "in vec4 WorldPos;" + "uniform float uDitherAmount;" + "uniform vec4 uLightFactor;" + "uniform sampler2D uTextures;" + "uniform sampler2D uLightmap;" + "uniform vec2 uMapDims;" + "uniform vec4 uCameraPos;" + "uniform float uFogDistance;" + "uniform vec4 uFogColor;" + "out vec4 FragColor;" + + "void dither(ivec2 pos, float amount) {" + "if (amount > 1.0) {" + "int d = int(amount) - 1;" + "if ((pos.x & d) == 0 && (pos.y & d) == 0) { discard; }" + "} else if (amount == 1.0) {" + "if (((pos.x + pos.y) & 1) == 0) { discard; }" + "} else if (amount < 1.0) {" + "int d = int(1.0 / amount) - 1;" + "if ((pos.x & d) != 0 || (pos.y & d) != 0) { discard; }" + "}" + "}" + + "void main() {" + "dither(ivec2(gl_FragCoord), uDitherAmount);" + "vec2 LightCoord = WorldPos.xz / (uMapDims.xy * 32.0);" + "vec4 Lightmap = texture(uLightmap, LightCoord);" + "FragColor = texture(uTextures, TexCoord) * vec4(Color, 1.f) * uLightFactor * Lightmap;" + + "if (uFogDistance > 0.0) {" + "float dist = length(uCameraPos.xyz - WorldPos.xyz);" + "float lerp = (min(dist, uFogDistance) / uFogDistance) * uFogColor.a;" + "vec3 mixed = mix(FragColor.rgb, uFogColor.rgb, lerp);" + "FragColor = vec4(mixed, FragColor.a);" + "}" + "}"; + + buildWorldShader(worldDitheredShader, "worldDitheredShader", true, + world_vertex_glsl, sizeof(world_vertex_glsl), + world_dithered_fragment_glsl, sizeof(world_dithered_fragment_glsl)); + + static const char world_dark_fragment_glsl[] = + "in vec2 TexCoord;" + "in vec3 Color;" + "in vec4 WorldPos;" + "out vec4 FragColor;" + "void main() {" + "FragColor = vec4(0.0, 0.0, 0.0, 1.0);" + "}"; + + buildWorldShader(worldDarkShader, "worldDarkShader", false, + world_vertex_glsl, sizeof(world_vertex_glsl), + world_dark_fragment_glsl, sizeof(world_dark_fragment_glsl)); + + // sky shader: + + static const char sky_vertex_glsl[] = + "in vec3 iPosition;" + "in vec2 iTexCoord;" + "in vec4 iColor;" + "uniform mat4 uProj;" + "uniform mat4 uView;" + "uniform vec2 uScroll;" + "out vec2 TexCoord;" + "out vec2 Scroll;" + "out vec4 Color;" + "void main() {" "mat4 View = uView;" "View[3] = vec4(0.f, 0.f, 0.f, 1.f);" @@ -1765,6 +1765,7 @@ void raycast(const view_t& camera, Sint8 (*minimap)[MINIMAP_MAX_DIMENSION], bool TimeTest = true; }); #endif + static real_t raycastMaxDist = 16.0; // ray shooting functor struct outs_t { @@ -1865,11 +1866,14 @@ void raycast(const view_t& camera, Sint8 (*minimap)[MINIMAP_MAX_DIMENSION], bool // check against tiles in each map layer bool zhit[MAPLAYERS] = { false }; for (int z = 0; z < MAPLAYERS; z++) { - if (tiles[z + iny * MAPLAYERS + inx * MAPLAYERS * mh] && d > dstart) { // hit something solid + if (tiles[z + iny * MAPLAYERS + inx * MAPLAYERS * mh] + && !(z > 0 && tiles[z + iny * MAPLAYERS + inx * MAPLAYERS * mh] == TRANSPARENT_TILE) + && d > dstart) { // hit something solid zhit[z] = true; // collect light information - if (tiles[z + iny2 * MAPLAYERS + inx2 * MAPLAYERS * mh]) { + if (tiles[z + iny2 * MAPLAYERS + inx2 * MAPLAYERS * mh] + && !(z > 0 && tiles[z + iny2 * MAPLAYERS + inx2 * MAPLAYERS * mh] == TRANSPARENT_TILE)) { continue; } auto& l = lights[iny2 + inx2 * mh]; @@ -1886,7 +1890,7 @@ void raycast(const view_t& camera, Sint8 (*minimap)[MINIMAP_MAX_DIMENSION], bool } // update minimap - if (d < 16 && z == OBSTACLELAYER) { + if (d < raycastMaxDist && z == OBSTACLELAYER) { if ( visible ) { // wall space if (WriteOutsSequentially) { @@ -1925,8 +1929,9 @@ void raycast(const view_t& camera, Sint8 (*minimap)[MINIMAP_MAX_DIMENSION], bool } } - if (d < 16) { - if ( visible && tiles[iny * MAPLAYERS + inx * MAPLAYERS * mh]) { + if (d < raycastMaxDist ) { + if ( visible && + tiles[iny * MAPLAYERS + inx * MAPLAYERS * mh] ) { // walkable space if (WriteOutsSequentially) { if ( ins.fillWithColor ) @@ -1947,7 +1952,8 @@ void raycast(const view_t& camera, Sint8 (*minimap)[MINIMAP_MAX_DIMENSION], bool minimap[iny][inx] = 3; } } - } else if (tiles[z + iny * MAPLAYERS + inx * MAPLAYERS * mh]) { + } else if (tiles[z + iny * MAPLAYERS + inx * MAPLAYERS * mh] + && tiles[z + iny * MAPLAYERS + inx * MAPLAYERS * mh] != TRANSPARENT_TILE) { // no floor /*if (WriteOutsSequentially) { result.push_back({inx, iny, 0}); @@ -1979,12 +1985,30 @@ void raycast(const view_t& camera, Sint8 (*minimap)[MINIMAP_MAX_DIMENSION], bool // shoot the rays const vec4_t* lightmap = lightmaps[0].data(); + int player = -1; for (int c = 0; c < MAXPLAYERS; ++c) { if (&camera == &cameras[c]) { lightmap = lightmaps[c + 1].data(); + player = c; break; } } + +#ifndef EDITOR + raycastMaxDist = 16.0; + if ( player >= 0 ) + { + if ( !strncmp(map.filename, "fortress", 8) ) + { + raycastMaxDist = 6.0; + } + else if ( !strncmp(map.name, "Caves", 5) ) + { + raycastMaxDist = 8.0; + } + } +#endif + if (DoRaysInParallel) { std::vector>> tasks; for (int x = 0; x < NumRays; x += NumRaysPerJob) { @@ -2128,10 +2152,12 @@ void drawEntities3D(view_t* camera, int mode) } } else if ( entity->behavior == &actHudAdditional + || entity->behavior == &actHudAdditional2 || entity->behavior == &actHudArrowModel || entity->behavior == &actHudShield || entity->behavior == &actLeftHandMagic - || entity->behavior == &actRightHandMagic ) + || entity->behavior == &actRightHandMagic + || entity->behavior == &actMagicRangefinder) { if ( entity->skill[2] != currentPlayerViewport ) { @@ -2140,42 +2166,85 @@ void drawEntities3D(view_t* camera, int mode) } } } + +#ifndef EDITOR + if ( currentPlayerViewport >= 0 ) + { + if ( entity->behavior == &actTouchCastThirdPersonParticle ) + { + if ( entity->skill[11] == currentPlayerViewport ) + { + if ( !players[currentPlayerViewport]->entity + || (players[currentPlayerViewport]->entity && !players[currentPlayerViewport]->entity->skill[3]) ) + { + // gib don't draw for local player, if not third person + continue; + } + } + } + else if ( (entity->behavior == &actGib && entity->actGibDisableDrawForLocalPlayer > 0) ) + { + if ( (entity->actGibDisableDrawForLocalPlayer - 1) == currentPlayerViewport ) + { + if ( !players[currentPlayerViewport]->entity + || (players[currentPlayerViewport]->entity && !players[currentPlayerViewport]->entity->skill[3]) ) + { + // gib don't draw for local player, if not third person + continue; + } + } + } + } +#endif // update dithering auto& dither = entity->dithering[camera]; if (ticks != dither.lastUpdateTick) { dither.lastUpdateTick = ticks; bool decrease = false; - if ( !entity->flags[OVERDRAW] ) + if ( !entity->flags[OVERDRAW] && !entity->flags[ENTITY_SKIP_CULLING] ) { const int x = entity->x / 16; const int y = entity->y / 16; if (x >= 0 && y >= 0 && x < map.width && y < map.height) { if ( !camera->vismap[y + x * map.height] - && entity->monsterEntityRenderAsTelepath != 1 + && entity->monsterEntityRenderAsTelepath == 0 +#ifndef EDITOR + && !(!intro && entity->goldTelepathy > 0 && entity->behavior == &actGoldBag + && currentPlayerViewport >= 0 && currentPlayerViewport < MAXPLAYERS + && entity->goldTelepathy & (1 << currentPlayerViewport)) + && !(!intro && entity->colliderTelepathy > 0 && entity->behavior == &actColliderDecoration + && currentPlayerViewport >= 0 && currentPlayerViewport < MAXPLAYERS + && entity->colliderTelepathy & (1 << currentPlayerViewport)) +#endif && !(entity->behavior == &actSpriteNametag && entity->ditheringDisabled) ) { decrease = true; goto end; } } - const real_t rx = entity->x / 16.0; - const real_t ry = entity->y / 16.0; - if ( behindCamera(*camera, rx, ry) ) - { - decrease = true; - goto end; - } + + const real_t rx = entity->x / 16.0; + const real_t ry = entity->y / 16.0; + if ( behindCamera(*camera, rx, ry) ) + { + decrease = true; + goto end; + } } - end: - if (entity->ditheringDisabled) { + end: + if ( entity->ditheringOverride >= 0 ) + { + dither.value = std::max(0, std::min(entity->ditheringOverride, Entity::Dither::MAX)); + } + else if (entity->ditheringDisabled) { dither.value = decrease ? 0 : Entity::Dither::MAX; } else { if (ditheringDisabled) { dither.value = decrease ? 0 : Entity::Dither::MAX; } else { - if ( entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER] ) + if ( (entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER]) || entity->flags[STASIS_DITHER] ) { #ifndef EDITOR static ConsoleVariable cvar_dither_invisibility("/dither_invisibility", 5); @@ -2184,6 +2253,17 @@ void drawEntities3D(view_t* camera, int mode) #else dither.value = decrease ? std::max(0, dither.value - 2) : dither.value + 1; +#endif + } + else if ( entity->mistformGLRender >= 0.45 ) + { +#ifndef EDITOR + static ConsoleVariable cvar_dither_mistform("/dither_mistform", 6); + dither.value = decrease ? std::max(0, dither.value - 2) : + std::min(*cvar_dither_mistform, dither.value + 1); +#else + dither.value = decrease ? std::max(0, dither.value - 2) : + dither.value + 1; #endif } else @@ -2200,7 +2280,7 @@ void drawEntities3D(view_t* camera, int mode) // don't draw hud weapons if we're being a telepath. they get in the way of world models if (entity->flags[OVERDRAW] && currentPlayerViewport >= 0) { - if (stats[currentPlayerViewport]->EFFECTS[EFF_TELEPATH]) { + if (stats[currentPlayerViewport]->getEffectActive(EFF_TELEPATH)) { continue; } } @@ -2440,6 +2520,15 @@ void drawEntities3D(view_t* camera, int mode) { glDrawSprite(camera, entity, mode); } + else if ( entity->skill[7] == 3 ) + { + glDrawSpriteFromImage(camera, entity, Language::get(6464), mode); + } + else if ( entity->skill[7] == 4 ) + { + snprintf(buf, sizeof(buf), "DPS: %d", entity->skill[0]); + glDrawSpriteFromImage(camera, entity, buf, mode); + } else { snprintf(buf, sizeof(buf), "%d", entity->skill[0]); @@ -2537,6 +2626,35 @@ void drawEntities2D(long camx, long camy) drawImageScaled(itemSprite(tmpItem), nullptr, &pos); free(tmpItem); } + else if ( entity->sprite == 21 ) + { + pos.y += sprites[entity->sprite]->h / 2; + pos.x += sprites[entity->sprite]->w / 2; + // chest + switch ( (int)entity->yaw ) + { + case 0: + pos.y += pos.h; + drawImageRotatedAlpha(sprites[entity->sprite], nullptr, &pos, 3 * PI / 2, 255); + break; + case 1: + pos.x -= pos.w; + drawImageRotatedAlpha(sprites[entity->sprite], nullptr, &pos, 0.f, 255); + break; + case 2: + pos.y -= pos.h; + drawImageRotatedAlpha(sprites[entity->sprite], nullptr, &pos, PI / 2, 255); + break; + case 3: + pos.x += pos.w; + drawImageRotatedAlpha(sprites[entity->sprite], nullptr, &pos, PI, 255); + break; + default: + // draw sprite normally from sprites list + drawImageScaled(sprites[entity->sprite], nullptr, &pos); + break; + } + } else if ( entity->sprite == 133 ) { pos.y += sprites[entity->sprite]->h / 2; @@ -2587,6 +2705,18 @@ void drawEntities2D(long camx, long camy) } else { + if ( entity->sprite == 131 ) // light source draw rgb + { + box.w = 12; + box.h = 12; + box.x = pos.x + 16; + box.y = pos.y + 4; + drawRect(&box, makeColorRGB( + (entity->lightSourceRGB >> 0) & 0xFF, + (entity->lightSourceRGB >> 8) & 0xFF, + (entity->lightSourceRGB >> 16) & 0xFF), 255); + } + // draw sprite normally from sprites list drawImageScaled(sprites[entity->sprite], nullptr, &pos); } @@ -2650,7 +2780,7 @@ void drawEntities2D(long camx, long camy) pos.h = TEXTURESIZE; //ttfPrintText(ttf8, 100, 100, inputstr); debug any errant text input in editor - if ( entity->sprite >= 0 && entity->sprite < numsprites ) + if ( entity->sprite >= 0 && entity->sprite < numsprites && entity->sprite < spriteEditorNameStrings.size() ) { if ( sprites[entity->sprite] != nullptr ) { @@ -2677,8 +2807,17 @@ void drawEntities2D(long camx, long camy) snprintf(tmpStr, 10, "Level: %d", entity->getStats()->LVL); ttfPrintText(ttf8, padx, pady + 20, tmpStr); } + break; + case 15: // light source + pady += 15; + strcpy(tmpStr, spriteEditorNameStrings[selectedEntity[0]->sprite]); + ttfPrintText(ttf8, padx, pady, tmpStr); - + snprintf(tmpStr, sizeof(tmpStr), "R: %d G: %d B: %d", + (selectedEntity[0]->lightSourceRGB >> 0) & 0xFF, + (selectedEntity[0]->lightSourceRGB >> 8) & 0xFF, + (selectedEntity[0]->lightSourceRGB >> 16) & 0xFF); + ttfPrintText(ttf8, padx, pady + 10, tmpStr); break; case 2: //chest pady += 5; @@ -3115,6 +3254,7 @@ void drawEntities2D(long camx, long camy) else if ( (omousex / TEXTURESIZE) * 32 == pos.x && (omousey / TEXTURESIZE) * 32 == pos.y && selectedEntity[0] == NULL + && entity->sprite < spriteEditorNameStrings.size() && hovertext ) { @@ -3717,7 +3857,12 @@ static inline bool testTileOccludes(const map_t& map, int index) { assert(index >= 0 && index <= map.width * map.height * MAPLAYERS - MAPLAYERS); const Uint64& t0 = *(Uint64*)&map.tiles[index]; const Uint32& t1 = *(Uint32*)&map.tiles[index + 2]; - return (t0 & 0xffffffff00000000) && (t0 & 0x00000000ffffffff) && t1; + return (t0 & 0xffffffff00000000) // is floor != 0 + && (t0 & 0x00000000ffffffff) // is obstacle layer != 0 + && t1 // is ceiling != 0 + && (((t0 & 0xffffffff00000000) >> 32) != TRANSPARENT_TILE) // is floor != TRANSPARENT_TILE + && ((t0 & 0x00000000ffffffff) != TRANSPARENT_TILE) // is obstacle layer != TRANSPARENT_TILE + && (t1 != TRANSPARENT_TILE); // is ceiling != TRANSPARENT_TILE } void occlusionCulling(map_t& map, view_t& camera) diff --git a/src/draw.hpp b/src/draw.hpp index 4819d7ebe..84fd76e76 100644 --- a/src/draw.hpp +++ b/src/draw.hpp @@ -440,4 +440,15 @@ extern bool hdrEnabled; extern ConsoleVariable cvar_hdrBrightness; extern ConsoleVariable cvar_fogDistance; extern ConsoleVariable cvar_fogColor; +extern ConsoleVariable cvar_hdrExposure; +extern ConsoleVariable cvar_hdrGamma; +extern ConsoleVariable cvar_hdrAdjustment; +extern ConsoleVariable cvar_hdrLimitHigh; +extern ConsoleVariable cvar_hdrLimitLow; +extern const Vector4 defaultBrightness; +extern const float defaultGamma; +extern const float defaultExposure; +extern const float defaultAdjustmentRate; +extern const float defaultLimitHigh; +extern const float defaultLimitLow; #endif diff --git a/src/editor.cpp b/src/editor.cpp index 7cfab087c..fdbe957e2 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -56,7 +56,7 @@ std::vector mapNames; std::list modFolderNames; std::string physfs_saveDirectory = BASE_DATA_DIR; std::string physfs_openDirectory = BASE_DATA_DIR; -float limbs[NUMMONSTERS][20][3]; // dummy variable for files.cpp limbs reloading in Barony. +float limbs[NUMMONSTERS][30][3]; // dummy variable for files.cpp limbs reloading in Barony. void buttonStartSingleplayer(button_t* my) {} // dummy function for mod_tools.cpp void steamStatisticUpdate(int statisticNum, ESteamStatTypes type, int value) {} // dummy function for mod_tools.cpp //AchievementObserver achievementObserver; // dummy function for mod_tools.cpp @@ -74,10 +74,15 @@ void actHUDMagicParticle(Entity* my) {} // dummy for draw.cpp void actHUDMagicParticleCircling(Entity* my) {} // dummy for draw.cpp void actHudShield(Entity* my) {} // dummy for draw.cpp void actHudAdditional(Entity* my) {} // dummy for draw.cpp +void actHudAdditional2(Entity* my) {} // dummy for draw.cpp void actHudArrowModel(Entity* my) {} // dummy for draw.cpp void actLeftHandMagic(Entity* my) {} // dummy for draw.cpp void actRightHandMagic(Entity* my) {} // dummy for draw.cpp +void actMagicRangefinder(Entity* my) {} // dummy for draw.cpp +void actTouchCastThirdPersonParticle(Entity* my) {} // dummy for draw.cpp +void actSprite(Entity* my) {} // dummy for draw.cpp bool messagePlayer(int player, Uint32 type, char const * const message, ...) {return true;} // dummy +bool itemTypeIsFoci(const ItemType type) { return false; } // dummy map_t copymap; @@ -232,13 +237,19 @@ char furniturePropertyNames[1][19] = "Direction (-1 - 7)" }; -char floorDecorationPropertyNames[9][59] = +char windPropertyNames[1][19] = +{ + "Direction (-1 - 7)" +}; + +char floorDecorationPropertyNames[10][59] = { "Model texture to use (0-9999)", "Direction (-1 - 7)", "Height Offset (Qtrs of a voxel, +ive is higher)", "X Offset (L/R, Qtrs of a voxel, +ive is right)", "Y Offset (U/D, Qtrs of a voxel, +ive is down)", + "Destroy if no wall (-1 - 8)", "Interact Text", "", "", @@ -269,7 +280,7 @@ char soundSourcePropertyNames[5][59] = "Sound origin (0 = this entity, 1 = global)" }; -char lightSourcePropertyNames[7][48] = +char lightSourcePropertyNames[10][48] = { "Light always on (0-1)", "Brightness (0-255)", @@ -277,7 +288,10 @@ char lightSourcePropertyNames[7][48] = "Light/unlight once only (0-1)", "Tile radius of light source (0-64)", "Light flicker enable (0-1)", - "Activation delay (0-9999 ticks, 50 ticks / sec)" + "Activation delay (0-9999 ticks, 50 ticks / sec)", + "Color (R, G, B) (0-255):", + "", + "" }; char textSourcePropertyNames[10][45] = @@ -352,6 +366,14 @@ char doorPropertyNames[3][42] = "Disable unlocking with spell (0-1)" }; +char doorIronPropertyNames[4][42] = +{ + "Unlock When Powered (0-1)", + "Disable unlocking with lockpick (0-1)", + "Disable unlocking with spell (0-1)", + "Force Door Locked/Unlocked (0-2)" +}; + char gatePropertyNames[1][35] = { "Disable unlocking with spell (0-1)" @@ -368,6 +390,22 @@ char statuePropertyNames[2][16] = "Statue ID", }; +char wallLockPropertyNames[6][32] = +{ + "Material (0 - 7)", + "Invert Power (0 - 1)", + "Key Turnable (0 - 1)", + "Lockpickable (-1 - 100)", + "Skeleton Key Usable (0 - 1)", + "Auto Gen Matching Key (0 - 1)" +}; + +char wallButtonPropertyNames[2][49] = +{ + "Invert Power (0 - 1)", + "Deactivation Time (0-9999 ticks)" +}; + const char* playerClassLangEntry(int classnum, int playernum) { if ( classnum >= CLASS_BARBARIAN && classnum <= CLASS_JOKER ) @@ -2126,6 +2164,8 @@ int main(int argc, char** argv) textInsertCaratPosition = -1; } + GL_CHECK_ERR(glViewport(0, 0, xres, yres)); // fix for resizing editor with hdr enabled. + if ( !spritepalette && !tilepalette ) { allowediting = 1; @@ -4145,7 +4185,7 @@ int main(int argc, char** argv) SDL_StartTextInput(); inputstr = spriteProperties[0]; } - if ( editproperty == 2 ) + if ( editproperty == 2 || editproperty == 3 ) { inputlen = 3; } @@ -5988,15 +6028,15 @@ int main(int argc, char** argv) Uint32 color = makeColorRGB(0, 255, 0); Uint32 colorRandom = makeColorRGB(0, 168, 255); Uint32 colorError = makeColorRGB(255, 0, 0); - + std::string rotationStr = ""; for ( int i = 0; i < numProperties; i++ ) { int propertyInt = atoi(spriteProperties[i]); - if ( i >= 5 && i <= 8 ) + if ( i >= 6 && i <= 9 ) { inputFieldWidth = subx2 - inputField_x; // width of the text field - if ( i > 5 ) + if ( i > 6 ) { spacing = 18; } @@ -6014,9 +6054,9 @@ int main(int argc, char** argv) inputFieldHeader_y = suby1 + 28 + i * spacing; inputField_y = inputFieldHeader_y + 16; - if ( i > 5 && i <= 8 ) + if ( i > 6 && i <= 9 ) { - int offsetBoxes = 2.5 * 36; + int offsetBoxes = 3 * 36; inputFieldHeader_y = suby1 + 28 + i * spacing + offsetBoxes; inputField_y = inputFieldHeader_y + 16; // box outlines then text @@ -6097,6 +6137,7 @@ int main(int argc, char** argv) break; } printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, tmpStr); + rotationStr = tmpStr; } } else if ( i == 2 || i == 3 || i == 4 ) @@ -6106,6 +6147,53 @@ int main(int argc, char** argv) propertyPageError(i, 0); // reset to default 0. } } + else if ( i == 5 ) + { + if ( propertyInt > 8 || propertyInt < -1 ) + { + propertyPageError(i, -1); // reset to default -1 + } + else + { + char tmpStr[32] = ""; + switch ( propertyInt ) + { + case -1: + strcpy(tmpStr, "n/a"); + break; + case 0: + strcpy(tmpStr, "East"); + break; + case 1: + strcpy(tmpStr, "Southeast"); + break; + case 2: + strcpy(tmpStr, "South"); + break; + case 3: + strcpy(tmpStr, "Southwest"); + break; + case 4: + strcpy(tmpStr, "West"); + break; + case 5: + strcpy(tmpStr, "Northwest"); + break; + case 6: + strcpy(tmpStr, "North"); + break; + case 7: + strcpy(tmpStr, "Northeast"); + break; + case 8: + snprintf(tmpStr, sizeof(tmpStr), "Opposite To Rotation"); + break; + default: + break; + } + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, tmpStr); + } + } else { // enter other row entries here @@ -6143,10 +6231,10 @@ int main(int argc, char** argv) for ( int i = 0; i < numProperties; i++ ) { inputField_x = subx1 + 8; - if ( i > 5 && i <= 8 ) + if ( i > 6 && i <= 9 ) { spacing = 18; - int offsetBoxes = 2.5 * 36; + int offsetBoxes = 3 * 36; inputFieldWidth = subx2 - inputField_x; // width of the text field if ( mouseInBounds(inputField_x - 4, inputField_x - 4 + inputFieldWidth, suby1 + 40 + i * spacing + offsetBoxes, suby1 + 56 + i * spacing + offsetBoxes) ) @@ -6158,7 +6246,7 @@ int main(int argc, char** argv) } else { - if ( i == 5 ) + if ( i == 6 ) { inputFieldWidth = subx2 - inputField_x; // width of the text field } @@ -6188,7 +6276,7 @@ int main(int argc, char** argv) } // set the maximum length allowed for user input - if ( editproperty >= 5 && editproperty <= 8 ) + if ( editproperty >= 6 && editproperty <= 9 ) { inputlen = 48; } @@ -6198,10 +6286,10 @@ int main(int argc, char** argv) } if ( (ticks - cursorflash) % TICKS_PER_SECOND < TICKS_PER_SECOND / 2 ) { - if ( editproperty > 5 && editproperty <= 8 ) + if ( editproperty > 6 && editproperty <= 9 ) { spacing = 18; - int offsetBoxes = 2.5 * 36; + int offsetBoxes = 3 * 36; if ( (ticks - cursorflash) % TICKS_PER_SECOND < TICKS_PER_SECOND / 2 ) { printText(font8x8_bmp, subx1 + 8 + strlen(spriteProperties[editproperty]) * 8, suby1 + 44 + editproperty * spacing + offsetBoxes, "\26"); @@ -6352,16 +6440,52 @@ int main(int argc, char** argv) for ( int i = 0; i < numProperties; i++ ) { + inputField_x = subx1 + 8; int propertyInt = atoi(spriteProperties[i]); - strcpy(tmpPropertyName, lightSourcePropertyNames[i]); - inputFieldHeader_y = suby1 + 28 + i * spacing; - inputField_y = inputFieldHeader_y + 16; - // box outlines then text - drawDepressed(inputField_x - 4, inputField_y - 4, inputField_x - 4 + inputFieldWidth, inputField_y + 16 - 4); - // print values on top of boxes - printText(font8x8_bmp, inputField_x, suby1 + 44 + i * spacing, spriteProperties[i]); - printText(font8x8_bmp, inputField_x, inputFieldHeader_y, tmpPropertyName); + if ( i >= 7 && i <= 9 ) + { + inputFieldFeedback_x = inputField_x + (inputFieldWidth + 8) * 4 - 4; + } + + if ( i == 8 || i == 9 ) + { + // no header + inputFieldHeader_y = suby1 + 28 + 7 * spacing; + inputField_y = inputFieldHeader_y + 16; + inputField_x = subx1 + 8 + (inputFieldWidth + 8) * (i - 7); + + // box outlines then text + drawDepressed(inputField_x - 4, inputField_y - 4, inputField_x - 4 + inputFieldWidth, inputField_y + 16 - 4); + // print values on top of boxes + printText(font8x8_bmp, inputField_x, suby1 + 44 + 7 * spacing, spriteProperties[i]); + + if ( i == 9 ) + { + Uint32 colorPreview = makeColorRGB((Uint32)atoi(spriteProperties[7]), + (Uint32)atoi(spriteProperties[8]), (Uint32)atoi(spriteProperties[9])); + SDL_Rect src; + src.x = subx1 + 8 + (inputFieldWidth + 8) * 3; + src.h = 16; + src.w = 32; + src.y = inputField_y - 4; + drawRect(&src, colorPreview, 255); + } + } + else + { + + strcpy(tmpPropertyName, lightSourcePropertyNames[i]); + inputFieldHeader_y = suby1 + 28 + i * spacing; + + inputField_y = inputFieldHeader_y + 16; + // box outlines then text + drawDepressed(inputField_x - 4, inputField_y - 4, inputField_x - 4 + inputFieldWidth, inputField_y + 16 - 4); + // print values on top of boxes + printText(font8x8_bmp, inputField_x, suby1 + 44 + i * spacing, spriteProperties[i]); + printText(font8x8_bmp, inputField_x, inputFieldHeader_y, tmpPropertyName); + } + if ( errorArr[i] != 1 ) { @@ -6386,7 +6510,7 @@ int main(int argc, char** argv) propertyPageError(i, 0); // reset to default 0. } } - else if ( i == 1 ) + else if ( i == 1 || (i >= 7 && i <= 9) ) { if ( propertyInt > 255 || propertyInt < -1 ) { @@ -6408,7 +6532,57 @@ int main(int argc, char** argv) } } - propertyPageTextAndInput(numProperties, inputFieldWidth); + //propertyPageTextAndInput(numProperties, inputFieldWidth); + { + int pad_x1 = subx1 + 8; + int spacing = 36; + int pad_x2 = inputFieldWidth; + + // Cycle properties with TAB. + if ( keystatus[SDLK_TAB] ) + { + keystatus[SDLK_TAB] = 0; + cursorflash = ticks; + editproperty++; + if ( editproperty == numProperties ) + { + editproperty = 0; + } + + inputstr = spriteProperties[editproperty]; + } + + // select a textbox + if ( mousestatus[SDL_BUTTON_LEFT] ) + { + for ( int i = 0; i < numProperties; i++ ) + { + if ( i == 8 || i == 9 ) + { + inputFieldWidth = 64; + inputField_x = subx1 + 8 + (inputFieldWidth + 8) * (i - 7); + if ( omousex >= inputField_x - 4 + && omousex < inputField_x - 4 + inputFieldWidth + && omousey >= suby1 + 40 + 7 * spacing + && omousey < suby1 + 56 + 7 * spacing ) + { + inputstr = spriteProperties[i]; + editproperty = i; + cursorflash = ticks; + } + } + else + { + if ( omousex >= pad_x1 - 4 && omousey >= suby1 + 40 + i * spacing && omousex < pad_x1 - 4 + pad_x2 && omousey < suby1 + 56 + i * spacing ) + { + inputstr = spriteProperties[i]; + editproperty = i; + cursorflash = ticks; + } + } + } + } + } if ( editproperty < numProperties ) // edit { @@ -6435,7 +6609,18 @@ int main(int argc, char** argv) { inputlen = 4; } - propertyPageCursorFlash(spacing); + //propertyPageCursorFlash(spacing); + if ( (ticks - cursorflash) % TICKS_PER_SECOND < TICKS_PER_SECOND / 2 ) + { + if ( editproperty == 8 || editproperty == 9 ) + { + printText(font8x8_bmp, (subx1 + 8 + (inputFieldWidth + 8) * (editproperty - 7)) + strlen(spriteProperties[editproperty]) * 8, suby1 + 44 + 7 * spacing, "\26"); + } + else + { + printText(font8x8_bmp, subx1 + 8 + strlen(spriteProperties[editproperty]) * 8, suby1 + 44 + editproperty * spacing, "\26"); + } + } } } } @@ -7594,12 +7779,12 @@ int main(int argc, char** argv) } } } - else if ( newwindow == 27 ) + else if ( newwindow == 36 ) { if ( selectedEntity[0] != NULL ) { - int numProperties = sizeof(gatePropertyNames) / sizeof(gatePropertyNames[0]); //find number of entries in property list - const int lenProperties = sizeof(gatePropertyNames[0]) / sizeof(char); //find length of entry in property list + int numProperties = sizeof(doorIronPropertyNames) / sizeof(doorIronPropertyNames[0]); //find number of entries in property list + const int lenProperties = sizeof(doorIronPropertyNames[0]) / sizeof(char); //find length of entry in property list int spacing = 36; // 36 px between each item in the list. int inputFieldHeader_y = suby1 + 28; // 28 px spacing from subwindow start. int inputField_x = subx1 + 8; // 8px spacing from subwindow start. @@ -7615,7 +7800,7 @@ int main(int argc, char** argv) { int propertyInt = atoi(spriteProperties[i]); - strcpy(tmpPropertyName, gatePropertyNames[i]); + strcpy(tmpPropertyName, doorIronPropertyNames[i]); inputFieldHeader_y = suby1 + 28 + i * spacing; inputField_y = inputFieldHeader_y + 16; // box outlines then text @@ -7626,7 +7811,31 @@ int main(int argc, char** argv) if ( errorArr[i] != 1 ) { - if ( i == 0 ) + if ( i == 3 ) + { + if ( propertyInt > 2 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0 random. + } + else + { + char tmpStr[32] = ""; + if ( propertyInt == 1 ) + { + strcpy(tmpStr, "force locked"); + } + else if ( propertyInt == 2 ) + { + strcpy(tmpStr, "force unlocked"); + } + else + { + strcpy(tmpStr, "default random"); + } + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, tmpStr); + } + } + else if ( i == 1 || i == 2 ) { if ( propertyInt > 1 || propertyInt < 0 ) { @@ -7642,6 +7851,22 @@ int main(int argc, char** argv) printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, tmpStr); } } + else if ( i == 0 ) + { + if ( propertyInt > 1 || propertyInt < 0 ) + { + propertyPageError(i, 1); // reset to default power to unlock + } + else + { + char tmpStr[32] = ""; + if ( propertyInt == 1 ) + { + strcpy(tmpStr, "unlock when powered"); + } + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, tmpStr); + } + } else { // enter other row entries here @@ -7673,12 +7898,12 @@ int main(int argc, char** argv) } } } - else if ( newwindow == 28 ) + else if ( newwindow == 27 ) { - if ( selectedEntity[0] != nullptr ) + if ( selectedEntity[0] != NULL ) { - int numProperties = sizeof(playerSpawnPropertyNames) / sizeof(playerSpawnPropertyNames[0]); //find number of entries in property list - const int lenProperties = sizeof(playerSpawnPropertyNames[0]) / sizeof(char); //find length of entry in property list + int numProperties = sizeof(gatePropertyNames) / sizeof(gatePropertyNames[0]); //find number of entries in property list + const int lenProperties = sizeof(gatePropertyNames[0]) / sizeof(char); //find length of entry in property list int spacing = 36; // 36 px between each item in the list. int inputFieldHeader_y = suby1 + 28; // 28 px spacing from subwindow start. int inputField_x = subx1 + 8; // 8px spacing from subwindow start. @@ -7694,7 +7919,7 @@ int main(int argc, char** argv) { int propertyInt = atoi(spriteProperties[i]); - strcpy(tmpPropertyName, playerSpawnPropertyNames[i]); + strcpy(tmpPropertyName, gatePropertyNames[i]); inputFieldHeader_y = suby1 + 28 + i * spacing; inputField_y = inputFieldHeader_y + 16; // box outlines then text @@ -7707,44 +7932,16 @@ int main(int argc, char** argv) { if ( i == 0 ) { - if ( propertyInt > 7 || propertyInt < -1 ) + if ( propertyInt > 1 || propertyInt < 0 ) { - propertyPageError(i, 0); // reset to default 0. + propertyPageError(i, 0); // reset to default no disable } else { char tmpStr[32] = ""; - switch ( propertyInt ) + if ( propertyInt == 1 ) { - case -1: - strcpy(tmpStr, "random"); - break; - case 0: - strcpy(tmpStr, "East"); - break; - case 1: - strcpy(tmpStr, "Southeast"); - break; - case 2: - strcpy(tmpStr, "South"); - break; - case 3: - strcpy(tmpStr, "Southwest"); - break; - case 4: - strcpy(tmpStr, "West"); - break; - case 5: - strcpy(tmpStr, "Northwest"); - break; - case 6: - strcpy(tmpStr, "North"); - break; - case 7: - strcpy(tmpStr, "Northeast"); - break; - default: - break; + strcpy(tmpStr, "disabled"); } printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, tmpStr); } @@ -7780,12 +7977,12 @@ int main(int argc, char** argv) } } } - else if ( newwindow == 29 ) + else if ( newwindow == 28 ) { if ( selectedEntity[0] != nullptr ) { - int numProperties = sizeof(statuePropertyNames) / sizeof(statuePropertyNames[0]); //find number of entries in property list - const int lenProperties = sizeof(statuePropertyNames[0]) / sizeof(char); //find length of entry in property list + int numProperties = sizeof(playerSpawnPropertyNames) / sizeof(playerSpawnPropertyNames[0]); //find number of entries in property list + const int lenProperties = sizeof(playerSpawnPropertyNames[0]) / sizeof(char); //find length of entry in property list int spacing = 36; // 36 px between each item in the list. int inputFieldHeader_y = suby1 + 28; // 28 px spacing from subwindow start. int inputField_x = subx1 + 8; // 8px spacing from subwindow start. @@ -7801,18 +7998,10 @@ int main(int argc, char** argv) { int propertyInt = atoi(spriteProperties[i]); - strcpy(tmpPropertyName, statuePropertyNames[i]); + strcpy(tmpPropertyName, playerSpawnPropertyNames[i]); inputFieldHeader_y = suby1 + 28 + i * spacing; inputField_y = inputFieldHeader_y + 16; // box outlines then text - if ( i == 1 ) - { - inputFieldWidth = 80; // width of the text field - } - else - { - inputFieldWidth = 64; // width of the text field - } drawDepressed(inputField_x - 4, inputField_y - 4, inputField_x - 4 + inputFieldWidth, inputField_y + 16 - 4); // print values on top of boxes printText(font8x8_bmp, inputField_x, suby1 + 44 + i * spacing, spriteProperties[i]); @@ -7822,7 +8011,7 @@ int main(int argc, char** argv) { if ( i == 0 ) { - if ( propertyInt > 3 || propertyInt < 0 ) + if ( propertyInt > 7 || propertyInt < -1 ) { propertyPageError(i, 0); // reset to default 0. } @@ -7831,11 +8020,126 @@ int main(int argc, char** argv) char tmpStr[32] = ""; switch ( propertyInt ) { + case -1: + strcpy(tmpStr, "random"); + break; case 0: strcpy(tmpStr, "East"); break; case 1: - strcpy(tmpStr, "South"); + strcpy(tmpStr, "Southeast"); + break; + case 2: + strcpy(tmpStr, "South"); + break; + case 3: + strcpy(tmpStr, "Southwest"); + break; + case 4: + strcpy(tmpStr, "West"); + break; + case 5: + strcpy(tmpStr, "Northwest"); + break; + case 6: + strcpy(tmpStr, "North"); + break; + case 7: + strcpy(tmpStr, "Northeast"); + break; + default: + break; + } + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, tmpStr); + } + } + else + { + // enter other row entries here + } + } + + if ( errorMessage ) + { + if ( errorArr[i] == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, colorError, "Invalid ID!"); + } + } + } + + propertyPageTextAndInput(numProperties, inputFieldWidth); + + if ( editproperty < numProperties ) // edit + { + if ( !SDL_IsTextInputActive() ) + { + SDL_StartTextInput(); + inputstr = spriteProperties[0]; + } + + // set the maximum length allowed for user input + inputlen = 2; + propertyPageCursorFlash(spacing); + } + } + } + else if ( newwindow == 29 ) + { + if ( selectedEntity[0] != nullptr ) + { + int numProperties = sizeof(statuePropertyNames) / sizeof(statuePropertyNames[0]); //find number of entries in property list + const int lenProperties = sizeof(statuePropertyNames[0]) / sizeof(char); //find length of entry in property list + int spacing = 36; // 36 px between each item in the list. + int inputFieldHeader_y = suby1 + 28; // 28 px spacing from subwindow start. + int inputField_x = subx1 + 8; // 8px spacing from subwindow start. + int inputField_y = inputFieldHeader_y + 16; + int inputFieldWidth = 64; // width of the text field + int inputFieldFeedback_x = inputField_x + inputFieldWidth + 8; + char tmpPropertyName[lenProperties] = ""; + Uint32 color = makeColorRGB(0, 255, 0); + Uint32 colorRandom = makeColorRGB(0, 168, 255); + Uint32 colorError = makeColorRGB(255, 0, 0); + + for ( int i = 0; i < numProperties; i++ ) + { + int propertyInt = atoi(spriteProperties[i]); + + strcpy(tmpPropertyName, statuePropertyNames[i]); + inputFieldHeader_y = suby1 + 28 + i * spacing; + inputField_y = inputFieldHeader_y + 16; + // box outlines then text + if ( i == 1 ) + { + inputFieldWidth = 80; // width of the text field + } + else + { + inputFieldWidth = 64; // width of the text field + } + drawDepressed(inputField_x - 4, inputField_y - 4, inputField_x - 4 + inputFieldWidth, inputField_y + 16 - 4); + // print values on top of boxes + printText(font8x8_bmp, inputField_x, suby1 + 44 + i * spacing, spriteProperties[i]); + printText(font8x8_bmp, inputField_x, inputFieldHeader_y, tmpPropertyName); + + if ( errorArr[i] != 1 ) + { + if ( i == 0 ) + { + if ( propertyInt > 3 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0. + } + else + { + char tmpStr[32] = ""; + switch ( propertyInt ) + { + case 0: + strcpy(tmpStr, "East"); + break; + case 1: + strcpy(tmpStr, "South"); break; case 2: strcpy(tmpStr, "West"); @@ -8573,6 +8877,376 @@ int main(int argc, char** argv) } } } + else if ( newwindow == 34 ) + { + if ( selectedEntity[0] != nullptr ) + { + int numProperties = sizeof(wallLockPropertyNames) / sizeof(wallLockPropertyNames[0]); //find number of entries in property list + const int lenProperties = sizeof(wallLockPropertyNames[0]) / sizeof(char); //find length of entry in property list + int spacing = 36; // 36 px between each item in the list. + int inputFieldHeader_y = suby1 + 28; // 28 px spacing from subwindow start. + int inputField_x = subx1 + 8; // 8px spacing from subwindow start. + int inputField_y = inputFieldHeader_y + 16; + int inputFieldWidth = 64; // width of the text field + int inputFieldFeedback_x = inputField_x + inputFieldWidth + 8; + char tmpPropertyName[lenProperties] = ""; + Uint32 color = makeColorRGB(0, 255, 0); + Uint32 colorRandom = makeColorRGB(0, 168, 255); + Uint32 colorError = makeColorRGB(255, 0, 0); + + for ( int i = 0; i < numProperties; i++ ) + { + int propertyInt = atoi(spriteProperties[i]); + + strcpy(tmpPropertyName, wallLockPropertyNames[i]); + inputFieldHeader_y = suby1 + 28 + i * spacing; + inputField_y = inputFieldHeader_y + 16; + // box outlines then text + drawDepressed(inputField_x - 4, inputField_y - 4, inputField_x - 4 + inputFieldWidth, inputField_y + 16 - 4); + // print values on top of boxes + printText(font8x8_bmp, inputField_x, suby1 + 44 + i * spacing, spriteProperties[i]); + printText(font8x8_bmp, inputField_x, inputFieldHeader_y, tmpPropertyName); + + if ( errorArr[i] != 1 ) + { + if ( i == 0 ) + { + if ( propertyInt > 7 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0. + } + else + { + switch ( propertyInt ) + { + case 0: + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "stone"); + break; + case 1: + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "bone"); + break; + case 2: + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "bronze"); + break; + case 3: + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "iron"); + break; + case 4: + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "silver"); + break; + case 5: + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "gold"); + break; + case 6: + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "crystal"); + break; + case 7: + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "machine"); + break; + default: + break; + } + } + } + else if ( i == 1 ) + { + if ( propertyInt > 1 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0. + } + else + { + if ( propertyInt == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "output without key"); + } + } + } + else if ( i == 2 ) + { + if ( propertyInt > 1 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0. + } + else + { + if ( propertyInt == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "toggleable with key"); + } + } + } + else if ( i == 3 ) + { + if ( propertyInt > 100 || propertyInt < -1 ) + { + propertyPageError(i, -1); // reset to default -1 + } + else + { + if ( propertyInt >= 0 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "%d skill required", propertyInt); + } + } + } + else if ( i == 4 ) + { + if ( propertyInt > 1 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0. + } + else + { + if ( propertyInt == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "skeleton key usable"); + } + } + } + else if ( i == 5 ) + { + if ( propertyInt > 1 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0. + } + else + { + if ( propertyInt == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "auto gen key"); + } + } + } + else + { + // enter other row entries here + } + } + + if ( errorMessage ) + { + if ( errorArr[i] == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, colorError, "Invalid ID!"); + } + } + } + + propertyPageTextAndInput(numProperties, inputFieldWidth); + + if ( editproperty < numProperties ) // edit + { + if ( !SDL_IsTextInputActive() ) + { + SDL_StartTextInput(); + inputstr = spriteProperties[0]; + } + + // set the maximum length allowed for user input + inputlen = 4; + propertyPageCursorFlash(spacing); + } + } + } + else if ( newwindow == 35 ) + { + if ( selectedEntity[0] != nullptr ) + { + int numProperties = sizeof(wallButtonPropertyNames) / sizeof(wallButtonPropertyNames[0]); //find number of entries in property list + const int lenProperties = sizeof(wallButtonPropertyNames[0]) / sizeof(char); //find length of entry in property list + int spacing = 36; // 36 px between each item in the list. + int inputFieldHeader_y = suby1 + 28; // 28 px spacing from subwindow start. + int inputField_x = subx1 + 8; // 8px spacing from subwindow start. + int inputField_y = inputFieldHeader_y + 16; + int inputFieldWidth = 64; // width of the text field + int inputFieldFeedback_x = inputField_x + inputFieldWidth + 8; + char tmpPropertyName[lenProperties] = ""; + Uint32 color = makeColorRGB(0, 255, 0); + Uint32 colorRandom = makeColorRGB(0, 168, 255); + Uint32 colorError = makeColorRGB(255, 0, 0); + + for ( int i = 0; i < numProperties; i++ ) + { + int propertyInt = atoi(spriteProperties[i]); + + strcpy(tmpPropertyName, wallButtonPropertyNames[i]); + inputFieldHeader_y = suby1 + 28 + i * spacing; + inputField_y = inputFieldHeader_y + 16; + // box outlines then text + drawDepressed(inputField_x - 4, inputField_y - 4, inputField_x - 4 + inputFieldWidth, inputField_y + 16 - 4); + // print values on top of boxes + printText(font8x8_bmp, inputField_x, suby1 + 44 + i * spacing, spriteProperties[i]); + printText(font8x8_bmp, inputField_x, inputFieldHeader_y, tmpPropertyName); + + if ( errorArr[i] != 1 ) + { + if ( i == 0 ) + { + if ( propertyInt > 1 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0. + } + else + { + if ( propertyInt == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "powered when unpressed"); + } + } + } + else if ( i == 1 ) + { + if ( propertyInt > 9999 || propertyInt < 0 ) + { + propertyPageError(i, 0); // reset to default 0. + } + else + { + if ( propertyInt == 0 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, "no reset"); + } + } + } + else + { + // enter other row entries here + } + } + + if ( errorMessage ) + { + if ( errorArr[i] == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, colorError, "Invalid ID!"); + } + } + } + + propertyPageTextAndInput(numProperties, inputFieldWidth); + + if ( editproperty < numProperties ) // edit + { + if ( !SDL_IsTextInputActive() ) + { + SDL_StartTextInput(); + inputstr = spriteProperties[0]; + } + + // set the maximum length allowed for user input + inputlen = 4; + propertyPageCursorFlash(spacing); + } + } + } + else if ( newwindow == 37 ) + { + if ( selectedEntity[0] != nullptr ) + { + int numProperties = sizeof(windPropertyNames) / sizeof(windPropertyNames[0]); //find number of entries in property list + const int lenProperties = sizeof(windPropertyNames[0]) / sizeof(char); //find length of entry in property list + int spacing = 36; // 36 px between each item in the list. + int inputFieldHeader_y = suby1 + 28; // 28 px spacing from subwindow start. + int inputField_x = subx1 + 8; // 8px spacing from subwindow start. + int inputField_y = inputFieldHeader_y + 16; + int inputFieldWidth = 64; // width of the text field + int inputFieldFeedback_x = inputField_x + inputFieldWidth + 8; + char tmpPropertyName[lenProperties] = ""; + Uint32 color = makeColorRGB(0, 255, 0); + Uint32 colorRandom = makeColorRGB(0, 168, 255); + Uint32 colorError = makeColorRGB(255, 0, 0); + + for ( int i = 0; i < numProperties; i++ ) + { + int propertyInt = atoi(spriteProperties[i]); + + strcpy(tmpPropertyName, windPropertyNames[i]); + inputFieldHeader_y = suby1 + 28 + i * spacing; + inputField_y = inputFieldHeader_y + 16; + // box outlines then text + drawDepressed(inputField_x - 4, inputField_y - 4, inputField_x - 4 + inputFieldWidth, inputField_y + 16 - 4); + // print values on top of boxes + printText(font8x8_bmp, inputField_x, suby1 + 44 + i * spacing, spriteProperties[i]); + printText(font8x8_bmp, inputField_x, inputFieldHeader_y, tmpPropertyName); + + if ( errorArr[i] != 1 ) + { + if ( i == 0 ) + { + if ( propertyInt > 9 || propertyInt < -1 ) + { + propertyPageError(i, -1); // reset to default -1. + } + else + { + char tmpStr[32] = ""; + switch ( propertyInt ) + { + case -1: + strcpy(tmpStr, "random"); + break; + case 0: + strcpy(tmpStr, "East"); + break; + case 1: + strcpy(tmpStr, "Southeast"); + break; + case 2: + strcpy(tmpStr, "South"); + break; + case 3: + strcpy(tmpStr, "Southwest"); + break; + case 4: + strcpy(tmpStr, "West"); + break; + case 5: + strcpy(tmpStr, "Northwest"); + break; + case 6: + strcpy(tmpStr, "North"); + break; + case 7: + strcpy(tmpStr, "Northeast"); + break; + default: + break; + } + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, color, tmpStr); + } + } + else + { + // enter other row entries here + } + } + + if ( errorMessage ) + { + if ( errorArr[i] == 1 ) + { + printTextFormattedColor(font8x8_bmp, inputFieldFeedback_x, inputField_y, colorError, "Invalid ID!"); + } + } + } + + propertyPageTextAndInput(numProperties, inputFieldWidth); + + if ( editproperty < numProperties ) // edit + { + if ( !SDL_IsTextInputActive() ) + { + SDL_StartTextInput(); + inputstr = spriteProperties[0]; + } + + // set the maximum length allowed for user input + inputlen = 2; + propertyPageCursorFlash(spacing); + } + } + } else if ( newwindow == 16 || newwindow == 17 ) { int textColumnLeft = subx1 + 16; @@ -9325,6 +9999,16 @@ int main(int argc, char** argv) keystatus[SDLK_KP_MULTIPLY] = 0; lockTilePalette[recentUsedTilePalette] = !lockTilePalette[recentUsedTilePalette]; // toggle lock/unlock } + if ( keystatus[SDLK_F5] ) + { + keystatus[SDLK_F5] = 0; + buttonOpenPrevMap(nullptr); + } + if ( keystatus[SDLK_F8] ) + { + keystatus[SDLK_F8] = 0; + buttonOpenNextMap(nullptr); + } } // process and draw buttons handleButtons(); @@ -9457,9 +10141,9 @@ int main(int argc, char** argv) default: strcpy(action,"STATIC"); break; }*/ - int numsprites = static_cast(sizeof(spriteEditorNameStrings) / sizeof(spriteEditorNameStrings[0])); + int numsprites = spriteEditorNameStrings.size(); - if ( (mousex <= xres && mousey <= yres) && palette[mousey + mousex * yres] >= 0 && palette[mousey + mousex * yres] <= numsprites ) + if ( (mousex <= xres && mousey <= yres) && palette[mousey + mousex * yres] >= 0 && palette[mousey + mousex * yres] < numsprites ) { printTextFormatted(font8x8_bmp, 0, yres - 8, "Sprite index:%5d", palette[mousey + mousex * yres]); printTextFormatted(font8x8_bmp, 0, yres - 16, "%s", spriteEditorNameStrings[palette[mousey + mousex * yres]]); diff --git a/src/editor.hpp b/src/editor.hpp index 0afdced1e..dcccca0eb 100644 --- a/src/editor.hpp +++ b/src/editor.hpp @@ -199,6 +199,8 @@ void buttonCloseSpriteSubwindow(button_t* my); void buttonMonsterItems(button_t* my); void initMonsterPropertiesWindow(); void buttonOpenDirectory(button_t* my); +void buttonOpenPrevMap(button_t* my); +void buttonOpenNextMap(button_t* my); extern char itemName[128]; extern int itemSelect; diff --git a/src/engine/audio/defines.cpp b/src/engine/audio/defines.cpp index 64b1991d7..cb245fe94 100644 --- a/src/engine/audio/defines.cpp +++ b/src/engine/audio/defines.cpp @@ -30,6 +30,7 @@ FMOD::Sound** ruinsmusic = nullptr; FMOD::Sound** underworldmusic = nullptr; FMOD::Sound** hellmusic = nullptr; FMOD::Sound** intromusic = nullptr; +FMOD::Sound** fortressmusic = nullptr; FMOD::Sound* intermissionmusic = nullptr; FMOD::Sound* minetownmusic = nullptr; FMOD::Sound* splashmusic = nullptr; @@ -68,6 +69,11 @@ FMOD::ChannelGroup* soundNotification_group = nullptr; FMOD::ChannelGroup* music_group = nullptr; FMOD::ChannelGroup* music_notification_group = nullptr; +FMOD::ChannelGroup* music_ensemble_global_send_group = nullptr; +FMOD::ChannelGroup* music_ensemble_global_recv_group = nullptr; +FMOD::ChannelGroup* music_ensemble_local_recv_player[MAXPLAYERS] = { nullptr }; +FMOD::ChannelGroup* music_ensemble_local_recv_group = nullptr; + float fadein_increment = 0.002f; float default_fadein_increment = 0.002f; float fadeout_increment = 0.005f; diff --git a/src/engine/audio/init_audio.cpp b/src/engine/audio/init_audio.cpp index 997cd4adb..7a9d2835e 100644 --- a/src/engine/audio/init_audio.cpp +++ b/src/engine/audio/init_audio.cpp @@ -62,7 +62,11 @@ bool initSoundEngine() if (!no_sound) { - fmod_result = fmod_system->init(fmod_maxchannels, FMOD_INIT_NORMAL | FMOD_INIT_3D_RIGHTHANDED | FMOD_INIT_VOL0_BECOMES_VIRTUAL | FMOD_INIT_STREAM_FROM_UPDATE | FMOD_INIT_THREAD_UNSAFE/* | FMOD_INIT_PROFILE_ENABLE | FMOD_INIT_PROFILE_METER_ALL*/, fmod_extraDriverData); + FMOD_INITFLAGS flags = FMOD_INIT_NORMAL | FMOD_INIT_3D_RIGHTHANDED | FMOD_INIT_VOL0_BECOMES_VIRTUAL | FMOD_INIT_STREAM_FROM_UPDATE | FMOD_INIT_THREAD_UNSAFE; +#ifndef NDEBUG + flags |= FMOD_INIT_PROFILE_ENABLE | FMOD_INIT_PROFILE_METER_ALL; +#endif + fmod_result = fmod_system->init(fmod_maxchannels, flags, fmod_extraDriverData); if (FMODErrorCheck()) { printlog("[FMOD]: Failed to initialize FMOD. DISABLING AUDIO.\n"); @@ -148,6 +152,158 @@ bool initSoundEngine() no_sound = true; return false; } + fmod_result = fmod_system->createChannelGroup(nullptr, &music_ensemble_global_send_group); + if ( FMODErrorCheck() ) + { + printlog("[FMOD]: Failed to create music channel group. DISABLING AUDIO.\n"); + no_sound = true; + return false; + } + music_ensemble_global_send_group->setVolumeRamp(true); + + fmod_result = fmod_system->createChannelGroup(nullptr, &music_ensemble_global_recv_group); + if ( FMODErrorCheck() ) + { + printlog("[FMOD]: Failed to create music channel group. DISABLING AUDIO.\n"); + no_sound = true; + return false; + } + + fmod_result = fmod_system->createChannelGroup(nullptr, &music_ensemble_local_recv_group); + if ( FMODErrorCheck() ) + { + printlog("[FMOD]: Failed to create music channel group. DISABLING AUDIO.\n"); + no_sound = true; + return false; + } + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + fmod_result = fmod_system->createChannelGroup(nullptr, &music_ensemble_local_recv_player[i]); + if ( FMODErrorCheck() ) + { + printlog("[FMOD]: Failed to create music channel group. DISABLING AUDIO.\n"); + no_sound = true; + return false; + } + music_ensemble_local_recv_group->addGroup(music_ensemble_local_recv_player[i]); + music_ensemble_local_recv_player[i]->setMode(FMOD_3D | FMOD_3D_WORLDRELATIVE); + } + { + // add dsp + + //FMOD::DSP* dspeq = 0; + //fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_MULTIBAND_EQ, &dspeq); + //FMODErrorCheck(); + // + //fmod_result = music_ensemble_global_group->addDSP(0, dspeq); + //FMODErrorCheck(); + // + //dspeq->setParameterInt(FMOD_DSP_MULTIBAND_EQ_A_FILTER, FMOD_DSP_MULTIBAND_EQ_FILTER_NOTCH); + //dspeq->setParameterFloat(FMOD_DSP_MULTIBAND_EQ_A_FREQUENCY, 2000); + //dspeq->setParameterFloat(FMOD_DSP_MULTIBAND_EQ_A_GAIN, -6); + //dspeq->setParameterFloat(FMOD_DSP_MULTIBAND_EQ_A_Q, 1.f); + + // global sends + { + // send group does not output to master bus + FMOD::DSP* fader = nullptr; + fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_FADER, &fader); + fader->setParameterFloat(FMOD_DSP_FADER_GAIN, -80.f); // inaudible + music_ensemble_global_send_group->addDSP(0, fader); + } + + // global recv transceivers + { + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + FMOD::DSP* transceiver = nullptr; + fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_TRANSCEIVER, &transceiver); + music_ensemble_global_recv_group->addDSP(1, transceiver); + transceiver->setParameterInt(FMOD_DSP_TRANSCEIVER_CHANNEL, i + 1); // receive on channel x + transceiver->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, -80.f); // inaudible + transceiver->setChannelFormat(0, 2, FMOD_SPEAKERMODE_STEREO); // force stereo on empty channel, otherwise defaults to mono + } + } + + // player recv transceivers + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + FMOD::DSP* transceiver = nullptr; + fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_TRANSCEIVER, &transceiver); + music_ensemble_local_recv_player[c]->addDSP(1, transceiver); + transceiver->setParameterInt(FMOD_DSP_TRANSCEIVER_CHANNEL, i + 1); // receive on channel x + transceiver->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, -80.f); // inaudible + transceiver->setChannelFormat(0, 2, FMOD_SPEAKERMODE_STEREO); // force stereo on empty channel, otherwise defaults to mono + } + } + } + + // global recv reverb + { + FMOD::DSP* dspreverb = 0; + fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_SFXREVERB, &dspreverb); + FMODErrorCheck(); + + fmod_result = music_ensemble_global_recv_group->addDSP(0, dspreverb); + dspreverb->setBypass(true); + FMODErrorCheck(); + + FMOD_REVERB_PROPERTIES props = FMOD_PRESET_OFF; + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DECAYTIME, props.DecayTime); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_EARLYDELAY, props.EarlyDelay); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LATEDELAY, props.LateDelay); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HFREFERENCE, props.HFReference); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HFDECAYRATIO, props.HFDecayRatio); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DIFFUSION, props.Diffusion); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DENSITY, props.Density); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LOWSHELFFREQUENCY, props.LowShelfFrequency); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LOWSHELFGAIN, props.LowShelfGain); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HIGHCUT, props.HighCut); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_EARLYLATEMIX, props.EarlyLateMix); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_WETLEVEL, props.WetLevel); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DRYLEVEL, 0.f); + } + } + //{ + // // add dsp + + // //FMOD::DSP* dspeq = 0; + // //fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_MULTIBAND_EQ, &dspeq); + // //FMODErrorCheck(); + // // + // //fmod_result = music_ensemble_local_group->addDSP(0, dspeq); + // //FMODErrorCheck(); + // // + // //dspeq->setParameterInt(FMOD_DSP_MULTIBAND_EQ_A_FILTER, FMOD_DSP_MULTIBAND_EQ_FILTER_NOTCH); + // //dspeq->setParameterFloat(FMOD_DSP_MULTIBAND_EQ_A_FREQUENCY, 2000); + // //dspeq->setParameterFloat(FMOD_DSP_MULTIBAND_EQ_A_GAIN, -6); + // //dspeq->setParameterFloat(FMOD_DSP_MULTIBAND_EQ_A_Q, 1.f); + + // FMOD::DSP* dspreverb = 0; + // fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_SFXREVERB, &dspreverb); + // FMODErrorCheck(); + + // fmod_result = music_ensemble_local_group->addDSP(0, dspreverb); + // FMODErrorCheck(); + + // FMOD_REVERB_PROPERTIES props = FMOD_PRESET_OFF; + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DECAYTIME, props.DecayTime); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_EARLYDELAY, props.EarlyDelay); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LATEDELAY, props.LateDelay); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HFREFERENCE, props.HFReference); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HFDECAYRATIO, props.HFDecayRatio); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DIFFUSION, props.Diffusion); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DENSITY, props.Density); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LOWSHELFFREQUENCY, props.LowShelfFrequency); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LOWSHELFGAIN, props.LowShelfGain); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HIGHCUT, props.HighCut); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_EARLYLATEMIX, props.EarlyLateMix); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_WETLEVEL, props.WetLevel); + // dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DRYLEVEL, 0.f); + //} #ifndef EDITOR int selected_recording_driver = 0; @@ -252,7 +408,7 @@ int loadSoundResources(real_t base_load_percent, real_t top_load_percent) fp->gets2(name, 128); completePath(full_path, name); FMOD_MODE flags = FMOD_DEFAULT | FMOD_3D | FMOD_LOWMEM; - if ( c == 133 || c == 672 || c == 135 || c == 155 || c == 149 ) + if ( c == 133 || c == 672 || c == 135 || c == 155 || c == 149 || c == 710 ) { flags |= FMOD_LOOP_NORMAL; } diff --git a/src/engine/audio/music.cpp b/src/engine/audio/music.cpp index 3acd502df..eadd69c7f 100644 --- a/src/engine/audio/music.cpp +++ b/src/engine/audio/music.cpp @@ -75,6 +75,11 @@ bool loadMusic() intromusic = (FMOD::Sound**)malloc(sizeof(FMOD::Sound*) * NUMINTROMUSIC); memset(intromusic, 0, sizeof(FMOD::Sound*) * NUMINTROMUSIC); } + if ( NUMFORTRESSMUSIC > 0 ) + { + fortressmusic = (FMOD::Sound**)malloc(sizeof(FMOD::Sound*) * NUMINTROMUSIC); + memset(fortressmusic, 0, sizeof(FMOD::Sound*) * NUMINTROMUSIC); + } #endif bool introMusicChanged; @@ -426,6 +431,19 @@ void handleLevelMusic() } playMusic(citadelmusic[currenttrack], false, true, true); } + else if ( !strncmp(map.filename, "fortress", 8) ) + { + if ( !playing ) + { + currenttrack = 1 + local_rng.rand() % (NUMFORTRESSMUSIC - 1); + } + currenttrack = currenttrack % NUMFORTRESSMUSIC; + if ( currenttrack == 0 ) + { + currenttrack = 1; + } + playMusic(fortressmusic[currenttrack], false, true, true); + } else if ( !strcmp(map.name, "Mages Guild") ) { if ( hamletmusic ) @@ -539,6 +557,10 @@ void handleLevelMusic() { playMusic(citadelmusic[0], true, true, true); } + else if ( !strncmp(map.filename, "fortress", 8) ) + { + playMusic(fortressmusic[0], true, true, true); + } else { playMusic(minesmusic[0], true, true, true); diff --git a/src/engine/audio/sound.cpp b/src/engine/audio/sound.cpp index db54f0c7e..16f2b12eb 100644 --- a/src/engine/audio/sound.cpp +++ b/src/engine/audio/sound.cpp @@ -119,8 +119,11 @@ void setGlobalVolume(real_t master, real_t music, real_t gameplay, real_t ambien soundEnvironment_group->setVolume(master * environment); music_notification_group->setVolume(master * notification); soundNotification_group->setVolume(master * notification); + music_ensemble_global_send_group->setVolume(1.f); #ifndef EDITOR + ensembleSounds.ensemble_recv_global_volume = master * (music * 4); + ensembleSounds.ensemble_recv_player_volume = master * gameplay; if ( VoiceChat.outChannelGroup ) { VoiceChat.outChannelGroup->setVolume(master); @@ -130,6 +133,7 @@ void setGlobalVolume(real_t master, real_t music, real_t gameplay, real_t ambien #ifndef EDITOR static ConsoleVariable cvar_sfx_notification_music_fade("/sfx_notification_music_fade", 0.5f); + static ConsoleVariable cvar_sfx_ensemble_music_fade("/sfx_ensemble_music_fade", 0.f); #endif // !EDITOR void sound_update(int player, int index, int numplayers) @@ -204,12 +208,51 @@ void sound_update(int player, int index, int numplayers) #endif if (player == 0) { +#ifndef EDITOR //Fade in the currently playing music. bool notificationPlaying = false; if ( music_notification_group ) { music_notification_group->isPlaying(¬ificationPlaying); } + bool ensemblePlaying = false; + if ( music_ensemble_global_send_group ) + { + music_ensemble_global_send_group->isPlaying(&ensemblePlaying); + if ( ensemblePlaying ) + { + bool ensemblePaused = false; + music_ensemble_global_send_group->getPaused(&ensemblePaused); // if playing, then check if paused + if ( ensemblePaused ) + { + ensemblePlaying = false; + } + else + { + Uint32 globalEnsemblePlaying = 0; + Uint32 localEnsemblePlaying = 0; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i]->isLocalPlayerAlive() ) + { + globalEnsemblePlaying |= (players[i]->mechanics.ensembleDataUpdate >> 16) & 0xFFFF; + localEnsemblePlaying |= (players[i]->mechanics.ensembleDataUpdate >> 8) & 0xFF; + } + /*if ( players[i]->entity && !client_disconnected[i] ) + { + // if we want other players to override the main soundtrack with local sound + localEnsemblePlaying |= (players[i]->mechanics.ensembleDataUpdate >> 8) & 0xFF; + }*/ + } + if ( globalEnsemblePlaying == 0 || (*cvar_ensemble_vol_bg <= -79.f && localEnsemblePlaying == 0) + || (!instrument_bg_enabled && localEnsemblePlaying == 0) ) + { + ensemblePlaying = false; + } + } + } + } +#endif #ifdef DEBUG_EVENT_TIMERS time2 = std::chrono::high_resolution_clock::now(); @@ -259,6 +302,15 @@ void sound_update(int player, int index, int numplayers) } music_channel->setVolume(volume); } + else if ( ensemblePlaying ) + { + volume -= fadeout_increment * 5; + if ( volume < *cvar_sfx_ensemble_music_fade ) + { + volume = *cvar_sfx_ensemble_music_fade; + } + music_channel->setVolume(volume); + } else if (volume < 1.0f) { volume += fadein_increment * 2; @@ -1176,6 +1228,110 @@ bool physfsSearchMusicToUpdate_helper_findModifiedMusic(uint32_t numMusic, const return false; } +const std::vector themeMusic = { + "music/introduction.ogg", + "music/intermission.ogg", + "music/minetown.ogg", + "music/splash.ogg", + "music/library.ogg", + "music/shop.ogg", + "music/herxboss.ogg", + "music/temple.ogg", + "music/endgame.ogg", + "music/escape.ogg", + "music/devil.ogg", + "music/sanctum.ogg", + "music/gnomishmines.ogg", + "music/greatcastle.ogg", + "music/sokoban.ogg", + "music/caveslair.ogg", + "music/bramscastle.ogg", + "music/hamlet.ogg", + "music/tutorial.ogg", + "sound/Death.ogg", + "sound/ui/StoryMusicV3.ogg", + "sound/ensemble/ensemble1_drumV1.ogg", + "sound/ensemble/ensemble1_fluteV1.ogg", + "sound/ensemble/ensemble1_hornV1.ogg", + "sound/ensemble/ensemble1_luteV1.ogg", + "sound/ensemble/ensemble1_lyreV1.ogg", + "sound/ensemble/ensemble1_tamboV1.ogg", + "sound/ensemble/ensemble1_BEB_tier1_V1.ogg", + "sound/ensemble/ensemble1_BEB_tier2_V1.ogg", + "sound/ensemble/ensemble1_drum_combatV1.ogg", + "sound/ensemble/ensemble1_flute_combatV1.ogg", + "sound/ensemble/ensemble1_horn_combatV1.ogg", + "sound/ensemble/ensemble1_lute_combatV1.ogg", + "sound/ensemble/ensemble1_lyre_combatV1.ogg", + "sound/ensemble/ensemble1_tambo_combatV1.ogg", + "sound/ensemble/ensemble1_BEB_tier1_combatV1.ogg", + "sound/ensemble/ensemble1_BEB_tier2_combatV1.ogg", + /*"sound/ensemble/Trans1/ensemble1_drum_Trans1_120_4-4_V1.ogg", + "sound/ensemble/Trans1/ensemble1_flute_Trans1_120_4-4_V1.ogg", + "sound/ensemble/Trans1/ensemble1_horn_Trans1_120_4-4_V1.ogg", + "sound/ensemble/Trans1/ensemble1_lute_Trans1_120_4-4_V1.ogg", + "sound/ensemble/Trans1/ensemble1_lyre_Trans1_120_4-4_V1.ogg", + "sound/ensemble/Trans1/ensemble1_tambo_Trans1_120_4-4_V1.ogg", + "sound/ensemble/Trans1/ensemble1_tambo_Trans1_120_4-4_V1.ogg", + "sound/ensemble/Trans1/ensemble1_tambo_Trans1_120_4-4_V1.ogg", + "sound/ensemble/Trans2/ensemble1_drum_Trans2_120_4-4_V1.ogg", + "sound/ensemble/Trans2/ensemble1_flute_Trans2_120_4-4_V1.ogg", + "sound/ensemble/Trans2/ensemble1_horn_Trans2_120_4-4_V1.ogg", + "sound/ensemble/Trans2/ensemble1_lute_Trans2_120_4-4_V1.ogg", + "sound/ensemble/Trans2/ensemble1_lyre_Trans2_120_4-4_V1.ogg", + "sound/ensemble/Trans2/ensemble1_tambo_Trans2_120_4-4_V1.ogg", + "sound/ensemble/Trans2/ensemble1_tambo_Trans2_120_4-4_V1.ogg", + "sound/ensemble/Trans2/ensemble1_tambo_Trans2_120_4-4_V1.ogg", + "sound/ensemble/Trans3/ensemble1_drum_Trans3_120_4-4_V1.ogg", + "sound/ensemble/Trans3/ensemble1_flute_Trans3_120_4-4_V1.ogg", + "sound/ensemble/Trans3/ensemble1_horn_Trans3_120_4-4_V1.ogg", + "sound/ensemble/Trans3/ensemble1_lute_Trans3_120_4-4_V1.ogg", + "sound/ensemble/Trans3/ensemble1_lyre_Trans3_120_4-4_V1.ogg", + "sound/ensemble/Trans3/ensemble1_tambo_Trans3_120_4-4_V1.ogg", + "sound/ensemble/Trans3/ensemble1_tambo_Trans3_120_4-4_V1.ogg", + "sound/ensemble/Trans3/ensemble1_tambo_Trans3_120_4-4_V1.ogg",*/ + "sound/ensemble/CombatEnd1/ensemble1_drum_combat_End1_90_7-8.ogg", + "sound/ensemble/CombatEnd1/ensemble1_flute_combat_End1_90_7-8.ogg", + "sound/ensemble/CombatEnd1/ensemble1_horn_combat_End1_90_7-8.ogg", + "sound/ensemble/CombatEnd1/ensemble1_lute_combat_End1_90_7-8.ogg", + "sound/ensemble/CombatEnd1/ensemble1_lyre_combat_End1_90_7-8.ogg", + "sound/ensemble/CombatEnd1/ensemble1_tambo_combat_End1_90_7-8.ogg", + "sound/ensemble/CombatEnd1/ensemble1_BEB_tier1_combat_End1_90_7-8.ogg", + "sound/ensemble/CombatEnd1/ensemble1_BEB_tier2_combat_End1_90_7-8.ogg", + "sound/ensemble/CombatEnd2/ensemble1_drum_combat_End2_90_7-8.ogg", + "sound/ensemble/CombatEnd2/ensemble1_flute_combat_End2_90_7-8.ogg", + "sound/ensemble/CombatEnd2/ensemble1_horn_combat_End2_90_7-8.ogg", + "sound/ensemble/CombatEnd2/ensemble1_lute_combat_End2_90_7-8.ogg", + "sound/ensemble/CombatEnd2/ensemble1_lyre_combat_End2_90_7-8.ogg", + "sound/ensemble/CombatEnd2/ensemble1_tambo_combat_End2_90_7-8.ogg", + "sound/ensemble/CombatEnd2/ensemble1_BEB_tier1_combat_End2_90_7-8.ogg", + "sound/ensemble/CombatEnd2/ensemble1_BEB_tier2_combat_End2_90_7-8.ogg", + /*"sound/ensemble/CombatEnd3/ensemble1_drum_combat_End3_90_7-8.ogg", + "sound/ensemble/CombatEnd3/ensemble1_flute_combat_End3_90_7-8.ogg", + "sound/ensemble/CombatEnd3/ensemble1_horn_combat_End3_90_7-8.ogg", + "sound/ensemble/CombatEnd3/ensemble1_lute_combat_End3_90_7-8.ogg", + "sound/ensemble/CombatEnd3/ensemble1_lyre_combat_End3_90_7-8.ogg", + "sound/ensemble/CombatEnd3/ensemble1_tambo_combat_End3_90_7-8.ogg", + "sound/ensemble/CombatEnd3/ensemble1_tambo_combat_End3_90_7-8.ogg", + "sound/ensemble/CombatEnd3/ensemble1_tambo_combat_End3_90_7-8.ogg", + "sound/ensemble/CombatEnd4/ensemble1_drum_combat_End4_90_7-8.ogg", + "sound/ensemble/CombatEnd4/ensemble1_flute_combat_End4_90_7-8.ogg", + "sound/ensemble/CombatEnd4/ensemble1_horn_combat_End4_90_7-8.ogg", + "sound/ensemble/CombatEnd4/ensemble1_lute_combat_End4_90_7-8.ogg", + "sound/ensemble/CombatEnd4/ensemble1_lyre_combat_End4_90_7-8.ogg", + "sound/ensemble/CombatEnd4/ensemble1_tambo_combat_End4_90_7-8.ogg", + "sound/ensemble/CombatEnd4/ensemble1_tambo_combat_End4_90_7-8.ogg", + "sound/ensemble/CombatEnd4/ensemble1_tambo_combat_End4_90_7-8.ogg",*/ + "sound/ensemble/Trans4/ensemble1_drum_Trans_120_4-4.ogg", + "sound/ensemble/Trans4/ensemble1_flute_Trans_120_4-4.ogg", + "sound/ensemble/Trans4/ensemble1_horn_Trans_120_4-4.ogg", + "sound/ensemble/Trans4/ensemble1_lute_Trans_120_4-4.ogg", + "sound/ensemble/Trans4/ensemble1_lyre_Trans_120_4-4.ogg", + "sound/ensemble/Trans4/ensemble1_tambo_Trans_120_4-4.ogg", + "sound/ensemble/Trans4/ensemble1_BEB_tier1_Trans_120_4-4.ogg", + "sound/ensemble/Trans4/ensemble1_BEB_tier2_Trans_120_4-4.ogg" +}; + bool physfsSearchMusicToUpdate() { if ( no_sound ) @@ -1183,30 +1339,8 @@ bool physfsSearchMusicToUpdate() return false; } #ifdef SOUND - std::vector themeMusic; - themeMusic.push_back("music/introduction.ogg"); - themeMusic.push_back("music/intermission.ogg"); - themeMusic.push_back("music/minetown.ogg"); - themeMusic.push_back("music/splash.ogg"); - themeMusic.push_back("music/library.ogg"); - themeMusic.push_back("music/shop.ogg"); - themeMusic.push_back("music/herxboss.ogg"); - themeMusic.push_back("music/temple.ogg"); - themeMusic.push_back("music/endgame.ogg"); - themeMusic.push_back("music/escape.ogg"); - themeMusic.push_back("music/devil.ogg"); - themeMusic.push_back("music/sanctum.ogg"); - themeMusic.push_back("music/gnomishmines.ogg"); - themeMusic.push_back("music/greatcastle.ogg"); - themeMusic.push_back("music/sokoban.ogg"); - themeMusic.push_back("music/caveslair.ogg"); - themeMusic.push_back("music/bramscastle.ogg"); - themeMusic.push_back("music/hamlet.ogg"); - themeMusic.push_back("music/tutorial.ogg"); - themeMusic.push_back("sound/Death.ogg"); - themeMusic.push_back("sound/ui/StoryMusicV3.ogg"); - - for ( std::vector::iterator it = themeMusic.begin(); it != themeMusic.end(); ++it ) + + for ( auto it = themeMusic.begin(); it != themeMusic.end(); ++it ) { std::string filename = *it; if ( PHYSFS_getRealDir(filename.c_str()) != nullptr ) @@ -1230,7 +1364,8 @@ bool physfsSearchMusicToUpdate() || physfsSearchMusicToUpdate_helper_findModifiedMusic(NUMHELLMUSIC, "music/hell%02d.ogg") || physfsSearchMusicToUpdate_helper_findModifiedMusic(NUMMINOTAURMUSIC, "music/minotaur%02d.ogg") || physfsSearchMusicToUpdate_helper_findModifiedMusic(NUMCAVESMUSIC, "music/caves%02d.ogg") - || physfsSearchMusicToUpdate_helper_findModifiedMusic(NUMCITADELMUSIC, "music/citadel%02d.ogg") ) + || physfsSearchMusicToUpdate_helper_findModifiedMusic(NUMCITADELMUSIC, "music/citadel%02d.ogg") + || physfsSearchMusicToUpdate_helper_findModifiedMusic(NUMFORTRESSMUSIC, "music/fortress%02d.ogg") ) { return true; } @@ -1304,30 +1439,6 @@ void physfsReloadMusic(bool &introMusicChanged, bool reloadAll) //TODO: This sho return; } #ifdef SOUND - - std::vector themeMusic; - themeMusic.push_back("music/introduction.ogg"); - themeMusic.push_back("music/intermission.ogg"); - themeMusic.push_back("music/minetown.ogg"); - themeMusic.push_back("music/splash.ogg"); - themeMusic.push_back("music/library.ogg"); - themeMusic.push_back("music/shop.ogg"); - themeMusic.push_back("music/herxboss.ogg"); - themeMusic.push_back("music/temple.ogg"); - themeMusic.push_back("music/endgame.ogg"); - themeMusic.push_back("music/escape.ogg"); - themeMusic.push_back("music/devil.ogg"); - themeMusic.push_back("music/sanctum.ogg"); - themeMusic.push_back("music/gnomishmines.ogg"); - themeMusic.push_back("music/greatcastle.ogg"); - themeMusic.push_back("music/sokoban.ogg"); - themeMusic.push_back("music/caveslair.ogg"); - themeMusic.push_back("music/bramscastle.ogg"); - themeMusic.push_back("music/hamlet.ogg"); - themeMusic.push_back("music/tutorial.ogg"); - themeMusic.push_back("sound/Death.ogg"); - themeMusic.push_back("sound/ui/StoryMusicV3.ogg"); - int index = 0; #ifdef USE_OPENAL #define FMOD_System_CreateStream(A, B, C, D, E) OPENAL_CreateStreamSound(B, E) //TODO: If this is still needed, it's probably now broke! @@ -1337,7 +1448,8 @@ void physfsReloadMusic(bool &introMusicChanged, bool reloadAll) //TODO: This sho #define FMOD_Sound_Release OPENAL_Sound_Release int fmod_result; #endif - for ( std::vector::iterator it = themeMusic.begin(); it != themeMusic.end(); ++it ) + bool ensembleNeedsUpdate = false; + for ( auto it = themeMusic.begin(); it != themeMusic.end(); ++it ) { std::string filename = *it; if ( PHYSFS_getRealDir(filename.c_str()) != nullptr ) @@ -1644,6 +1756,93 @@ void physfsReloadMusic(bool &introMusicChanged, bool reloadAll) //TODO: This sho } break; default: +#ifdef USE_FMOD +#ifndef EDITOR + if ( index >= 21 && index < 21 + NUMENSEMBLEMUSIC * 5 ) + { + ensembleNeedsUpdate = true; + int c = (index - 21) % NUMENSEMBLEMUSIC; + if ( index >= 21 + NUMENSEMBLEMUSIC * 0 && index < 21 + NUMENSEMBLEMUSIC * 1 ) + { + fmod_result = ensembleSounds.exploreChannel[c] ? ensembleSounds.exploreChannel[c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.exploreSound[c] ? ensembleSounds.exploreSound[c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.exploreSound[c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 1 && index < 21 + NUMENSEMBLEMUSIC * 2 ) + { + fmod_result = ensembleSounds.combatChannel[c] ? ensembleSounds.combatChannel[c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.combatSound[c] ? ensembleSounds.combatSound[c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.combatSound[c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 2 && index < 21 + NUMENSEMBLEMUSIC * 3 ) + { + fmod_result = ensembleSounds.combatTransChannel[0][c] ? ensembleSounds.combatTransChannel[0][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.combatTransSound[0][c] ? ensembleSounds.combatTransSound[0][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.combatTransSound[0][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 3 && index < 21 + NUMENSEMBLEMUSIC * 4 ) + { + fmod_result = ensembleSounds.combatTransChannel[1][c] ? ensembleSounds.combatTransChannel[1][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.combatTransSound[1][c] ? ensembleSounds.combatTransSound[1][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.combatTransSound[1][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 4 && index < 21 + NUMENSEMBLEMUSIC * 5 ) + { + fmod_result = ensembleSounds.exploreTransChannel[3][c] ? ensembleSounds.exploreTransChannel[3][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.exploreTransSound[3][c] ? ensembleSounds.exploreTransSound[3][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.exploreTransSound[3][c]); + } + /*else if ( index >= 21 + NUMENSEMBLEMUSIC * 2 && index < 21 + NUMENSEMBLEMUSIC * 3 ) + { + fmod_result = ensembleSounds.exploreTransChannel[0][c] ? ensembleSounds.exploreTransChannel[0][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.exploreTransSound[0][c] ? ensembleSounds.exploreTransSound[0][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.exploreTransSound[0][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 3 && index < 21 + NUMENSEMBLEMUSIC * 4 ) + { + fmod_result = ensembleSounds.exploreTransChannel[1][c] ? ensembleSounds.exploreTransChannel[1][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.exploreTransSound[1][c] ? ensembleSounds.exploreTransSound[1][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.exploreTransSound[1][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 4 && index < 21 + NUMENSEMBLEMUSIC * 5 ) + { + fmod_result = ensembleSounds.exploreTransChannel[2][c] ? ensembleSounds.exploreTransChannel[2][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.exploreTransSound[2][c] ? ensembleSounds.exploreTransSound[2][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.exploreTransSound[2][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 5 && index < 21 + NUMENSEMBLEMUSIC * 6 ) + { + fmod_result = ensembleSounds.combatTransChannel[0][c] ? ensembleSounds.combatTransChannel[0][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.combatTransSound[0][c] ? ensembleSounds.combatTransSound[0][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.combatTransSound[0][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 6 && index < 21 + NUMENSEMBLEMUSIC * 7 ) + { + fmod_result = ensembleSounds.combatTransChannel[1][c] ? ensembleSounds.combatTransChannel[1][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.combatTransSound[1][c] ? ensembleSounds.combatTransSound[1][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.combatTransSound[1][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 7 && index < 21 + NUMENSEMBLEMUSIC * 8 ) + { + fmod_result = ensembleSounds.combatTransChannel[2][c] ? ensembleSounds.combatTransChannel[2][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.combatTransSound[2][c] ? ensembleSounds.combatTransSound[2][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.combatTransSound[2][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 8 && index < 21 + NUMENSEMBLEMUSIC * 9 ) + { + fmod_result = ensembleSounds.combatTransChannel[3][c] ? ensembleSounds.combatTransChannel[3][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.combatTransSound[3][c] ? ensembleSounds.combatTransSound[3][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.combatTransSound[3][c]); + } + else if ( index >= 21 + NUMENSEMBLEMUSIC * 9 && index < 21 + NUMENSEMBLEMUSIC * 10 ) + { + fmod_result = ensembleSounds.exploreTransChannel[3][c] ? ensembleSounds.exploreTransChannel[3][c]->stop() : FMOD_OK; + fmod_result = ensembleSounds.exploreTransSound[3][c] ? ensembleSounds.exploreTransSound[3][c]->release() : FMOD_OK; + fmod_result = fmod_system->createSound(musicDir.c_str(), FMOD_3D | FMOD_LOOP_NORMAL, nullptr, &ensembleSounds.exploreTransSound[3][c]); + }*/ + } +#endif +#endif break; } if ( FMODErrorCheck() ) @@ -1696,6 +1895,10 @@ void physfsReloadMusic(bool &introMusicChanged, bool reloadAll) //TODO: This sho { printlog("[PhysFS]: Failed to reload citadel music array."); } + if ( FMOD_OK != (fmod_result = physfsReloadMusic_helper_reloadMusicArray(NUMFORTRESSMUSIC, "music/fortress%02d.ogg", fortressmusic, reloadAll)) ) + { + printlog("[PhysFS]: Failed to reload fortress music array."); + } bool introChanged = false; @@ -1739,6 +1942,12 @@ void physfsReloadMusic(bool &introMusicChanged, bool reloadAll) //TODO: This sho } } +#ifdef USE_FMOD +#ifndef EDITOR + ensembleSounds.setup(); +#endif +#endif + introMusicChanged = introChanged; // use this variable outside of this function to start playing a new fresh list of tracks in the main menu. #ifdef USE_OPENAL #undef FMOD_System_CreateStream diff --git a/src/engine/audio/sound.hpp b/src/engine/audio/sound.hpp index fda7d7562..f82735a79 100644 --- a/src/engine/audio/sound.hpp +++ b/src/engine/audio/sound.hpp @@ -36,6 +36,7 @@ typedef int16_t opus_int16; #include #endif #endif +#include "../../interface/consolecommand.hpp" extern Uint32 numsounds; bool initSoundEngine(); //If it fails to initialize the sound engine, it'll just disable audio. @@ -99,9 +100,11 @@ extern FMOD::Sound* sokobanmusic; extern FMOD::Sound* caveslairmusic; extern FMOD::Sound* bramscastlemusic; extern FMOD::Sound* hamletmusic; +extern FMOD::Sound** fortressmusic; #define NUMCAVESMUSIC 3 #define NUMCITADELMUSIC 3 #define NUMINTROMUSIC 3 +#define NUMFORTRESSMUSIC 2 //TODO: Automatically scan the music folder for a mines subdirectory and use all the music for the mines or something like that. I'd prefer something neat like for that loading music for a level, anyway. And I can just reuse the code I had for ORR. extern FMOD::Channel* music_channel, *music_channel2, *music_resume; //TODO: List of music, play first one, fade out all the others? Eh, maybe some other day. //music_resume is the music to resume after, say, combat or shops. //TODO: Clear music_resume every biome change. Or otherwise validate it for that level set. @@ -109,6 +112,75 @@ extern FMOD::Channel* music_channel, *music_channel2, *music_resume; //TODO: Lis extern FMOD::ChannelGroup* sound_group, *music_group; extern FMOD::ChannelGroup* soundAmbient_group, *soundEnvironment_group, *music_notification_group, *soundNotification_group; +#define NUMENSEMBLEMUSIC 8 +extern FMOD::ChannelGroup* music_ensemble_global_send_group; +extern FMOD::ChannelGroup* music_ensemble_global_recv_group; +extern FMOD::ChannelGroup* music_ensemble_local_recv_player[MAXPLAYERS]; +extern FMOD::ChannelGroup* music_ensemble_local_recv_group; +#ifndef EDITOR +extern ConsoleVariable cvar_ensemble_vol_bg; +extern ConsoleVariable cvar_ensemble_explore_seek; +extern ConsoleVariable cvar_ensemble_combat_seek; +#endif +struct EnsembleSounds_t +{ + float ensemble_recv_global_volume = 0.f; + float ensemble_recv_player_volume = 0.f; + static const int NUM_EXPLORE_TRANS = 4; + static const int NUM_COMBAT_TRANS = 4; + FMOD::Sound* exploreSound[NUMENSEMBLEMUSIC] = { nullptr }; + FMOD::Channel* exploreChannel[NUMENSEMBLEMUSIC] = { nullptr }; + + FMOD::Sound* combatSound[NUMENSEMBLEMUSIC] = { nullptr }; + FMOD::Channel* combatChannel[NUMENSEMBLEMUSIC] = { nullptr }; + + FMOD::Sound* exploreTransSound[NUM_EXPLORE_TRANS][NUMENSEMBLEMUSIC] = { nullptr }; + FMOD::Channel* exploreTransChannel[NUM_EXPLORE_TRANS][NUMENSEMBLEMUSIC] = { nullptr }; + + FMOD::Sound* combatTransSound[NUM_COMBAT_TRANS][NUMENSEMBLEMUSIC] = { nullptr }; + FMOD::Channel* combatTransChannel[NUM_COMBAT_TRANS][NUMENSEMBLEMUSIC] = { nullptr }; + + FMOD::ChannelGroup* transceiver_group[NUMENSEMBLEMUSIC] = { nullptr }; + + enum SongTransitionState + { + TRANSITION_EXPLORE, + TRANSITION_COMBAT_START, + TRANSITION_COMBAT, + TRANSITION_COMBAT_ENDING, + TRANSITION_COMBAT_ENDED + }; + SongTransitionState songTransitionState = TRANSITION_EXPLORE; + enum TransitionMode + { + TRANSITION_MODE_FULL, + TRANSITION_MODE_FADE, + TRANSITION_MODE_FADE_HALF, + TRANSITION_MODE_DEFAULT + }; + TransitionMode songTransitionMode = TRANSITION_MODE_DEFAULT; + void setup(); + void playSong(); + void deinit(); + void stopPlaying(bool setCombatDelay); + unsigned int exploreSoundSyncPointInterval = 0; + unsigned int combatSoundSyncPointInterval = 0; + int exploreSongSeek = 0; + int combatSongSeek = 0; + std::vector exploreSyncPoints; + std::vector exploreSyncPointsToSeek; + std::vector exploreSyncPointsUnique; + std::vector combatSyncPoints; + void updatePlayingChannelVolumes(); + int combatBeat = 0; + Uint32 ticksCombatPlaying = 0; + Uint32 lastTickCombatPlaying = 0; + Uint32 combatDelay = 0; + Uint32 lastUpdateTick = 0; +}; +extern EnsembleSounds_t ensembleSounds; + + /* * Checks for FMOD errors. Store return value of all FMOD functions in fmod_result so that this funtion can access it and check for errors. * Returns true on error (and prints an error message), false if everything went fine. diff --git a/src/engine/audio/sound_game.cpp b/src/engine/audio/sound_game.cpp index a621daad2..be52db568 100644 --- a/src/engine/audio/sound_game.cpp +++ b/src/engine/audio/sound_game.cpp @@ -1883,6 +1883,10 @@ void VoiceChat_t::updateOnMapChange3DRolloff() void VoiceChat_t::PlayerChannels_t::setupPlayback() { + if ( no_sound ) + { + return; + } const int windowSize = desiredLatency * sizeof(short) * VoiceChat.nativeChannels; FMOD_CREATESOUNDEXINFO exinfoBuffer = { 0 }; exinfoBuffer.cbsize = sizeof(FMOD_CREATESOUNDEXINFO); @@ -3237,4 +3241,1206 @@ int VoiceChat_t::RingBuffer::GetReadAvail() { return _size - _writeBytesAvail; } -#endif + +EnsembleSounds_t ensembleSounds; + +FMOD_RESULT F_CALLBACK ensembleExplorationCallback(FMOD_CHANNELCONTROL* channelcontrol, + FMOD_CHANNELCONTROL_TYPE controltype, + FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype, + void* commanddata1, + void* commanddata2); + +FMOD_RESULT F_CALLBACK ensembleCombatCallback(FMOD_CHANNELCONTROL* channelcontrol, + FMOD_CHANNELCONTROL_TYPE controltype, + FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype, + void* commanddata1, + void* commanddata2); + +FMOD_VECTOR ensemble_global_position {0.f, 0.f, 0.f}; + +void EnsembleSounds_t::playSong() +{ + songTransitionState = TRANSITION_EXPLORE; + unsigned int dsp_block_len = 0; + fmod_result = fmod_system->getDSPBufferSize(&dsp_block_len, 0); + int outputrate = 0; + fmod_result = fmod_system->getSoftwareFormat(&outputrate, 0, 0); + FMODErrorCheck(); + unsigned long long clock_start = 0; + + float buffer_ms = (dsp_block_len * 1000.f / (float)outputrate); + + for ( int i = 0; i < NUMENSEMBLEMUSIC; i++ ) + { + fmod_result = fmod_system->playSound(exploreSound[i], transceiver_group[i], true, &exploreChannel[i]); + fmod_result = exploreChannel[i]->setCallback(ensembleExplorationCallback); + + fmod_result = fmod_system->playSound(combatSound[i], transceiver_group[i], true, &combatChannel[i]); + fmod_result = combatChannel[i]->setCallback(ensembleCombatCallback); + + unsigned int songPos = 0; + FMOD_SYNCPOINT* syncPoint = nullptr; + fmod_result = exploreSound[i]->getSyncPoint(exploreSongSeek, &syncPoint); + fmod_result = exploreSound[i]->getSyncPointInfo(syncPoint, nullptr, 0, &songPos, FMOD_TIMEUNIT_PCM); + fmod_result = exploreChannel[i]->setPosition(songPos, FMOD_TIMEUNIT_PCM); + + unsigned long long clock_now = 0; + fmod_result = exploreChannel[i]->getDSPClock(0, &clock_now); + if ( !clock_start ) + { + clock_start = clock_now; + clock_start += (dsp_block_len * 5); + } + assert(clock_now < clock_start); + + /*if ( !clock_start ) + { + } + else + { + float freq; + unsigned int slen = 0; + fmod_result = sound[count]->getLength(&slen, FMOD_TIMEUNIT_PCM); + + fmod_result = sound[count]->getDefaults(&freq, 0); + slen = (unsigned int)((float)slen / freq * outputrate); + + clock_start += slen; + }*/ + + exploreChannel[i]->setDelay(clock_start, 0, false); + + unsigned long long fade_delay = (1000.f / buffer_ms) * dsp_block_len; + fmod_result = exploreChannel[i]->removeFadePoints(0, (unsigned long long)(-1)); + fmod_result = exploreChannel[i]->addFadePoint(clock_start, 0.f); + fmod_result = exploreChannel[i]->addFadePoint(clock_start + fade_delay, 1.f); + + fmod_result = exploreChannel[i]->setPaused(false); + + transceiver_group[i]->setVolume(0.f); + + fmod_result = exploreChannel[i]->setMode(FMOD_3D_HEADRELATIVE); + fmod_result = exploreChannel[i]->set3DAttributes(&ensemble_global_position, nullptr); + fmod_result = combatChannel[i]->setMode(FMOD_3D_HEADRELATIVE); + fmod_result = combatChannel[i]->set3DAttributes(&ensemble_global_position, nullptr); + } +} + +void EnsembleSounds_t::stopPlaying(bool setCombatDelay) +{ + songTransitionState = TRANSITION_EXPLORE; + if ( setCombatDelay ) + { + combatDelay = TICKS_PER_SECOND * 5; + } + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + fmod_result = exploreChannel[i] ? exploreChannel[i]->stop() : FMOD_OK; + exploreChannel[i] = nullptr; + fmod_result = combatChannel[i] ? combatChannel[i]->stop() : FMOD_OK; + combatChannel[i] = nullptr; + + for ( int j = 0; j < NUM_COMBAT_TRANS; ++j ) + { + fmod_result = combatTransChannel[j][i] ? combatTransChannel[j][i]->stop() : FMOD_OK; + combatTransChannel[j][i] = nullptr; + } + for ( int j = 0; j < NUM_EXPLORE_TRANS; ++j ) + { + fmod_result = exploreTransChannel[j][i] ? exploreTransChannel[j][i]->stop() : FMOD_OK; + exploreTransChannel[j][i] = nullptr; + } + } +} + +void EnsembleSounds_t::deinit() +{ + stopPlaying(true); + + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + fmod_result = exploreChannel[i] ? exploreChannel[i]->stop() : FMOD_OK; + exploreChannel[i] = nullptr; + fmod_result = combatChannel[i] ? combatChannel[i]->stop() : FMOD_OK; + combatChannel[i] = nullptr; + fmod_result = exploreSound[i] ? exploreSound[i]->release() : FMOD_OK; + exploreSound[i] = nullptr; + fmod_result = combatSound[i] ? combatSound[i]->release() : FMOD_OK; + combatSound[i] = nullptr; + + for ( int j = 0; j < NUM_COMBAT_TRANS; ++j ) + { + fmod_result = combatTransChannel[j][i] ? combatTransChannel[j][i]->stop() : FMOD_OK; + combatTransChannel[j][i] = nullptr; + fmod_result = combatTransSound[j][i] ? combatTransSound[j][i]->release() : FMOD_OK; + combatTransSound[j][i] = nullptr; + } + for ( int j = 0; j < NUM_EXPLORE_TRANS; ++j ) + { + fmod_result = exploreTransChannel[j][i] ? exploreTransChannel[j][i]->stop() : FMOD_OK; + exploreTransChannel[j][i] = nullptr; + fmod_result = exploreTransSound[j][i] ? exploreTransSound[j][i]->release() : FMOD_OK; + exploreTransSound[j][i] = nullptr; + } + } +} + +static ConsoleCommand ccmd_ensemble_transition_mode("/ensemble_transition_mode", "", + [](int argc, const char* argv[]) { + if ( argc > 1 ) + { + ensembleSounds.songTransitionMode = (EnsembleSounds_t::TransitionMode)atoi(argv[1]); + messagePlayer(clientnum, MESSAGE_HINT, "Set transition mode to: %d", (int)ensembleSounds.songTransitionMode); + } +}); + +static ConsoleCommand ccmd_ensemble_transition_state("/ensemble_transition_state", "", + [](int argc, const char* argv[]) { + if ( argc > 1 ) + { + ensembleSounds.songTransitionState = (EnsembleSounds_t::SongTransitionState)atoi(argv[1]); + messagePlayer(clientnum, MESSAGE_HINT, "Set transition state to: %d", (int)ensembleSounds.songTransitionState); + } + }); + +void EnsembleSounds_t::setup() +{ + songTransitionState = TRANSITION_EXPLORE; + + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + fmod_result = exploreChannel[i] ? exploreChannel[i]->stop() : FMOD_OK; + fmod_result = combatChannel[i] ? combatChannel[i]->stop() : FMOD_OK; + //fmod_result = exploreSound[i] ? exploreSound[i]->release() : FMOD_OK; + //fmod_result = combatSound[i] ? combatSound[i]->release() : FMOD_OK; + for ( int j = 0; j < NUM_COMBAT_TRANS; ++j ) + { + fmod_result = combatTransChannel[j][i] ? combatTransChannel[j][i]->stop() : FMOD_OK; + //fmod_result = combatTransSound[j][i] ? combatTransSound[j][i]->release() : FMOD_OK; + } + for ( int j = 0; j < NUM_EXPLORE_TRANS; ++j ) + { + fmod_result = exploreTransChannel[j][i] ? exploreTransChannel[j][i]->stop() : FMOD_OK; + //fmod_result = exploreTransSound[j][i] ? exploreTransSound[j][i]->release() : FMOD_OK; + } + + if ( !transceiver_group[i] ) + { + fmod_result = fmod_system->createChannelGroup(nullptr, &transceiver_group[i]); + + FMOD::DSP* transceiver = nullptr; + fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_TRANSCEIVER, &transceiver); + fmod_result = transceiver_group[i]->addDSP(0, transceiver); + fmod_result = transceiver->setParameterBool(FMOD_DSP_TRANSCEIVER_TRANSMIT, true); // sending signal + fmod_result = transceiver->setParameterInt(FMOD_DSP_TRANSCEIVER_CHANNEL, 1 + i); // sending on channel x + + music_ensemble_global_send_group->addGroup(transceiver_group[i]); + } + + { + int syncPoints = 0; + exploreSound[i]->getNumSyncPoints(&syncPoints); + for ( int point = 0; point < syncPoints; ++point ) + { + FMOD_SYNCPOINT* syncpoint = nullptr; + fmod_result = exploreSound[i]->getSyncPoint(point, &syncpoint); + if ( syncpoint ) + { + exploreSound[i]->deleteSyncPoint(syncpoint); + } + } + + FMOD_SOUND_TYPE type; + int chan = 0; + unsigned int len = 0; + exploreSound[i]->getLength(&len, FMOD_TIMEUNIT_PCM); + exploreSound[i]->getFormat(&type, nullptr, &chan, nullptr); + float freq = 0; + exploreSound[i]->getDefaults(&freq, nullptr); + int beatTime = freq * (60 / 120.f); // beats per sample, 120bpm + + int interval = 1; + int numBeats = 2; + exploreSoundSyncPointInterval = interval * (beatTime * numBeats); + unsigned int beat = beatTime * 3 / 4; // starting point from beginning of song + //exploreSound[i]->setLoopPoints(0, FMOD_TIMEUNIT_PCM, len - beat, FMOD_TIMEUNIT_PCM); + int numSyncPoints = ((len - beat) / (interval * beatTime * numBeats)) + 1; + int index = -1; + if ( i == 0 ) + { + exploreSyncPoints.clear(); + exploreSyncPointsToSeek.clear(); + exploreSyncPointsUnique.clear(); + } + int seekBar = -1; + int oldSeekBar = -1; + while ( beat < len ) + { + ++index; + if ( i == 0 ) + { + if ( index == 0 ) + { + seekBar = -1; // no action + } + else if ( index < 166 ) + { + seekBar = (index / 8) * 8; + } + else if ( index < 310 ) + { + seekBar = 166 + ((index - 166) / 8) * 8; + } + else if ( index == 310 || index == 311 ) + { + seekBar = 302; + } + else if ( index < 360 ) + { + seekBar = 312 + ((index - 312) / 8) * 8; + } + else if ( index == 360 || index == 361 ) + { + seekBar = 352; + } + else if ( index >= 362 ) + { + seekBar = 362; + } + if ( oldSeekBar != seekBar ) + { + exploreSyncPointsUnique.push_back(seekBar); + } + oldSeekBar = seekBar; + } + fmod_result = exploreSound[i]->addSyncPoint(beat, FMOD_TIMEUNIT_PCM, nullptr, nullptr); + if ( i == 0 ) + { + exploreSyncPoints.push_back(beat); + exploreSyncPointsToSeek.push_back(seekBar); + } + FMODErrorCheck(); + beat += interval * (beatTime * numBeats); + } + + fmod_result = fmod_system->playSound(exploreSound[i], transceiver_group[i], true, &exploreChannel[i]); + fmod_result = exploreChannel[i]->setMode(FMOD_3D_HEADRELATIVE); + fmod_result = exploreChannel[i]->set3DAttributes(&ensemble_global_position, nullptr); + FMODErrorCheck(); + fmod_result = exploreChannel[i]->setCallback(ensembleExplorationCallback); + } + + + { + int syncPoints = 0; + combatSound[i]->getNumSyncPoints(&syncPoints); + for ( int point = 0; point < syncPoints; ++point ) + { + FMOD_SYNCPOINT* syncpoint = nullptr; + combatSound[i]->getSyncPoint(point, &syncpoint); + if ( syncpoint ) + { + combatSound[i]->deleteSyncPoint(syncpoint); + } + } + + FMOD_SOUND_TYPE type; + int chan = 0; + unsigned int len = 0; + combatSound[i]->getLength(&len, FMOD_TIMEUNIT_PCM); + combatSound[i]->getFormat(&type, nullptr, &chan, nullptr); + float freq = 0; + combatSound[i]->getDefaults(&freq, nullptr); + int beatTime = freq * (60 / 90.f) / 8.f; // eighths, 90bpm + combatBeat = beatTime; + int interval = 2; + int numBeats = 7; + combatSoundSyncPointInterval = interval * (beatTime * numBeats); + unsigned int beat = beatTime * 0; // starting point from beginning of song + int numSyncPoints = ((len - beat) / (interval * beatTime * numBeats)) + 1; + int index = -1; + if ( i == 0 ) + { + combatSyncPoints.clear(); + } + while ( beat < len ) + { + ++index; + fmod_result = combatSound[i]->addSyncPoint(beat, FMOD_TIMEUNIT_PCM, nullptr, nullptr); + if ( i == 0 ) + { + combatSyncPoints.push_back(beat); + } + FMODErrorCheck(); + beat += interval * beatTime * numBeats; + } + + fmod_result = fmod_system->playSound(combatSound[i], transceiver_group[i], true, &combatChannel[i]); + fmod_result = combatChannel[i]->setMode(FMOD_3D_HEADRELATIVE); + fmod_result = combatChannel[i]->set3DAttributes(&ensemble_global_position, nullptr); + FMODErrorCheck(); + combatChannel[i]->setCallback(ensembleCombatCallback); + } + + for ( int j = 0; j < NUM_EXPLORE_TRANS; ++j ) + { + int syncPoints = 0; + exploreTransSound[j][i]->getNumSyncPoints(&syncPoints); + for ( int point = 0; point < syncPoints; ++point ) + { + FMOD_SYNCPOINT* syncpoint = nullptr; + fmod_result = exploreTransSound[j][i]->getSyncPoint(point, &syncpoint); + if ( syncpoint ) + { + exploreTransSound[j][i]->deleteSyncPoint(syncpoint); + } + } + + FMOD_SOUND_TYPE type; + int chan = 0; + unsigned int len = 0; + exploreTransSound[j][i]->getLength(&len, FMOD_TIMEUNIT_PCM); + exploreTransSound[j][i]->getFormat(&type, nullptr, &chan, nullptr); + float freq = 0; + exploreTransSound[j][i]->getDefaults(&freq, nullptr); + int beatTime = freq * (60 / 120.f); // beats per sample, 120bpm + + int interval = 1; + int numBeats = 4; + unsigned int beat = interval * beatTime * numBeats; + if ( j == 3 ) + { + fmod_result = exploreTransSound[j][i]->addSyncPoint(0, FMOD_TIMEUNIT_PCM, nullptr, nullptr); + fmod_result = exploreTransSound[j][i]->addSyncPoint(8 * beat / 16, FMOD_TIMEUNIT_PCM, nullptr, nullptr); + } + else + { + fmod_result = exploreTransSound[j][i]->addSyncPoint(4 * beat / 8, FMOD_TIMEUNIT_PCM, nullptr, nullptr); + fmod_result = exploreTransSound[j][i]->addSyncPoint(beat, FMOD_TIMEUNIT_PCM, nullptr, nullptr); + } + FMODErrorCheck(); + //result = fmod_system->playSound(exploreTransSound[j][i], groupTx[i], true, &channelTrans[j][count]); + FMODErrorCheck(); + } + + for ( int j = 0; j < NUM_COMBAT_TRANS; ++j ) + { + int syncPoints = 0; + combatTransSound[j][i]->getNumSyncPoints(&syncPoints); + for ( int point = 0; point < syncPoints; ++point ) + { + FMOD_SYNCPOINT* syncpoint = nullptr; + fmod_result = combatTransSound[j][i]->getSyncPoint(point, &syncpoint); + if ( syncpoint ) + { + combatTransSound[j][i]->deleteSyncPoint(syncpoint); + } + } + + FMOD_SOUND_TYPE type; + int chan = 0; + unsigned int len = 0; + combatTransSound[j][i]->getLength(&len, FMOD_TIMEUNIT_PCM); + combatTransSound[j][i]->getFormat(&type, nullptr, &chan, nullptr); + float freq = 0; + combatTransSound[j][i]->getDefaults(&freq, nullptr); + int beatTime = freq * (60 / 90.f); // beats per sample, 120bpm + + int interval = 2; + if ( j == 3 ) + { + interval = 1; + } + int numBeats = 4; + unsigned int beat = interval * beatTime * numBeats; + fmod_result = combatTransSound[j][i]->addSyncPoint(3 * beat / 8, FMOD_TIMEUNIT_PCM, nullptr, nullptr); + fmod_result = combatTransSound[j][i]->addSyncPoint(beat, FMOD_TIMEUNIT_PCM, nullptr, nullptr); + FMODErrorCheck(); + //result = fmod_system->playSound(combatTransSound[j][i], groupTx[i], true, &channelCombatTrans[j][count]); + FMODErrorCheck(); + } + } +} + +static ConsoleVariable cvar_ensemble_combat_length("/ensemble_combat_length", 30); + +FMOD_RESULT F_CALLBACK ensembleCombatCallback(FMOD_CHANNELCONTROL* channelcontrol, + FMOD_CHANNELCONTROL_TYPE controltype, + FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype, + void* commanddata1, + void* commanddata2) +{ + FMOD_RESULT result; + if ( callbacktype == FMOD_CHANNELCONTROL_CALLBACK_TYPE::FMOD_CHANNELCONTROL_CALLBACK_SYNCPOINT ) + { + int currentSyncPoint = reinterpret_cast(commanddata1); + FMOD::Channel* chan = reinterpret_cast(channelcontrol); + FMOD::Sound* sound = nullptr; + chan->getCurrentSound(&sound); + if ( ensembleSounds.songTransitionState == EnsembleSounds_t::TRANSITION_COMBAT_ENDING ) + { + if ( sound == ::ensembleSounds.combatSound[0] ) + { + unsigned int currentPos = 0; + chan->getPosition(¤tPos, FMOD_TIMEUNIT_PCM); + + FMOD_SYNCPOINT* syncPoint = nullptr; + sound->getSyncPoint(currentSyncPoint, &syncPoint); + unsigned int currentOffset = 0; + result = sound->getSyncPointInfo(syncPoint, nullptr, 0, ¤tOffset, FMOD_TIMEUNIT_PCM); + + if ( currentPos >= currentOffset // check our callback is within expected sync range + && (currentPos < currentOffset + ensembleSounds.combatSoundSyncPointInterval) ) + { + int syncInterval = 8 / 2; + int syncpointNext = currentSyncPoint + syncInterval - currentSyncPoint % (syncInterval); + /*if ( currentSyncPoint % (syncInterval * 2) >= syncInterval ) + { + syncpointNext += syncInterval; + }*/ + + bool noTransition = false; + if ( ensembleSounds.ticksCombatPlaying < *cvar_ensemble_combat_length * TICKS_PER_SECOND ) + { + noTransition = true; + } + + if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE + || ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE_HALF + || noTransition ) + { + syncpointNext = currentSyncPoint + 1; + } + + FMOD_SYNCPOINT* syncPoint2 = nullptr; + int numSyncPoints = 0; + sound->getNumSyncPoints(&numSyncPoints); + while ( syncpointNext >= numSyncPoints ) + { + syncpointNext -= numSyncPoints; + } + + sound->getSyncPoint(syncpointNext, &syncPoint2); + + unsigned int soundLength = 0; + sound->getLength(&soundLength, FMOD_TIMEUNIT_PCM); + + unsigned int soundPos = 0; + chan->getPosition(&soundPos, FMOD_TIMEUNIT_PCM); + + unsigned int offset2 = 0; + result = sound->getSyncPointInfo(syncPoint2, nullptr, 0, &offset2, FMOD_TIMEUNIT_PCM); + + unsigned int delayLength = 0; + if ( soundPos > offset2 ) + { + // wrap ahead + delayLength = (soundLength - soundPos) + offset2; + } + else + { + delayLength = offset2 - soundPos; + } + + ensembleSounds.songTransitionState = EnsembleSounds_t::TRANSITION_COMBAT_ENDED; + + //chan->addFadePoint(clock_now + (unsigned long long)(rate * 2.0), 0.f); + int combatTransition = 0; + if ( syncpointNext >= 28 && syncpointNext <= 32 ) + { + combatTransition = 1; + } + if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE_HALF ) + { + combatTransition = 0; + } + + float volume = 0.f; + chan->getVolume(&volume); + /*if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE_HALF && volume < 0.5f ) + { + noTransition = true; + delayLength = ensembleSounds.combatSoundSyncPointInterval; + }*/ + auto mainChannelDelay = delayLength; + + unsigned int buffersize = 0; + fmod_system->getDSPBufferSize(&buffersize, nullptr); + int sampleRate = 1; + fmod_system->getSoftwareFormat(&sampleRate, nullptr, nullptr); + float buffer_ms = (buffersize * 1000.f / (float)sampleRate); + + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + unsigned long long clock_now = 0; + result = ensembleSounds.combatChannel[i]->getDSPClock(0, &clock_now); + result = ensembleSounds.combatChannel[i]->setDelay(0, clock_now + (unsigned long long)(mainChannelDelay), false); + + if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_DEFAULT && noTransition ) + { + result = ensembleSounds.combatChannel[i]->addFadePoint(clock_now, 1.f); + result = ensembleSounds.combatChannel[i]->addFadePoint(clock_now + (unsigned long long)(mainChannelDelay), 0.f); + } + else if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_DEFAULT && !noTransition ) + { + result = ensembleSounds.combatChannel[i]->addFadePoint(clock_now + (unsigned long long)(mainChannelDelay) - 500.f * buffer_ms, 1.f); + result = ensembleSounds.combatChannel[i]->addFadePoint(clock_now + (unsigned long long)(mainChannelDelay), 0.f); + } + + unsigned int transitionLength = 0; + result = ensembleSounds.combatTransSound[combatTransition][i]->getLength(&transitionLength, FMOD_TIMEUNIT_PCM); + //result = channelCombatTrans[combatTransition][i]->setPaused(true); + + FMOD_SYNCPOINT* syncPoint4 = nullptr; + unsigned int offset3 = 0; + if ( ensembleSounds.songTransitionMode != EnsembleSounds_t::TRANSITION_MODE_FADE && !noTransition ) + { + result = fmod_system->playSound(ensembleSounds.combatTransSound[combatTransition][i], + ensembleSounds.transceiver_group[i], true, &ensembleSounds.combatTransChannel[combatTransition][i]); + result = ensembleSounds.combatTransChannel[combatTransition][i]->setMode(FMOD_3D_HEADRELATIVE); + result = ensembleSounds.combatTransChannel[combatTransition][i]->set3DAttributes(&ensemble_global_position, nullptr); + + FMOD_SYNCPOINT* syncPoint3 = nullptr; + result = ensembleSounds.combatTransSound[combatTransition][i]->getSyncPoint(1, &syncPoint3); + result = ensembleSounds.combatTransSound[combatTransition][i]->getSyncPointInfo(syncPoint3, nullptr, + 0, &offset3, FMOD_TIMEUNIT_PCM); + + if ( ensembleSounds.songTransitionMode != EnsembleSounds_t::TRANSITION_MODE_FADE_HALF ) + { + result = ensembleSounds.combatTransChannel[combatTransition][i]->setPosition(0, FMOD_TIMEUNIT_PCM); + } + else if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE_HALF ) + { + result = ensembleSounds.combatTransSound[combatTransition][i]->getSyncPoint(0, &syncPoint4); + unsigned int offset4 = 0; + result = ensembleSounds.combatTransSound[combatTransition][i]->getSyncPointInfo(syncPoint4, nullptr, + 0, &offset4, FMOD_TIMEUNIT_PCM); + + ensembleSounds.combatTransChannel[combatTransition][i]->setPosition(offset4, FMOD_TIMEUNIT_PCM); + + unsigned int beat = ensembleSounds.combatBeat * 64; + offset3 = offset3 - offset4; + offset3 -= beat / 4; + transitionLength -= offset4; + } + + result = ensembleSounds.combatTransChannel[combatTransition][i]->setDelay(clock_now + (unsigned long long)(delayLength), + clock_now + (unsigned long long)(delayLength) + transitionLength, true); + { + unsigned long long delay = 0; + result = ensembleSounds.combatTransChannel[combatTransition][i]->getDelay(&delay, nullptr); + + result = ensembleSounds.combatTransChannel[combatTransition][i]->removeFadePoints(0, (unsigned long long)(-1)); + result = ensembleSounds.combatTransChannel[combatTransition][i]->addFadePoint(delay, 0.f); + unsigned long long fade_delay = (10.f / buffer_ms) * buffersize; + result = ensembleSounds.combatTransChannel[combatTransition][i]->addFadePoint(delay + fade_delay, 1.f); + } + + result = ensembleSounds.combatTransChannel[combatTransition][i]->setPaused(false); + } + + result = ensembleSounds.exploreChannel[i]->setPaused(true); + result = ensembleSounds.exploreChannel[i]->setPosition(0, FMOD_TIMEUNIT_PCM); + + //ensembleSounds.exploreChannel[i]->setVolume(0.f); + //ensembleSounds.combatTransChannel[combatTransition][i]->setVolume(0.f); + + // code to seek to position if needed, but have pickup notes at 0:00 currently + { + unsigned int songPos = 0; + result = ensembleSounds.exploreSound[i]->getSyncPoint(ensembleSounds.exploreSongSeek, &syncPoint); + result = ensembleSounds.exploreSound[i]->getSyncPointInfo(syncPoint, nullptr, 0, &songPos, FMOD_TIMEUNIT_PCM); + result = ensembleSounds.exploreChannel[i]->setPosition(songPos, FMOD_TIMEUNIT_PCM); + } + + result = ensembleSounds.exploreChannel[i]->setDelay(clock_now + (unsigned long long)(delayLength + offset3), 0, false); + + { + unsigned long long delay = 0; + result = ensembleSounds.exploreChannel[i]->getDelay(&delay, nullptr); + + unsigned long long fade_delay = (1000.f / buffer_ms) * buffersize; + + result = ensembleSounds.exploreChannel[i]->removeFadePoints(0, (unsigned long long)(-1)); + result = ensembleSounds.exploreChannel[i]->addFadePoint(delay, 0.f); + result = ensembleSounds.exploreChannel[i]->addFadePoint(delay + fade_delay, 1.f); + } + result = ensembleSounds.exploreChannel[i]->setPaused(false); + } + } + } + } + + if ( false && currentSyncPoint % 8 == 0 ) + { + unsigned int currentOffset = 0; + FMOD_SYNCPOINT* syncPoint = nullptr; + result = sound->getSyncPoint(currentSyncPoint, &syncPoint); + result = sound->getSyncPointInfo(syncPoint, nullptr, 0, ¤tOffset, FMOD_TIMEUNIT_PCM); + + currentSyncPoint = ensembleSounds.combatSongSeek * 8; + + unsigned int offset = 0; + result = sound->getSyncPoint(currentSyncPoint, &syncPoint); + result = sound->getSyncPointInfo(syncPoint, nullptr, 0, &offset, FMOD_TIMEUNIT_PCM); + + unsigned int currentPos = 0; + result = chan->getPosition(¤tPos, FMOD_TIMEUNIT_PCM); + + // this goes crazy and calls back every missed sync point, twice for the same setpoint + if ( currentPos >= currentOffset // check our callback is within expected sync range + && (currentPos < currentOffset + ensembleSounds.combatSoundSyncPointInterval) // check our callback is within expected sync range + && (abs((int)currentPos - (int)offset) > ((ensembleSounds.combatSoundSyncPointInterval) * 0.9)) ) // check our position is sufficiently far and not a double up + { + //currentPos = currentPos % soundCombatSyncPointInterval; // callback interval + currentPos = currentPos - currentOffset; + result = chan->setPosition(offset + currentPos, FMOD_TIMEUNIT_PCM); + } + //chan->getPosition(&pos, FMOD_TIMEUNIT_MS); + } + } + + return FMOD_OK; +} + +FMOD_RESULT F_CALLBACK ensembleExplorationCallback(FMOD_CHANNELCONTROL* channelcontrol, + FMOD_CHANNELCONTROL_TYPE controltype, + FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype, + void* commanddata1, + void* commanddata2) +{ + FMOD_RESULT result = FMOD_OK; + if ( callbacktype == FMOD_CHANNELCONTROL_CALLBACK_TYPE::FMOD_CHANNELCONTROL_CALLBACK_SYNCPOINT ) + { + int currentSyncPoint = reinterpret_cast(commanddata1); + FMOD::Channel* chan = reinterpret_cast(channelcontrol); + FMOD::Sound* sound = nullptr; + chan->getCurrentSound(&sound); + + if ( sound == ::ensembleSounds.exploreSound[0] ) // should this be [5] ?? + { + if ( ensembleSounds.songTransitionState == EnsembleSounds_t::TRANSITION_COMBAT_ENDED ) + { + ensembleSounds.songTransitionState = EnsembleSounds_t::TRANSITION_EXPLORE; + } + if ( ensembleSounds.songTransitionState == EnsembleSounds_t::TRANSITION_COMBAT_START && currentSyncPoint > 0 ) + { + unsigned int currentPos = 0; + chan->getPosition(¤tPos, FMOD_TIMEUNIT_PCM); + + FMOD_SYNCPOINT* syncPoint = nullptr; + sound->getSyncPoint(currentSyncPoint, &syncPoint); + unsigned int currentOffset = 0; + result = sound->getSyncPointInfo(syncPoint, nullptr, 0, ¤tOffset, FMOD_TIMEUNIT_PCM); + + if ( currentPos >= currentOffset // check our callback is within expected sync range + && (currentPos < currentOffset + ensembleSounds.exploreSoundSyncPointInterval) ) + { + int syncInterval = 8 / 2; + int syncpointNext = currentSyncPoint + syncInterval - currentSyncPoint % (syncInterval); + if ( currentSyncPoint % (syncInterval * 2) >= syncInterval ) + { + syncpointNext += syncInterval; + } + + if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE + || ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE_HALF + || ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_DEFAULT ) + { + syncpointNext = currentSyncPoint + 1; + } + + FMOD_SYNCPOINT* syncPoint2 = nullptr; + int numSyncPoints = 0; + sound->getNumSyncPoints(&numSyncPoints); + while ( syncpointNext >= numSyncPoints ) + { + syncpointNext -= numSyncPoints; + } + + sound->getSyncPoint(syncpointNext, &syncPoint2); + + unsigned int soundLength = 0; + sound->getLength(&soundLength, FMOD_TIMEUNIT_PCM); + + unsigned int offset2 = 0; + result = sound->getSyncPointInfo(syncPoint2, nullptr, 0, &offset2, FMOD_TIMEUNIT_PCM); + + unsigned int delayLength = 0; + if ( currentPos > offset2 ) + { + // wrap ahead + delayLength = (soundLength - currentPos) + offset2; + } + else + { + delayLength = offset2 - currentPos; + } + + ensembleSounds.songTransitionState = EnsembleSounds_t::TRANSITION_COMBAT; + + //chan->addFadePoint(clock_now + (unsigned long long)(rate * 2.0), 0.f); + int explorationTransition = 2; + if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_DEFAULT ) + { + explorationTransition = 3; + } + else if ( syncpointNext == 20 ) + { + explorationTransition = 1; + } + else if ( syncpointNext == 28 ) + { + explorationTransition = 0; + } + + float volume = 0.f; + chan->getVolume(&volume); + bool noTransition = false; + if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE_HALF && volume < 0.5f ) + { + //noTransition = true; + delayLength = ensembleSounds.exploreSoundSyncPointInterval / 4; + } + auto mainChannelDelay = delayLength * 2; + if ( ensembleSounds.songTransitionMode != EnsembleSounds_t::TRANSITION_MODE_DEFAULT ) + { + mainChannelDelay = delayLength; + } + + unsigned int buffersize = 0; + fmod_system->getDSPBufferSize(&buffersize, nullptr); + int sampleRate = 1; + fmod_system->getSoftwareFormat(&sampleRate, nullptr, nullptr); + float buffer_ms = (buffersize * 1000.f / (float)sampleRate); + + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + unsigned long long clock_now = 0; + result = ensembleSounds.exploreChannel[i]->getDSPClock(0, &clock_now); + result = ensembleSounds.exploreChannel[i]->setDelay(0, clock_now + (unsigned long long)(mainChannelDelay), false); + + { + result = ensembleSounds.exploreChannel[i]->removeFadePoints(0, (unsigned long long)(-1)); + result = ensembleSounds.exploreChannel[i]->addFadePoint(clock_now, 1.f); + result = ensembleSounds.exploreChannel[i]->addFadePoint(clock_now + (unsigned long long)(mainChannelDelay), 0.f); + } + + unsigned int transitionLength = 0; + result = ensembleSounds.exploreTransSound[explorationTransition][i]->getLength(&transitionLength, FMOD_TIMEUNIT_PCM); + //result = channelTrans[explorationTransition][i]->setPaused(true); + + FMOD_SYNCPOINT* syncPoint4 = nullptr; + unsigned int offset3 = 0; + if ( ensembleSounds.songTransitionMode != EnsembleSounds_t::TRANSITION_MODE_FADE && !noTransition ) + { + if ( ensembleSounds.exploreTransChannel[explorationTransition][i] ) + { + ensembleSounds.exploreTransChannel[explorationTransition][i]->stop(); + } + result = fmod_system->playSound(ensembleSounds.exploreTransSound[explorationTransition][i], + ensembleSounds.transceiver_group[i], true, &ensembleSounds.exploreTransChannel[explorationTransition][i]); + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->setMode(FMOD_3D_HEADRELATIVE); + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->set3DAttributes(&ensemble_global_position, nullptr); + + FMOD_SYNCPOINT* syncPoint3 = nullptr; + result = ensembleSounds.exploreTransSound[explorationTransition][i]->getSyncPoint(1, &syncPoint3); + result = ensembleSounds.exploreTransSound[explorationTransition][i]->getSyncPointInfo(syncPoint3, nullptr, + 0, &offset3, FMOD_TIMEUNIT_PCM); + + if ( ensembleSounds.songTransitionMode != EnsembleSounds_t::TRANSITION_MODE_FADE_HALF ) + { + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->setPosition(0, FMOD_TIMEUNIT_PCM); + } + else if ( ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_FADE_HALF + || ensembleSounds.songTransitionMode == EnsembleSounds_t::TRANSITION_MODE_DEFAULT ) + { + result = ensembleSounds.exploreTransSound[explorationTransition][i]->getSyncPoint(0, &syncPoint4); + unsigned int offset4 = 0; + result = ensembleSounds.exploreTransSound[explorationTransition][i]->getSyncPointInfo(syncPoint4, nullptr, + 0, &offset4, FMOD_TIMEUNIT_PCM); + + ensembleSounds.exploreTransChannel[explorationTransition][i]->setPosition(offset4, FMOD_TIMEUNIT_PCM); + + offset3 = offset3 - offset4; + transitionLength -= offset4; + } + + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->setDelay(clock_now + (unsigned long long)(delayLength), + clock_now + (unsigned long long)(delayLength) + transitionLength, true); + + { + unsigned long long delay = 0; + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->getDelay(&delay, nullptr); + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->removeFadePoints(0, (unsigned long long)(-1)); + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->addFadePoint(delay, 0.f); + unsigned long long fade_delay = (500.f / buffer_ms) * buffersize; + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->addFadePoint(delay + fade_delay, 1.f); + } + + result = ensembleSounds.exploreTransChannel[explorationTransition][i]->setPaused(false); + } + + result = ensembleSounds.combatChannel[i]->setPaused(true); + + unsigned int songPos = 0; + result = ensembleSounds.combatSound[i]->getSyncPoint(ensembleSounds.combatSongSeek * 8, &syncPoint); + result = ensembleSounds.combatSound[i]->getSyncPointInfo(syncPoint, nullptr, 0, &songPos, FMOD_TIMEUNIT_PCM); + result = ensembleSounds.combatChannel[i]->setPosition(songPos, FMOD_TIMEUNIT_PCM); + + //result = ensembleSounds.combatChannel[i]->setVolume(0.f); + //result = ensembleSounds.exploreTransChannel[explorationTransition][i]->setVolume(0.f); + + result = ensembleSounds.combatChannel[i]->removeFadePoints(0, (unsigned long long)(-1)); + result = ensembleSounds.combatChannel[i]->setDelay(clock_now + (unsigned long long)(delayLength + offset3), 0, false); + result = ensembleSounds.combatChannel[i]->setPaused(false); + } + } + } + } + + if ( currentSyncPoint == 0 /*currentSyncPoint % 8 == 0*/ ) + { + unsigned int currentOffset = 0; + FMOD_SYNCPOINT* syncPoint = nullptr; + result = sound->getSyncPoint(currentSyncPoint, &syncPoint); + result = sound->getSyncPointInfo(syncPoint, nullptr, 0, ¤tOffset, FMOD_TIMEUNIT_PCM); + + currentSyncPoint = ensembleSounds.exploreSongSeek; + + unsigned int offset = 0; + result = sound->getSyncPoint(currentSyncPoint, &syncPoint); + result = sound->getSyncPointInfo(syncPoint, nullptr, 0, &offset, FMOD_TIMEUNIT_PCM); + + unsigned int currentPos = 0; + result = chan->getPosition(¤tPos, FMOD_TIMEUNIT_PCM); + + // this goes crazy and calls back every missed sync point, twice for the same setpoint + if ( currentPos >= currentOffset // check our callback is within expected sync range + && (currentPos < currentOffset + ensembleSounds.exploreSoundSyncPointInterval) // check our callback is within expected sync range + && (abs((int)currentPos - (int)offset) > ((ensembleSounds.exploreSoundSyncPointInterval) * 0.9)) ) // check our position is sufficiently far and not a double up + { + //currentPos = currentPos % soundSyncPointInterval; // callback interval + currentPos = currentPos - currentOffset; + result = chan->setPosition(offset + currentPos, FMOD_TIMEUNIT_PCM); + } + } + } + + return FMOD_OK; +} + +ConsoleVariable cvar_ensemble_explore_seek("/ensemble_explore_seek", -1); +ConsoleVariable cvar_ensemble_combat_seek("/ensemble_combat_seek", -1); +void EnsembleSounds_t::updatePlayingChannelVolumes() +{ + bool fade_only_mode = (songTransitionMode == TRANSITION_MODE_FADE); + static ConsoleVariable cvar_ensemble_fade_in("/ensemble_fade_in", 0.02f); + static ConsoleVariable cvar_ensemble_fade_out("/ensemble_fade_out", 0.01f); + float fadeoutspeed = fade_only_mode ? 2 * *cvar_ensemble_fade_out : *cvar_ensemble_fade_out; + float fadeinspeed = *cvar_ensemble_fade_in; + + if ( songTransitionMode == TRANSITION_MODE_FULL ) + { + fadeoutspeed = 0.f; + fadeinspeed = 1.f; + } + + if ( exploreChannel[5] ) + { + bool playing = false; + float audibility = 0.f; + exploreChannel[5]->getAudibility(&audibility); + exploreChannel[5]->isPlaying(&playing); + + if ( playing && audibility > 0.01 ) + { + unsigned int pos = 0; + exploreChannel[5]->getPosition(&pos, FMOD_TIMEUNIT_PCM); + { + int lastPoint = -1; + int index = -1; + for ( auto& point : exploreSyncPoints ) + { + ++index; + if ( index == 0 ) + { + continue; + } + if ( point > pos ) + { + break; + } + lastPoint = index; + } + if ( lastPoint >= 0 ) + { + if ( *cvar_ensemble_explore_seek >= 0 ) + { + lastPoint = *cvar_ensemble_explore_seek; + } + if ( lastPoint >= (exploreSyncPointsToSeek.size() - 4) ) + { + exploreSongSeek = 0; // loop back to beginning + } + else + { + if ( lastPoint < exploreSyncPointsToSeek.size() ) + { + exploreSongSeek = exploreSyncPointsToSeek[lastPoint]; + } + else + { + exploreSongSeek = 0; // loop back to beginning, unknown error? + } + } + } + } + } + } + + if ( combatChannel[5] ) + { + bool playing = false; + float audibility = 0.f; + combatChannel[5]->getAudibility(&audibility); + combatChannel[5]->isPlaying(&playing); + bool paused = true; + combatChannel[5]->getPaused(&paused); + + if ( playing && !paused && audibility > 0.0001 ) + { + unsigned int pos = 0; + combatChannel[5]->getPosition(&pos, FMOD_TIMEUNIT_PCM); + { + int lastPoint = -1; + int index = -1; + for ( auto& point : combatSyncPoints ) + { + ++index; + if ( index % 8 != 0 ) + { + continue; + } + if ( point > pos ) + { + break; + } + lastPoint = index; + } + if ( lastPoint >= 0 ) + { + if ( *cvar_ensemble_combat_seek >= 0 ) + { + lastPoint = *cvar_ensemble_combat_seek; + } + combatSongSeek = lastPoint / 8; + } + } + } + if ( playing && !paused && audibility > 0.0001 ) + { + if ( lastUpdateTick != ticks ) + { + ++ticksCombatPlaying; + lastTickCombatPlaying = ticks; + } + } + else + { + ticksCombatPlaying = 0; + } + } + else + { + ticksCombatPlaying = 0; + } + + if ( lastUpdateTick != ticks ) + { + if ( combat && songTransitionState == TRANSITION_COMBAT_START ) + { + messagePlayer(clientnum, MESSAGE_DEBUG, "Combat start"); + } + else if ( !combat && songTransitionState == TRANSITION_COMBAT_ENDING ) + { + messagePlayer(clientnum, MESSAGE_DEBUG, "Combat end"); + } + if ( combatDelay > 0 ) + { + --combatDelay; + } + } + lastUpdateTick = ticks; + + // non-callback volume control if needed + for ( int i = 0; i < NUMENSEMBLEMUSIC && false; ++i ) + { + if ( songTransitionState == TRANSITION_EXPLORE || songTransitionState == TRANSITION_COMBAT_ENDED ) + { + bool playing = false; + float audibility = 0.f; + if ( exploreChannel[i] ) + { + exploreChannel[i]->getAudibility(&audibility); + exploreChannel[i]->isPlaying(&playing); + if ( playing ) + { + unsigned int ms = 0; + exploreChannel[i]->getPosition(&ms, FMOD_TIMEUNIT_MS); + if ( ms > 250 /*|| fade_only_mode*/ ) + { + float volume = 0.f; + exploreChannel[i]->getVolume(&volume); + volume = std::min(1.f, volume + fadeinspeed); + exploreChannel[i]->setVolume(volume); + } + } + else + { + exploreChannel[i]->setVolume(0.f); + } + } + + playing = false; + audibility = 0.f; + if ( combatChannel[i] ) + { + combatChannel[i]->getAudibility(&audibility); + combatChannel[i]->isPlaying(&playing); + if ( playing ) + { + float volume = 0.f; + combatChannel[i]->getVolume(&volume); + volume = std::max(0.f, volume - fadeoutspeed); + combatChannel[i]->setVolume(volume); + } + else + { + combatChannel[i]->setVolume(0.f); + } + } + + for ( int j = 0; j < 3; ++j ) + { + if ( !exploreTransChannel[j][i] ) { continue; } + playing = false; + audibility = 0.f; + exploreTransChannel[j][i]->getAudibility(&audibility); + exploreTransChannel[j][i]->isPlaying(&playing); + if ( playing ) + { + float volume = 0.f; + exploreTransChannel[j][i]->getVolume(&volume); + volume = std::max(0.f, volume - fadeoutspeed); + exploreTransChannel[j][i]->setVolume(volume); + } + else + { + exploreTransChannel[j][i]->setVolume(0.f); + } + } + + for ( int j = 0; j < 4; ++j ) + { + if ( !combatTransChannel[j][i] ) { continue; } + playing = false; + audibility = 0.f; + combatTransChannel[j][i]->getAudibility(&audibility); + combatTransChannel[j][i]->isPlaying(&playing); + if ( playing ) + { + float volume = 0.f; + combatTransChannel[j][i]->getVolume(&volume); + volume = std::min(1.f, volume + fadeinspeed); + combatTransChannel[j][i]->setVolume(volume); + } + else + { + combatTransChannel[j][i]->setVolume(0.f); + } + } + } + else if ( songTransitionState == TRANSITION_COMBAT ) + { + bool playing = false; + float audibility = 0.f; + if ( exploreChannel[i] ) + { + exploreChannel[i]->getAudibility(&audibility); + exploreChannel[i]->isPlaying(&playing); + if ( playing ) + { + float volume = 0.f; + exploreChannel[i]->getVolume(&volume); + volume = std::max(0.f, volume - fadeoutspeed); + exploreChannel[i]->setVolume(volume); + } + else + { + exploreChannel[i]->setVolume(0.f); + } + } + + playing = false; + audibility = 0.f; + if ( combatChannel[i] ) + { + combatChannel[i]->getAudibility(&audibility); + combatChannel[i]->isPlaying(&playing); + if ( playing ) + { + float volume = 0.f; + combatChannel[i]->getVolume(&volume); + volume = std::min(1.f, volume + fadeinspeed); + combatChannel[i]->setVolume(volume); + } + else + { + combatChannel[i]->setVolume(0.f); + } + } + + for ( int j = 0; j < 3; ++j ) + { + if ( !exploreTransChannel[j][i] ) { continue; } + playing = false; + audibility = 0.f; + exploreTransChannel[j][i]->getAudibility(&audibility); + exploreTransChannel[j][i]->isPlaying(&playing); + if ( playing ) + { + float volume = 0.f; + exploreTransChannel[j][i]->getVolume(&volume); + volume = std::min(1.f, volume + fadeinspeed); + exploreTransChannel[j][i]->setVolume(volume); + } + else + { + exploreTransChannel[j][i]->setVolume(0.f); + } + } + + for ( int j = 0; j < 4; ++j ) + { + if ( !combatTransChannel[j][i] ) { continue; } + playing = false; + audibility = 0.f; + combatTransChannel[j][i]->getAudibility(&audibility); + combatTransChannel[j][i]->isPlaying(&playing); + if ( playing ) + { + float volume = 0.f; + combatTransChannel[j][i]->getVolume(&volume); + volume = std::max(0.f, volume - fadeoutspeed); + combatTransChannel[j][i]->setVolume(volume); + } + else + { + combatTransChannel[j][i]->setVolume(0.f); + } + } + } + } +} +#endif \ No newline at end of file diff --git a/src/entity.cpp b/src/entity.cpp index 2a764de9a..f1a71a54c 100644 --- a/src/entity.cpp +++ b/src/entity.cpp @@ -46,481 +46,9 @@ Construct an Entity -------------------------------------------------------------------------------*/ ConsoleVariable cvar_entity_bodypart_sync_tick("/entity_bodypart_sync_tick", TICKS_PER_SECOND / 4); -Entity::Entity(Sint32 in_sprite, Uint32 pos, list_t* entlist, list_t* creaturelist) : - lightBonus(0.f), - chanceToPutOutFire(skill[37]), - circuit_status(skill[28]), - switch_power(skill[0]), - chestInit(skill[0]), - chestStatus(skill[1]), - chestHealth(skill[3]), - chestLocked(skill[4]), - chestOpener(skill[5]), - chestLidClicked(skill[6]), - chestAmbience(skill[7]), - chestMaxHealth(skill[8]), - chestType(skill[9]), - chestPreventLockpickCapstoneExploit(skill[10]), - chestHasVampireBook(skill[11]), - chestLockpickHealth(skill[12]), - chestOldHealth(skill[15]), - chestMimicChance(skill[16]), - char_gonnavomit(skill[26]), - char_heal(skill[22]), - char_energize(skill[23]), - char_drunk(skill[24]), - char_torchtime(skill[25]), - char_poison(skill[21]), - char_fire(skill[36]), - monsterState(skill[0]), - monsterTarget(skill[1]), - monsterTargetX(fskill[2]), - monsterTargetY(fskill[3]), - crystalInitialised(skill[1]), - crystalTurning(skill[3]), - crystalTurnStartDir(skill[4]), - crystalGeneratedElectricityNodes(skill[5]), - crystalNumElectricityNodes(skill[6]), - crystalHoverDirection(skill[7]), - crystalHoverWaitTimer(skill[8]), - crystalTurnReverse(skill[9]), - crystalSpellToActivate(skill[10]), - crystalStartZ(fskill[0]), - crystalMaxZVelocity(fskill[1]), - crystalMinZVelocity(fskill[2]), - crystalTurnVelocity(fskill[3]), - monsterAnimationLimbDirection(skill[20]), - monsterAnimationLimbOvershoot(skill[30]), - monsterSpecialTimer(skill[29]), - monsterSpecialState(skill[33]), - monsterSpellAnimation(skill[31]), - monsterFootstepType(skill[32]), - monsterLookTime(skill[4]), - monsterMoveTime(skill[6]), - monsterLookDir(fskill[4]), - monsterEntityRenderAsTelepath(skill[41]), - playerLevelEntrySpeech(skill[18]), - playerAliveTime(skill[12]), - playerVampireCurse(skill[51]), - playerAutomatonDeathCounter(skill[15]), - playerCreatedDeathCam(skill[16]), - monsterAttack(skill[8]), - monsterAttackTime(skill[9]), - monsterArmbended(skill[10]), - monsterWeaponYaw(fskill[5]), - monsterShadowInitialMimic(skill[34]), - monsterShadowDontChangeName(skill[35]), - monsterSlimeLastAttack(skill[34]), - monsterLichFireMeleeSeq(skill[34]), - monsterLichFireMeleePrev(skill[35]), - monsterLichIceCastSeq(skill[34]), - monsterLichIceCastPrev(skill[35]), - monsterLichMagicCastCount(skill[37]), - monsterLichMeleeSwingCount(skill[38]), - monsterLichBattleState(skill[27]), - monsterLichTeleportTimer(skill[40]), - monsterLichAllyStatus(skill[18]), - monsterLichAllyUID(skill[17]), - monsterPathBoundaryXStart(skill[14]), - monsterPathBoundaryYStart(skill[15]), - monsterPathBoundaryXEnd(skill[16]), - monsterPathBoundaryYEnd(skill[17]), - monsterStoreType(skill[18]), - monsterDevilNumSummons(skill[18]), - monsterStrafeDirection(skill[39]), - monsterPathCount(skill[38]), - monsterAllyIndex(skill[42]), - monsterAllyState(skill[43]), - monsterAllyPickupItems(skill[44]), - monsterAllyInteractTarget(skill[45]), - monsterAllyClass(skill[46]), - monsterDefend(skill[47]), - monsterAllySpecial(skill[48]), - monsterAllySpecialCooldown(skill[49]), - monsterAllySummonRank(skill[50]), - monsterKnockbackVelocity(fskill[9]), - monsterKnockbackUID(skill[51]), - creatureWebbedSlowCount(skill[52]), - monsterFearfulOfUid(skill[53]), - creatureShadowTaggedThisUid(skill[54]), - monsterIllusionTauntingThisUid(skill[55]), - monsterLastDistractedByNoisemaker(skill[55]), // shares with above as above only applies to inner demons. - monsterExtraReflexTick(skill[56]), - monsterSentrybotLookDir(fskill[10]), - monsterKnockbackTangentDir(fskill[11]), - playerStrafeVelocity(fskill[12]), - playerStrafeDir(fskill[13]), - monsterSpecialAttackUnequipSafeguard(fskill[14]), - particleDuration(skill[0]), - particleShrink(skill[1]), - monsterHitTime(skill[7]), - itemNotMoving(skill[18]), - itemNotMovingClient(skill[19]), - itemSokobanReward(skill[20]), - itemOriginalOwner(skill[21]), - itemStolen(skill[22]), - itemShowOnMap(skill[23]), - itemDelayMonsterPickingUp(skill[24]), - itemReceivedDetailsFromServer(skill[25]), - itemAutoSalvageByPlayer(skill[26]), - itemSplooshed(skill[27]), - itemContainer(skill[29]), - itemWaterBob(fskill[2]), - gateInit(skill[1]), - gateStatus(skill[3]), - gateRattle(skill[4]), - gateStartHeight(fskill[0]), - gateVelZ(vel_z), - gateInverted(skill[5]), - gateDisableOpening(skill[6]), - leverStatus(skill[1]), - leverTimerTicks(skill[3]), - boulderTrapRefireAmount(skill[1]), - boulderTrapRefireDelay(skill[3]), - boulderTrapAmbience(skill[6]), - boulderTrapFired(skill[0]), - boulderTrapRefireCounter(skill[4]), - boulderTrapPreDelay(skill[5]), - boulderTrapRocksToSpawn(skill[7]), - doorDir(skill[0]), - doorInit(skill[1]), - doorStatus(skill[3]), - doorHealth(skill[4]), - doorLocked(skill[5]), - doorSmacked(skill[6]), - doorTimer(skill[7]), - doorOldStatus(skill[8]), - doorMaxHealth(skill[9]), - doorStartAng(fskill[0]), - doorPreventLockpickExploit(skill[10]), - doorForceLockedUnlocked(skill[11]), - doorDisableLockpicks(skill[12]), - doorDisableOpening(skill[13]), - doorLockpickHealth(skill[14]), - doorOldHealth(skill[15]), - particleTimerDuration(skill[0]), - particleTimerEndAction(skill[1]), - particleTimerEndSprite(skill[3]), - particleTimerCountdownAction(skill[4]), - particleTimerCountdownSprite(skill[5]), - particleTimerTarget(skill[6]), - particleTimerPreDelay(skill[7]), - particleTimerVariable1(skill[8]), - particleTimerVariable2(skill[9]), - pedestalHasOrb(skill[0]), - pedestalOrbType(skill[1]), - pedestalInvertedPower(skill[3]), - pedestalInGround(skill[4]), - pedestalInit(skill[5]), - pedestalAmbience(skill[6]), - pedestalLockOrb(skill[7]), - pedestalPowerStatus(skill[8]), - orbInitialised(skill[1]), - orbHoverDirection(skill[7]), - orbHoverWaitTimer(skill[8]), - orbStartZ(fskill[0]), - orbMaxZVelocity(fskill[1]), - orbMinZVelocity(fskill[2]), - orbTurnVelocity(fskill[3]), - portalAmbience(skill[0]), - portalInit(skill[1]), - portalNotSecret(skill[3]), - portalVictoryType(skill[4]), - portalFireAnimation(skill[5]), - portalCustomLevelsToJump(skill[6]), - portalCustomRequiresPower(skill[7]), - portalCustomSprite(skill[8]), - portalCustomSpriteAnimationFrames(skill[9]), - portalCustomZOffset(skill[10]), - portalCustomLevelText1(skill[11]), - portalCustomLevelText2(skill[12]), - portalCustomLevelText3(skill[13]), - portalCustomLevelText4(skill[14]), - portalCustomLevelText5(skill[15]), - portalCustomLevelText6(skill[16]), - portalCustomLevelText7(skill[17]), - portalCustomLevelText8(skill[18]), - teleporterX(skill[0]), - teleporterY(skill[1]), - teleporterType(skill[3]), - teleporterAmbience(skill[4]), - spellTrapType(skill[0]), - spellTrapRefire(skill[1]), - spellTrapLatchPower(skill[3]), - spellTrapFloorTile(skill[4]), - spellTrapRefireRate(skill[5]), - spellTrapAmbience(skill[6]), - spellTrapInit(skill[7]), - spellTrapCounter(skill[8]), - spellTrapReset(skill[9]), - shrineSpellEffect(skill[0]), - shrineRefire1(skill[1]), - shrineRefire2(skill[3]), - shrineDir(skill[4]), - shrineAmbience(skill[5]), - shrineInit(skill[6]), - shrineActivateDelay(skill[7]), - shrineZ(skill[8]), - shrineDestXOffset(skill[9]), - shrineDestYOffset(skill[10]), - shrineDaedalusState(skill[11]), - ceilingTileModel(skill[0]), - ceilingTileDir(skill[1]), - ceilingTileAllowTrap(skill[3]), - ceilingTileBreakable(skill[4]), - floorDecorationModel(skill[0]), - floorDecorationRotation(skill[1]), - floorDecorationHeightOffset(skill[3]), - floorDecorationXOffset(skill[4]), - floorDecorationYOffset(skill[5]), - floorDecorationInteractText1(skill[8]), - floorDecorationInteractText2(skill[9]), - floorDecorationInteractText3(skill[10]), - floorDecorationInteractText4(skill[11]), - floorDecorationInteractText5(skill[12]), - floorDecorationInteractText6(skill[13]), - floorDecorationInteractText7(skill[14]), - floorDecorationInteractText8(skill[15]), - colliderDecorationModel(skill[0]), - colliderDecorationRotation(skill[1]), - colliderDecorationHeightOffset(skill[3]), - colliderDecorationXOffset(skill[4]), - colliderDecorationYOffset(skill[5]), - colliderHasCollision(skill[6]), - colliderSizeX(skill[7]), - colliderSizeY(skill[8]), - colliderMaxHP(skill[9]), - colliderDiggable(skill[10]), - colliderDamageTypes(skill[11]), - colliderCurrentHP(skill[12]), - colliderOldHP(skill[13]), - colliderInit(skill[14]), - colliderContainedEntity(skill[15]), - colliderHideMonster(skill[16]), - colliderKillerUid(skill[17]), - furnitureType(skill[0]), - furnitureInit(skill[1]), - furnitureDir(skill[3]), - furnitureHealth(skill[4]), - furnitureMaxHealth(skill[9]), - furnitureTableRandomItemChance(skill[10]), - furnitureTableSpawnChairs(skill[11]), - furnitureOldHealth(skill[15]), - pistonCamDir(skill[0]), - pistonCamTimer(skill[1]), - pistonCamRotateSpeed(fskill[0]), - arrowPower(skill[3]), - arrowPoisonTime(skill[4]), - arrowArmorPierce(skill[5]), - arrowSpeed(fskill[4]), - arrowFallSpeed(fskill[5]), - arrowBoltDropOffRange(skill[6]), - arrowShotByWeapon(skill[7]), - arrowQuiverType(skill[8]), - arrowShotByParent(skill[9]), - arrowDropOffEquipmentModifier(skill[14]), - actmagicIsVertical(skill[6]), - actmagicIsOrbiting(skill[7]), - actmagicOrbitDist(skill[8]), - actmagicOrbitVerticalDirection(skill[9]), - actmagicOrbitLifetime(skill[10]), - actmagicMirrorReflected(skill[24]), - actmagicMirrorReflectedCaster(skill[12]), - actmagicCastByMagicstaff(skill[13]), - actmagicOrbitVerticalSpeed(fskill[2]), - actmagicOrbitStartZ(fskill[3]), - actmagicOrbitStationaryX(fskill[4]), - actmagicOrbitStationaryY(fskill[5]), - actmagicOrbitStationaryCurrentDist(fskill[6]), - actmagicSprayGravity(fskill[7]), - actmagicOrbitStationaryHitTarget(skill[14]), - actmagicOrbitHitTargetUID1(skill[15]), - actmagicOrbitHitTargetUID2(skill[16]), - actmagicOrbitHitTargetUID3(skill[17]), - actmagicOrbitHitTargetUID4(skill[18]), - actmagicProjectileArc(skill[19]), - actmagicOrbitCastFromSpell(skill[20]), - actmagicSpellbookBonus(skill[21]), - actmagicCastByTinkerTrap(skill[22]), - actmagicTinkerTrapFriendlyFire(skill[23]), - actmagicReflectionCount(skill[25]), - actmagicFromSpellbook(skill[26]), - actmagicSpray(skill[27]), - actmagicEmitter(skill[29]), - goldAmount(skill[0]), - goldAmbience(skill[1]), - goldSokoban(skill[2]), - goldBouncing(skill[3]), - goldInContainer(skill[4]), - interactedByMonster(skill[47]), - highlightForUI(fskill[29]), - highlightForUIGlow(fskill[28]), - grayscaleGLRender(fskill[27]), - noColorChangeAllyLimb(fskill[26]), - soundSourceFired(skill[0]), - soundSourceToPlay(skill[1]), - soundSourceVolume(skill[2]), - soundSourceLatchOn(skill[3]), - soundSourceDelay(skill[4]), - soundSourceDelayCounter(skill[5]), - soundSourceOrigin(skill[6]), - lightSourceBrightness(skill[0]), - lightSourceAlwaysOn(skill[1]), - lightSourceInvertPower(skill[2]), - lightSourceLatchOn(skill[3]), - lightSourceRadius(skill[4]), - lightSourceFlicker(skill[5]), - lightSourceDelay(skill[6]), - lightSourceDelayCounter(skill[7]), - textSourceColorRGB(skill[0]), - textSourceVariables4W(skill[1]), - textSourceDelay(skill[2]), - textSourceIsScript(skill[3]), - textSourceBegin(skill[4]), - signalActivateDelay(skill[1]), - signalTimerInterval(skill[2]), - signalTimerRepeatCount(skill[3]), - signalTimerLatchInput(skill[4]), - signalInputDirection(skill[5]), - signalGateANDPowerCount(skill[9]), - signalInvertOutput(skill[10]), - effectPolymorph(skill[50]), - effectShapeshift(skill[53]), - entityShowOnMap(skill[59]), - thrownProjectilePower(skill[19]), - thrownProjectileCharge(skill[20]), - playerStartDir(skill[1]), - pressurePlateTriggerType(skill[3]), - worldTooltipAlpha(fskill[0]), - worldTooltipZ(fskill[1]), - worldTooltipActive(skill[0]), - worldTooltipPlayer(skill[1]), - worldTooltipInit(skill[3]), - worldTooltipFadeDelay(skill[4]), - worldTooltipIgnoreDrawing(skill[5]), - worldTooltipRequiresButtonHeld(skill[6]), - statueInit(skill[0]), - statueDir(skill[1]), - statueId(skill[3]) -{ - int c; - // add the entity to the entity list - if ( !pos ) - { - mynode = list_AddNodeFirst(entlist); - } - else - { - mynode = list_AddNodeLast(entlist); - } - - if ( mynode ) - { - mynode->element = this; - mynode->deconstructor = &entityDeconstructor; - mynode->size = sizeof(Entity); - } - - myCreatureListNode = nullptr; - if ( creaturelist ) - { - addToCreatureList(creaturelist); - } - myWorldUIListNode = nullptr; - myTileListNode = nullptr; - - // now reset all of my data elements - lastupdate = 0; - lastupdateserver = 0; - ticks = 0; - x = 0; - y = 0; - z = 0; - new_x = 0; - new_y = 0; - new_z = 0; - focalx = 0; - focaly = 0; - focalz = 0; - scalex = 1; - scaley = 1; - scalez = 1; - vel_x = 0; - vel_y = 0; - vel_z = 0; - sizex = 0; - sizey = 0; - yaw = 0; - pitch = 0; - roll = 0; - new_yaw = 0; - new_pitch = 0; - new_roll = 0; - lerpCurrentState.resetMovement(); - lerpCurrentState.resetPosition(); - lerpPreviousState.resetMovement(); - lerpPreviousState.resetPosition(); - lerpRenderState.resetMovement(); - lerpRenderState.resetPosition(); - bNeedsRenderPositionInit = true; - bUseRenderInterpolation = false; - mapGenerationRoomX = 0; - mapGenerationRoomY = 0; - lerp_ox = 0.0; - lerp_oy = 0.0; - sprite = in_sprite; - light = nullptr; - string = nullptr; - children.first = nullptr; - children.last = nullptr; - //this->magic_effects = (list_t *) malloc(sizeof(list_t)); - //this->magic_effects->first = NULL; this->magic_effects->last = NULL; - for ( c = 0; c < NUMENTITYSKILLS; ++c ) - { - skill[c] = 0; - } - for ( c = 0; c < NUMENTITYFSKILLS; ++c ) - { - fskill[c] = 0; - } - skill[2] = -1; - for ( c = 0; c < 24; c++ ) - { - flags[c] = false; - } - if ( entlist != nullptr && entlist == map.entities ) - { - if ( multiplayer != CLIENT || loading ) - { - uid = entity_uids; - entity_uids++; - map.entities_map.insert({ uid, mynode }); - } - else - { - uid = -2; - } - } - else - { - uid = -2; - } - behavior = nullptr; - ranbehavior = false; - parent = 0; - path = nullptr; - monsterAllyIndex = -1; // set to -1 to not reference player indices 0-3. - /*if ( checkSpriteType(this->sprite) > 1 ) - { - setSpriteAttributes(this, nullptr, nullptr); - }*/ - - clientStats = nullptr; - clientsHaveItsStats = false; -} - void Entity::setUID(Uint32 new_uid) { + if ( !mynode ) { return; } if ( mynode->list == map.entities ) { map.entities_map.erase(uid); @@ -537,6 +65,70 @@ Deconstruct an Entity -------------------------------------------------------------------------------*/ +struct ClassBaseGrowths +{ + struct ClassHPMPValues + { + int baseHP = 3; + int baseMP = 3; + int baseRegenHP = 3; + int baseRegenMP = 3; + }; + static const std::vector statRegenHP; + static const std::vector statRegenMP; + static const std::vector hpStatGrowths; + static const std::vector mpStatGrowths; + static const real_t hpRegenFactor; + static const real_t mpRegenFactor; + static const std::vector classBaseGrowths; + static const ClassHPMPValues& getClassBaseGrowths(int classnum) + { + if ( classnum >= 0 && (classnum + 1) < classBaseGrowths.size() ) + { + return classBaseGrowths.at(classnum + 1); + } + return classBaseGrowths.at(0); + } +}; +// STR DEX CON INT PER CHR +const std::vector ClassBaseGrowths::statRegenHP = { 0.05, 0.1, 0.0, 0.0, 0.0, 0.0 }; +const std::vector ClassBaseGrowths::statRegenMP = { 0.0, 0.0, 0.0, 0.00, .05, 0.1 }; +const std::vector ClassBaseGrowths::hpStatGrowths = { 1, 2, 2, 0, 0, 0 }; +const std::vector ClassBaseGrowths::mpStatGrowths = { 0, 0, 0, 2, 2, 1 }; +const real_t ClassBaseGrowths::hpRegenFactor = 0.05; +const real_t ClassBaseGrowths::mpRegenFactor = 0.1; + +const std::vector ClassBaseGrowths::classBaseGrowths = { +// HP MP HP RGN MP RGN + {3, 3, 3, 3}, // default + {4, 2, 1, 1}, //CLASS_BARBARIAN, + {4, 2, 1, 1}, //CLASS_WARRIOR, + {3, 5, 1, 4}, //CLASS_HEALER, + {2, 3, 5, 3}, //CLASS_ROGUE, + {4, 3, 4, 3}, //CLASS_WANDERER, + {3, 4, 3, 4}, //CLASS_CLERIC, + {3, 3, 3, 3}, //CLASS_MERCHANT, + {2, 5, 2, 5}, //CLASS_WIZARD, + {2, 4, 3, 4}, //CLASS_ARCANIST, + {3, 3, 3, 3}, //CLASS_JOKER, + {3, 4, 2, 4}, //CLASS_SEXTON, + {2, 1, 1, 1}, //CLASS_NINJA, + {4, 2, 2, 2}, //CLASS_MONK, + {4, 3, 2, 3}, //CLASS_CONJURER, + {2, 4, 1, 4}, //CLASS_ACCURSED, + {2, 4, 2, 5}, //CLASS_MESMER, + {3, 2, 5, 3}, //CLASS_BREWER, + {3, 1, 4, 2}, //CLASS_MACHINIST, + {4, 3, 4, 3}, //CLASS_PUNISHER, + {3, 3, 3, 3}, //CLASS_SHAMAN, + {2, 2, 5, 2}, //CLASS_HUNTER, + {2, 4, 4, 4}, //CLASS_BARD, + {3, 2, 3, 2}, //CLASS_SAPPER, + {2, 4, 3, 4}, //CLASS_SCION, + {3, 4, 3, 3}, //CLASS_HERMIT, + {3, 3, 3, 3} //CLASS_PALADIN +}; + Entity::~Entity() { node_t* node; @@ -572,6 +164,11 @@ Entity::~Entity() list_RemoveNode(myTileListNode); myTileListNode = nullptr; } + if ( string ) + { + free(string); + string = nullptr; + } #ifdef USE_FMOD if ( entity_sound ) @@ -593,27 +190,12 @@ Entity::~Entity() continue; } - // create a reminder for the server to continue informing the client of the deletion - /*deleteent = (deleteent_t *) malloc(sizeof(deleteent_t)); //TODO: C++-PORT: Replace with new + class. - deleteent->uid = uid; - deleteent->tries = 0; - node = list_AddNodeLast(&entitiesToDelete[i]); - node->element = deleteent; - node->deconstructor = &defaultDeconstructor;*/ - // send the delete entity command to the client strcpy((char*)net_packet->data, "ENTD"); SDLNet_Write32((Uint32)uid, &net_packet->data[4]); net_packet->address.host = net_clients[i - 1].host; net_packet->address.port = net_clients[i - 1].port; net_packet->len = 8; - /*if ( directConnect ) { - SDLNet_UDP_Send(net_sock,-1,net_packet); - } else { - #ifdef STEAMWORKS - SteamNetworking()->SendP2PPacket(*static_cast(steamIDRemote[i - 1]), net_packet->data, net_packet->len, k_EP2PSendUnreliable, 0); - #endif - }*/ sendPacketSafe(net_sock, -1, net_packet, i - 1); } } @@ -679,7 +261,7 @@ Sets the obituary to that of a mon -------------------------------------------------------------------------------*/ -void Entity::killedByMonsterObituary(Entity* victim) +void Entity::killedByMonsterObituary(Entity* victim, bool fromSpell) { if ( !victim ) { @@ -719,6 +301,18 @@ void Entity::killedByMonsterObituary(Entity* victim) } return; } + if ( behavior == &actColliderDecoration ) + { + auto find = EditorEntityData_t::colliderData.find(colliderDamageTypes); + if ( find != EditorEntityData_t::colliderData.end() ) + { + if ( find->second.name.find("mushroom") != std::string::npos ) + { + hitstats->killer = KilledBy::MUSHROOM; + victim->setObituary(Language::get(6753)); + } + } + } Stat* myStats = this->getStats(); if ( !myStats ) @@ -726,6 +320,20 @@ void Entity::killedByMonsterObituary(Entity* victim) return; } + if ( this == victim && fromSpell ) + { + if ( hitstats->sex == MALE ) + { + victim->setObituary(Language::get(1528)); + } + else + { + victim->setObituary(Language::get(1529)); + } + hitstats->killer = KilledBy::FAILED_INVOCATION; + return; + } + if ( myStats->type == hitstats->type ) { hitstats->killer = KilledBy::ALLY_BETRAYAL; @@ -873,6 +481,36 @@ void Entity::killedByMonsterObituary(Entity* victim) case BUGBEAR: victim->setObituary(Language::get(6255)); break; + case REVENANT_SKULL: + victim->setObituary(Language::get(6669)); + break; + case MINIMIMIC: + victim->setObituary(Language::get(6670)); + break; + case MONSTER_ADORCISED_WEAPON: + victim->setObituary(Language::get(6672)); + break; + case FLAME_ELEMENTAL: + victim->setObituary(Language::get(6671)); + break; + case MOTH_SMALL: + victim->setObituary(Language::get(6698)); + break; + case EARTH_ELEMENTAL: + victim->setObituary(Language::get(6737)); + break; + case DRYAD: + victim->setObituary(Language::get(6738)); + break; + case GREMLIN: + victim->setObituary(Language::get(6739)); + break; + case MYCONID: + victim->setObituary(Language::get(6740)); + break; + case SALAMANDER: + victim->setObituary(Language::get(6741)); + break; default: victim->setObituary(Language::get(1500)); break; @@ -900,7 +538,21 @@ int Entity::entityLight() int light_y = (int)this->y / 16; const auto& light = lightmaps[0][light_y + light_x * map.height]; //return (light.x + light.y + light.z) / 3.f; - return std::min(std::max(0, (int)((light.x + light.y + light.z) / 3.f)), 255); + float level = (light.x + light.y + light.z) / 3.f; + if ( !strncmp(map.filename, "fortress", 8) ) + { + // reduce the ambient light a bit + level = (std::max(0.f, light.x - 32.f) + + std::max(0.f, light.y - 32.f) + + std::max(0.f, light.z - 40.f)) / 3.f; + } + if ( light.w > 0.f ) + { + float shade = std::min(std::max(0.f, light.w), 255.f) / 255.f; + level -= (level * shade * 0.8); + return std::min(std::max(0, (int)(level)), 255); + } + return std::min(std::max(0, (int)(level)), 255); //return std::min(std::max(0, (int)((light.x + light.y + light.z) / 3.f * 255.f)), 255); } @@ -934,25 +586,42 @@ int Entity::entityLightAfterReductions(Stat& myStats, Entity* observer) } } + real_t sneakEffectiveness = 1.0; + if ( !strncmp(map.filename, "fortress", 8) ) + { + sneakEffectiveness = 0.25; + } + if ( observer ) { - light += observer->getPER() * 4; // add light level for PER x 4 + if ( observer->behavior == &actColliderDecoration && observer->colliderSpellEvent > 0 ) + { + light += 8 * 4; + } + else + { + light += observer->getPER() * 4; // add light level for PER x 4 + } if ( sneaking ) { - light /= 2; // halve for sneaking + light *= (0.5 + (1.0 - sneakEffectiveness) * 0.3); // halve for sneaking, sneak effectiveness at 0.25 is 72.5% } - light -= (std::max(0, light - TOUCHRANGE)) * (1.0 * (myStats.getModifiedProficiency(PRO_STEALTH) / 100.0)); // reduce to 32 as sneak approaches 100 + light -= (std::max(0, light - TOUCHRANGE)) * (sneakEffectiveness * (myStats.getModifiedProficiency(PRO_STEALTH) / 100.0)); // reduce to 32 as sneak approaches 100 Stat* observerStats = observer->getStats(); - if ( observerStats && observerStats->EFFECTS[EFF_BLIND] ) + if ( observerStats && observerStats->getEffectActive(EFF_BLIND) ) { light = TOUCHRANGE; } + if ( observerStats && observerStats->getEffectActive(EFF_SEEK_CREATURE) ) + { + light = 16; + } if ( observer->behavior == &actMonster && observer->monsterLastDistractedByNoisemaker > 0 && uidToEntity(observer->monsterLastDistractedByNoisemaker) ) { if ( observer->monsterTarget == observer->monsterLastDistractedByNoisemaker - || myStats.EFFECTS[EFF_DISORIENTED] ) + || myStats.getEffectActive(EFF_DISORIENTED) ) { // currently hunting noisemaker. light = 16; @@ -963,9 +632,9 @@ int Entity::entityLightAfterReductions(Stat& myStats, Entity* observer) { if ( sneaking ) { - light /= 2; // halve for sneaking + light *= (0.5 + (1.0 - sneakEffectiveness) * 0.5); // halve for sneaking } - light -= (std::max(0, light - TOUCHRANGE)) * (1.0 * (myStats.getModifiedProficiency(PRO_STEALTH) / 100.0)); // reduce to 32 as sneak approaches 100 + light -= (std::max(0, light - TOUCHRANGE)) * (sneakEffectiveness * (myStats.getModifiedProficiency(PRO_STEALTH) / 100.0)); // reduce to 32 as sneak approaches 100 } } @@ -974,11 +643,41 @@ int Entity::entityLightAfterReductions(Stat& myStats, Entity* observer) light = std::min(light, TOUCHRANGE); } + if ( player >= 0 && players[player]->mechanics.ensemblePlaying >= 0 ) + { + light = std::max(16 * 5, light + 3 * 16); + } + + if ( myStats.getEffectActive(EFF_DUSTED) ) + { + int increment = 16 * 3; + if ( observer && observer->behavior == &actMonster ) + { + if ( Stat* observerStats = observer->getStats() ) + { + if ( observerStats->type == MYCONID ) + { + light = std::max(light, 5 * 16); + } + } + } + light = std::max(increment, light + increment); + } + if ( Uint8 effectStrength = myStats.getEffectActive(EFF_NOISE_VISIBILITY) ) + { + int increment = 16 * effectStrength; + light = std::max(increment, light + increment); + } + light = std::max(light, 0); - if ( myStats.type == DUMMYBOT ) + if ( myStats.type == DUMMYBOT || myStats.type == HOLOGRAM ) { light = std::max(light, 256); // dummybots can always be seen at least 16 tiles away. } + if ( observer && observer->behavior == &actColliderDecoration && observer->colliderSpellEvent > 0 ) + { + light = std::max(light, TOUCHRANGE * 2); + } return light; } @@ -990,6 +689,74 @@ Counts down effect timers and toggles effects whose timers reach zero - server o -------------------------------------------------------------------------------*/ +void sustainedSpellProcess(Entity& entity, Stat& myStats, int effectID, std::map& sustainedSpell_hijacked, + bool& bOutDissipated, spell_t*& outUnsustainSpell) +{ + if ( sustainedSpell_hijacked.find(effectID) != sustainedSpell_hijacked.end() ) + { + bool sustained = false; + Entity* caster = uidToEntity(sustainedSpell_hijacked[effectID]->caster); + if ( caster == &entity ) + { + //Deduct mana from caster. Cancel spell if not enough mana (simply leave sustained at false). + int oldMP = caster->getMP(); + int sustainCost = getSustainCostOfSpell(sustainedSpell_hijacked[effectID], &entity); + if ( effectID == EFF_FLAME_CLOAK && entity.flags[BURNING] ) + { + sustainCost *= 2; + } + else if ( effectID == EFF_GUARD_BODY || effectID == EFF_GUARD_SPIRIT || effectID == EFF_DIVINE_GUARD ) + { + int spellID = SPELL_GUARD_BODY; + if ( effectID == EFF_GUARD_SPIRIT ) + { + spellID = SPELL_GUARD_SPIRIT; + } + else if ( effectID == EFF_DIVINE_GUARD ) + { + spellID = SPELL_DIVINE_GUARD; + } + + int currentStrength = sustainedSpell_hijacked[effectID]->channel_effectStrength; + int effectStrengthMax = getSpellDamageSecondaryFromID(spellID, caster, nullptr, caster); + effectStrengthMax = std::min(effectStrengthMax, getSpellEffectDurationSecondaryFromID(spellID, caster, nullptr, caster)); + + int increment = std::max(1, getSpellDamageFromID(spellID, caster, nullptr, caster)); + + sustainedSpell_hijacked[effectID]->channel_effectStrength += increment; + sustainedSpell_hijacked[effectID]->channel_effectStrength = std::min(effectStrengthMax, sustainedSpell_hijacked[effectID]->channel_effectStrength); + if ( sustainedSpell_hijacked[effectID]->channel_effectStrength > currentStrength ) + { + sustainCost *= sustainedSpell_hijacked[effectID]->channel_effectStrength - currentStrength; + } + } + bool deducted = caster->safeConsumeMP(sustainCost); //Consume X mana ever duration / mana seconds + if ( deducted ) + { + sustained = true; + myStats.setEffectActive(effectID, sustainedSpell_hijacked[effectID]->channel_effectStrength); + myStats.EFFECTS_TIMERS[effectID] = sustainedSpell_hijacked[effectID]->channel_duration; + + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP(), sustainedSpell_hijacked[effectID]->skillID); + } + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_STATUS, Language::get(6504), sustainedSpell_hijacked[effectID]->getSpellName()); + //outUnsustainSpell = sustainedSpell_hijacked[effectID]; + list_RemoveNode(sustainedSpell_hijacked[effectID]->magic_effects_node); //Remove it from the entity's magic effects. This has the side effect of removing it from the sustained spells list too. + sustainedSpell_hijacked.erase(effectID); + } + } + if ( sustained ) + { + bOutDissipated = false; //Sustained the spell, so do not stop being invisible. + } + } +} + void Entity::effectTimes() { Stat* myStats = this->getStats(); @@ -1011,12 +778,12 @@ void Entity::effectTimes() player = -1; } - spell_t* invisibility_hijacked = nullptr; //If NULL, function proceeds as normal. If points to something, it ignores the invisibility timer since a spell is doing things. //TODO: Incorporate the spell into isInvisible() instead? spell_t* levitation_hijacked = nullptr; //If NULL, function proceeds as normal. If points to something, it ignore the levitation timer since a spell is doing things. spell_t* reflectMagic_hijacked = nullptr; spell_t* amplifyMagic_hijacked = nullptr; spell_t* vampiricAura_hijacked = nullptr; + std::map sustainedSpell_hijacked; //Handle magic effects (like invisibility) for ( node = myStats->magic_effects.first; node; node = node->next, ++count ) { @@ -1045,6 +812,16 @@ void Entity::effectTimes() net_packet->len = 9; sendPacketSafe(net_sock, -1, net_packet, player - 1); } + if ( spell->sustainEffectDissipate >= 0 ) + { + if ( myStats->getEffectActive(spell->sustainEffectDissipate) ) + { + if ( myStats->EFFECTS_TIMERS[spell->sustainEffectDissipate] > 0 ) + { + myStats->EFFECTS_TIMERS[spell->sustainEffectDissipate] = 1; + } + } + } list_RemoveNode(node); //Bugger the spell. node = temp; if ( !node ) @@ -1055,17 +832,18 @@ void Entity::effectTimes() } bool unsustain = false; - switch ( spell->ID ) + int spellID = spell->ID; + switch ( spellID ) { - case SPELL_INVISIBILITY: - invisibility_hijacked = spell; - if ( !myStats->EFFECTS[EFF_INVISIBLE] ) + case SPELL_BLOOD_WARD: + sustainedSpell_hijacked[EFF_BLOOD_WARD] = spell; + if ( !myStats->getEffectActive(EFF_BLOOD_WARD) ) { for ( int c = 0; c < MAXPLAYERS; ++c ) { if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) { - messagePlayer(c, MESSAGE_COMBAT, Language::get(591)); //If cure ailments or somesuch bombs the status effects. + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. } } node_t* temp = nullptr; @@ -1082,15 +860,15 @@ void Entity::effectTimes() node = temp; } break; - case SPELL_LEVITATION: - levitation_hijacked = spell; - if ( !myStats->EFFECTS[EFF_LEVITATING] ) + case SPELL_TRUE_BLOOD: + sustainedSpell_hijacked[EFF_TRUE_BLOOD] = spell; + if ( !myStats->getEffectActive(EFF_TRUE_BLOOD) ) { for ( int c = 0; c < MAXPLAYERS; ++c ) { if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) { - messagePlayer(c, MESSAGE_COMBAT, Language::get(592)); + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. } } node_t* temp = nullptr; @@ -1107,15 +885,15 @@ void Entity::effectTimes() node = temp; } break; - case SPELL_REFLECT_MAGIC: - reflectMagic_hijacked = spell; - if ( !myStats->EFFECTS[EFF_MAGICREFLECT] ) + case SPELL_DIVINE_ZEAL: + sustainedSpell_hijacked[EFF_DIVINE_ZEAL] = spell; + if ( !myStats->getEffectActive(EFF_DIVINE_ZEAL) ) { for ( int c = 0; c < MAXPLAYERS; ++c ) { if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) { - messagePlayer(c, MESSAGE_COMBAT, Language::get(2446)); + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. } } node_t* temp = nullptr; @@ -1132,15 +910,15 @@ void Entity::effectTimes() node = temp; } break; - case SPELL_AMPLIFY_MAGIC: - amplifyMagic_hijacked = spell; - if ( !myStats->EFFECTS[EFF_MAGICAMPLIFY] ) + case SPELL_FLAME_CLOAK: + sustainedSpell_hijacked[EFF_FLAME_CLOAK] = spell; + if ( !myStats->getEffectActive(EFF_FLAME_CLOAK) ) { for ( int c = 0; c < MAXPLAYERS; ++c ) { if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) { - messagePlayer(c, MESSAGE_COMBAT, Language::get(3441)); + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. } } node_t* temp = nullptr; @@ -1157,15 +935,15 @@ void Entity::effectTimes() node = temp; } break; - case SPELL_VAMPIRIC_AURA: - vampiricAura_hijacked = spell; - if ( !myStats->EFFECTS[EFF_VAMPIRICAURA] ) + case SPELL_GUARD_BODY: + sustainedSpell_hijacked[EFF_GUARD_BODY] = spell; + if ( !myStats->getEffectActive(EFF_GUARD_BODY) ) { for ( int c = 0; c < MAXPLAYERS; ++c ) { if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) { - messagePlayer(c, MESSAGE_COMBAT, Language::get(2447)); + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. } } node_t* temp = nullptr; @@ -1182,78 +960,678 @@ void Entity::effectTimes() node = temp; } break; - default: - //Unknown spell, undefined effect. Like, say, a fireball spell wound up in here for some reason. That's a nono. - printlog("[entityEffectTimes] Warning: magic_effects spell that's not relevant. Should not be in the magic_effects list!\n"); - list_RemoveNode(node); - } - - if ( unsustain ) - { - // the node has been removed, tell the client to unsustain in their list. - if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) - { - strcpy((char*)net_packet->data, "UNCH"); - net_packet->data[4] = player; - SDLNet_Write32(spell->ID, &net_packet->data[5]); - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 9; - sendPacketSafe(net_sock, -1, net_packet, player - 1); - } - } - - if ( !node ) - { - break; //BREAK OUT. YEAAAAAH. Because otherwise it crashes. - } - } - if ( count ) - { - //printlog( "Number of magic effects spells: %d\n", count); //Debugging output. - } - - bool dissipate = true; - bool updateClient = false; - spell_t* unsustainSpell = nullptr; - - for ( int c = 0; c < NUMEFFECTS; c++ ) - { - if ( myStats->EFFECTS_TIMERS[c] > 0 ) - { - myStats->EFFECTS_TIMERS[c]--; - if ( c == EFF_POLYMORPH ) - { - if ( myStats->EFFECTS_TIMERS[c] == TICKS_PER_SECOND * 15 ) + case SPELL_GUARD_SPIRIT: + sustainedSpell_hijacked[EFF_GUARD_SPIRIT] = spell; + if ( !myStats->getEffectActive(EFF_GUARD_SPIRIT) ) { - playSoundPlayer(player, 611, 192); - messagePlayer(player, MESSAGE_STATUS, Language::get(3193)); + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; } - } - else if ( c == EFF_SHAPESHIFT ) - { - if ( myStats->EFFECTS_TIMERS[c] == TICKS_PER_SECOND * 15 ) + break; + case SPELL_DIVINE_GUARD: + sustainedSpell_hijacked[EFF_DIVINE_GUARD] = spell; + if ( !myStats->getEffectActive(EFF_DIVINE_GUARD) ) { - playSoundPlayer(player, 611, 192); - messagePlayer(player, MESSAGE_STATUS, Language::get(3475)); + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; } - } - if ( myStats->EFFECTS_TIMERS[c] == 0 ) - { - myStats->EFFECTS[c] = false; - switch ( c ) + break; + case SPELL_ENVENOM_WEAPON: + sustainedSpell_hijacked[EFF_ENVENOM_WEAPON] = spell; + if ( !myStats->getEffectActive(EFF_ENVENOM_WEAPON) ) { - case EFF_ASLEEP: - messagePlayer(player, MESSAGE_STATUS, Language::get(593)); - if ( monsterAllyGetPlayerLeader() && monsterAllySpecial == ALLY_SPECIAL_CMD_REST ) + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) { - monsterAllySpecial = ALLY_SPECIAL_CMD_NONE; - myStats->EFFECTS[EFF_HP_REGEN] = false; - myStats->EFFECTS_TIMERS[EFF_HP_REGEN] = 0; + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. } - break; - case EFF_MIMIC_LOCKED: - if ( myStats->type == MIMIC ) + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_THORNS: + sustainedSpell_hijacked[EFF_THORNS] = spell; + if ( !myStats->getEffectActive(EFF_THORNS) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_BLADEVINES: + sustainedSpell_hijacked[EFF_BLADEVINES] = spell; + if ( !myStats->getEffectActive(EFF_BLADEVINES) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_BASTION_MUSHROOM: + sustainedSpell_hijacked[EFF_BASTION_MUSHROOM] = spell; + if ( !myStats->getEffectActive(EFF_BASTION_MUSHROOM) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_BASTION_ROOTS: + sustainedSpell_hijacked[EFF_BASTION_ROOTS] = spell; + if ( !myStats->getEffectActive(EFF_BASTION_ROOTS) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_SPORES: + sustainedSpell_hijacked[EFF_SPORES] = spell; + if ( !myStats->getEffectActive(EFF_SPORES) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_ABUNDANCE: + sustainedSpell_hijacked[EFF_ABUNDANCE] = spell; + if ( !myStats->getEffectActive(EFF_ABUNDANCE) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_GREATER_ABUNDANCE: + sustainedSpell_hijacked[EFF_GREATER_ABUNDANCE] = spell; + if ( !myStats->getEffectActive(EFF_GREATER_ABUNDANCE) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_PRESERVE: + sustainedSpell_hijacked[EFF_PRESERVE] = spell; + if ( !myStats->getEffectActive(EFF_PRESERVE) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_MIST_FORM: + sustainedSpell_hijacked[EFF_MIST_FORM] = spell; + if ( !myStats->getEffectActive(EFF_MIST_FORM) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_LIGHTEN_LOAD: + sustainedSpell_hijacked[EFF_LIGHTEN_LOAD] = spell; + if ( !myStats->getEffectActive(EFF_LIGHTEN_LOAD) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_ATTRACT_ITEMS: + sustainedSpell_hijacked[EFF_ATTRACT_ITEMS] = spell; + if ( !myStats->getEffectActive(EFF_ATTRACT_ITEMS) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_RETURN_ITEMS: + sustainedSpell_hijacked[EFF_RETURN_ITEM] = spell; + if ( !myStats->getEffectActive(EFF_RETURN_ITEM) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_OVERCHARGE: + sustainedSpell_hijacked[EFF_OVERCHARGE] = spell; + if ( !myStats->getEffectActive(EFF_OVERCHARGE) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(6503), spell->getSpellName()); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_INVISIBILITY: + invisibility_hijacked = spell; + if ( !myStats->getEffectActive(EFF_INVISIBLE) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(591)); //If cure ailments or somesuch bombs the status effects. + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_LEVITATION: + levitation_hijacked = spell; + if ( !myStats->getEffectActive(EFF_LEVITATING) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(592)); + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_REFLECT_MAGIC: + reflectMagic_hijacked = spell; + if ( !myStats->getEffectActive(EFF_MAGICREFLECT) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(2446)); + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_AMPLIFY_MAGIC: + amplifyMagic_hijacked = spell; + if ( !myStats->getEffectActive(EFF_MAGICAMPLIFY) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(3441)); + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + case SPELL_VAMPIRIC_AURA: + vampiricAura_hijacked = spell; + if ( !myStats->getEffectActive(EFF_VAMPIRICAURA) ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c] && players[c]->entity && players[c]->entity == uidToEntity(spell->caster) ) + { + messagePlayer(c, MESSAGE_COMBAT, Language::get(2447)); + } + } + node_t* temp = nullptr; + if ( node->prev ) + { + temp = node->prev; + } + else if ( node->next ) + { + temp = node->next; + } + unsustain = true; + list_RemoveNode(node); //Remove this here node. + node = temp; + } + break; + default: + //Unknown spell, undefined effect. Like, say, a fireball spell wound up in here for some reason. That's a nono. + printlog("[entityEffectTimes] Warning: magic_effects spell that's not relevant. Should not be in the magic_effects list!\n"); + list_RemoveNode(node); + } + + if ( unsustain ) + { + // the node has been removed, tell the client to unsustain in their list. + if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "UNCH"); + net_packet->data[4] = player; + SDLNet_Write32(spellID, &net_packet->data[5]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + } + + if ( !node ) + { + break; //BREAK OUT. YEAAAAAH. Because otherwise it crashes. + } + } + if ( count ) + { + //printlog( "Number of magic effects spells: %d\n", count); //Debugging output. + } + + bool dissipate = true; + bool updateClient = false; + spell_t* unsustainSpell = nullptr; + bool needServerUpdateEffects = false; + + for ( int c = 0; c < NUMEFFECTS; c++ ) + { + if ( myStats->EFFECTS_TIMERS[c] > 0 ) + { + myStats->EFFECTS_TIMERS[c]--; + if ( c == EFF_POLYMORPH ) + { + if ( myStats->EFFECTS_TIMERS[c] == TICKS_PER_SECOND * 15 ) + { + playSoundPlayer(player, 611, 192); + messagePlayer(player, MESSAGE_STATUS, Language::get(3193)); + } + } + else if ( c == EFF_SHAPESHIFT ) + { + if ( myStats->EFFECTS_TIMERS[c] == TICKS_PER_SECOND * 15 ) + { + playSoundPlayer(player, 611, 192); + messagePlayer(player, MESSAGE_STATUS, Language::get(3475)); + } + } + else if ( c == EFF_PROJECT_SPIRIT ) + { + if ( myStats->EFFECTS_TIMERS[c] == TICKS_PER_SECOND * 15 ) + { + playSoundPlayer(player, 611, 192); + messagePlayer(player, MESSAGE_STATUS, Language::get(6876)); + } + } + + if ( c == EFF_MAGICIANS_ARMOR ) + { + if ( myStats->EFFECTS_TIMERS[c] > 1 && myStats->EFFECTS_TIMERS[c] < (TICKS_PER_SECOND * 5) ) + { + // refresh timer and send to clients + this->setEffect(EFF_MAGICIANS_ARMOR, myStats->getEffectActive(EFF_MAGICIANS_ARMOR), 15 * TICKS_PER_SECOND, false, true, true); + updateClient = true; + } + } + + if ( c == EFF_SALAMANDER_HEART && behavior == &actPlayer ) + { + if ( !(myStats->type == SALAMANDER) ) + { + setEffect(EFF_SALAMANDER_HEART, false, 0, true); + } + else if ( Uint8 effectStrength = myStats->getEffectActive(c) ) + { + if ( myStats->EFFECTS_TIMERS[EFF_SALAMANDER_HEART] == 0 ) + { + if ( effectStrength == 2 ) + { + setEffect(EFF_SALAMANDER_HEART, + (Uint8)1, -1, true, true, true); + messagePlayer(isEntityPlayer(), MESSAGE_STATUS, Language::get(6921)); + } + else if ( effectStrength == 4 ) + { + setEffect(EFF_SALAMANDER_HEART, + (Uint8)3, -1, true, true, true); + messagePlayer(isEntityPlayer(), MESSAGE_STATUS, Language::get(6917)); + } + } + } + } + else if ( c == EFF_GROWTH && behavior == &actPlayer ) + { + if ( !(myStats->type == MYCONID || myStats->type == DRYAD) || myStats->helmet ) + { + setEffect(EFF_GROWTH, false, 0, false); + } + else if ( myStats->getEffectActive(c) >= 1 ) + { + if ( myStats->EFFECTS_TIMERS[c] < 10 * TICKS_PER_SECOND ) + { + setEffect(EFF_GROWTH, (Uint8)myStats->getEffectActive(c), 30 * TICKS_PER_SECOND, false); + } + } + /*else if ( myStats->EFFECTS_TIMERS[c] == 0 ) + { + Uint8 effectStrength = myStats->getEffectActive(c); + if ( effectStrength < 4 ) + { + setEffect(EFF_GROWTH, (Uint8)(effectStrength + 1), 15 * TICKS_PER_SECOND, false); + } + }*/ + /*else if ( myStats->EFFECTS_TIMERS[c] == ((TICKS_PER_SECOND * 5) - 1) ) + { + if ( player > 0 && multiplayer == SERVER ) + { + needServerUpdateEffects = true; + } + }*/ + } + else if ( myStats->EFFECTS_TIMERS[c] == 0 ) + { + Uint8 effectStrength = myStats->getEffectActive(c); + myStats->clearEffect(c); + switch ( c ) + { + case EFF_ASLEEP: + messagePlayer(player, MESSAGE_STATUS, Language::get(593)); + if ( monsterAllyGetPlayerLeader() && monsterAllySpecial == ALLY_SPECIAL_CMD_REST ) + { + monsterAllySpecial = ALLY_SPECIAL_CMD_NONE; + myStats->clearEffect(EFF_HP_REGEN); + myStats->EFFECTS_TIMERS[EFF_HP_REGEN] = 0; + } + break; + case EFF_MIMIC_LOCKED: + if ( myStats->type == MIMIC ) { monsterHitTime = std::max(HITRATE - 12, monsterHitTime); // ready to attack myStats->monsterMimicLockedBy = 0; @@ -1285,6 +1663,7 @@ void Entity::effectTimes() break; case EFF_CONFUSED: messagePlayer(player, MESSAGE_STATUS, Language::get(596)); + updateClient = true; break; case EFF_DRUNK: messagePlayer(player, MESSAGE_STATUS, Language::get(597)); @@ -1300,16 +1679,16 @@ void Entity::effectTimes() { //Deduct mana from caster. Cancel spell if not enough mana (simply leave sustained at false). int oldMP = caster->getMP(); - bool deducted = caster->safeConsumeMP(1); //Consume 1 mana ever duration / mana seconds + bool deducted = caster->safeConsumeMP(getSustainCostOfSpell(invisibility_hijacked, this)); //Consume 1 mana ever duration / mana seconds if ( deducted ) { sustained = true; - myStats->EFFECTS[c] = true; + myStats->setEffectActive(c, 1); myStats->EFFECTS_TIMERS[c] = invisibility_hijacked->channel_duration; if ( caster->behavior == &actPlayer ) { - players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP()); + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP(), invisibility_hijacked->skillID); } } else @@ -1322,7 +1701,7 @@ void Entity::effectTimes() messagePlayer(i, MESSAGE_STATUS, Language::get(598)); } } - unsustainSpell = invisibility_hijacked; + //unsustainSpell = invisibility_hijacked; list_RemoveNode(invisibility_hijacked->magic_effects_node); //Remove it from the entity's magic effects. This has the side effect of removing it from the sustained spells list too. //list_RemoveNode(invisibility_hijacked->sustain_node); //Remove it from the channeled spells list. } @@ -1385,16 +1764,16 @@ void Entity::effectTimes() { //Deduct mana from caster. Cancel spell if not enough mana (simply leave sustained at false). int oldMP = caster->getMP(); - bool deducted = caster->safeConsumeMP(1); //Consume 1 mana ever duration / mana seconds + bool deducted = caster->safeConsumeMP(getSustainCostOfSpell(levitation_hijacked, this)); //Consume 1 mana ever duration / mana seconds if ( deducted ) { sustained = true; - myStats->EFFECTS[c] = true; + myStats->setEffectActive(c, 1); myStats->EFFECTS_TIMERS[c] = levitation_hijacked->channel_duration; if ( caster->behavior == &actPlayer ) { - players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP()); + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP(), levitation_hijacked->skillID); } } else @@ -1407,7 +1786,7 @@ void Entity::effectTimes() messagePlayer(i, MESSAGE_STATUS, Language::get(606)); //TODO: Unhardcode name? } } - unsustainSpell = levitation_hijacked; + //unsustainSpell = levitation_hijacked; list_RemoveNode(levitation_hijacked->magic_effects_node); //Remove it from the entity's magic effects. This has the side effect of removing it from the sustained spells list too. } } @@ -1434,7 +1813,7 @@ void Entity::effectTimes() else { setEffect(EFF_NAUSEA_PROTECTION, false, 0, true); - if ( players[player]->entity->entityCanVomit() && !stats[player]->EFFECTS[EFF_VOMITING] ) + if ( players[player]->entity->entityCanVomit() && !stats[player]->getEffectActive(EFF_VOMITING) ) { messagePlayer(player, MESSAGE_STATUS, Language::get(634)); players[player]->entity->char_gonnavomit = 140 + local_rng.rand() % 60; @@ -1459,7 +1838,10 @@ void Entity::effectTimes() if ( mapCreature ) { // undo telepath rendering. - mapCreature->monsterEntityRenderAsTelepath = 0; + if ( mapCreature->monsterEntityRenderAsTelepath == 1 ) + { + mapCreature->monsterEntityRenderAsTelepath = 0; + } } } } @@ -1525,16 +1907,16 @@ void Entity::effectTimes() { //Deduct mana from caster. Cancel spell if not enough mana (simply leave sustained at false). int oldMP = caster->getMP(); - bool deducted = caster->safeConsumeMP(1); //Consume 1 mana ever duration / mana seconds + bool deducted = caster->safeConsumeMP(getSustainCostOfSpell(reflectMagic_hijacked, this)); //Consume 1 mana ever duration / mana seconds if ( deducted ) { sustained = true; - myStats->EFFECTS[c] = true; + myStats->setEffectActive(c, 1); myStats->EFFECTS_TIMERS[c] = reflectMagic_hijacked->channel_duration; if ( caster->behavior == &actPlayer ) { - players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP()); + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP(), reflectMagic_hijacked->skillID); } } else @@ -1547,7 +1929,7 @@ void Entity::effectTimes() messagePlayer(i, MESSAGE_STATUS, Language::get(2474)); } } - unsustainSpell = reflectMagic_hijacked; + //unsustainSpell = reflectMagic_hijacked; list_RemoveNode(reflectMagic_hijacked->magic_effects_node); //Remove it from the entity's magic effects. This has the side effect of removing it from the sustained spells list too. //list_RemoveNode(reflectMagic_hijacked->sustain_node); //Remove it from the channeled spells list. } @@ -1573,16 +1955,16 @@ void Entity::effectTimes() { //Deduct mana from caster. Cancel spell if not enough mana (simply leave sustained at false). int oldMP = caster->getMP(); - bool deducted = caster->safeConsumeMP(1); //Consume 1 mana ever duration / mana seconds + bool deducted = caster->safeConsumeMP(getSustainCostOfSpell(amplifyMagic_hijacked, this)); //Consume 1 mana ever duration / mana seconds if ( deducted ) { sustained = true; - myStats->EFFECTS[c] = true; + myStats->setEffectActive(c, 1); myStats->EFFECTS_TIMERS[c] = amplifyMagic_hijacked->channel_duration; if ( caster->behavior == &actPlayer ) { - players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP()); + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP(), amplifyMagic_hijacked->skillID); } } else @@ -1595,7 +1977,7 @@ void Entity::effectTimes() messagePlayer(i, MESSAGE_STATUS, Language::get(3443)); } } - unsustainSpell = amplifyMagic_hijacked; + //unsustainSpell = amplifyMagic_hijacked; list_RemoveNode(amplifyMagic_hijacked->magic_effects_node); //Remove it from the entity's magic effects. This has the side effect of removing it from the sustained spells list too. } } @@ -1610,6 +1992,207 @@ void Entity::effectTimes() updateClient = true; } break; + case EFF_BLOOD_WARD: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6505)); + updateClient = true; + } + break; + case EFF_TRUE_BLOOD: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6507)); + updateClient = true; + } + break; + case EFF_DIVINE_ZEAL: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6509)); + updateClient = true; + } + break; + case EFF_FLAME_CLOAK: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6859)); + updateClient = true; + } + break; + case EFF_GUARD_BODY: + dissipate = true; //Remove the effect by default. + if ( sustainedSpell_hijacked.find(EFF_GUARD_BODY) != sustainedSpell_hijacked.end() ) + { + sustainedSpell_hijacked[EFF_GUARD_BODY]->channel_effectStrength = effectStrength; + } + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6864)); + updateClient = true; + } + break; + case EFF_GUARD_SPIRIT: + dissipate = true; //Remove the effect by default. + if ( sustainedSpell_hijacked.find(EFF_GUARD_SPIRIT) != sustainedSpell_hijacked.end() ) + { + sustainedSpell_hijacked[EFF_GUARD_SPIRIT]->channel_effectStrength = effectStrength; + } + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6865)); + updateClient = true; + } + break; + case EFF_DIVINE_GUARD: + dissipate = true; //Remove the effect by default. + if ( sustainedSpell_hijacked.find(EFF_DIVINE_GUARD) != sustainedSpell_hijacked.end() ) + { + sustainedSpell_hijacked[EFF_DIVINE_GUARD]->channel_effectStrength = effectStrength; + } + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6866)); + updateClient = true; + } + break; + case EFF_ENVENOM_WEAPON: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6529)); + updateClient = true; + } + break; + case EFF_THORNS: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6797)); + updateClient = true; + } + break; + case EFF_BLADEVINES: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6798)); + updateClient = true; + } + break; + case EFF_BASTION_MUSHROOM: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6801)); + updateClient = true; + } + break; + case EFF_BASTION_ROOTS: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6802)); + updateClient = true; + } + break; + case EFF_ABUNDANCE: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6648)); + updateClient = true; + } + break; + case EFF_GREATER_ABUNDANCE: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6649)); + updateClient = true; + } + break; + case EFF_PRESERVE: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6656)); + updateClient = true; + } + break; + case EFF_MIST_FORM: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6664)); + updateClient = true; + } + break; + case EFF_LIGHTEN_LOAD: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6682)); + updateClient = true; + } + break; + case EFF_ATTRACT_ITEMS: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6684)); + updateClient = true; + } + break; + case EFF_RETURN_ITEM: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6686)); + updateClient = true; + } + break; + case EFF_SPORES: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6642)); + updateClient = true; + } + break; + case EFF_OVERCHARGE: + dissipate = true; //Remove the effect by default. + sustainedSpellProcess(*this, *myStats, c, sustainedSpell_hijacked, dissipate, unsustainSpell); + if ( dissipate ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6530)); + updateClient = true; + } + break; case EFF_VAMPIRICAURA: dissipate = true; //Remove the effect by default. if ( vampiricAura_hijacked ) @@ -1620,16 +2203,16 @@ void Entity::effectTimes() { //Deduct mana from caster. Cancel spell if not enough mana (simply leave sustained at false). int oldMP = caster->getMP(); - bool deducted = caster->safeConsumeMP(1); //Consume 3 mana ever duration / mana seconds + bool deducted = caster->safeConsumeMP(getSustainCostOfSpell(vampiricAura_hijacked, this)); //Consume 3 mana ever duration / mana seconds if ( deducted ) { sustained = true; - myStats->EFFECTS[c] = true; + myStats->setEffectActive(c, 1); myStats->EFFECTS_TIMERS[c] = vampiricAura_hijacked->channel_duration; if ( caster->behavior == &actPlayer ) { - players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP()); + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - caster->getMP(), vampiricAura_hijacked->skillID); } // monsters have a chance to un-sustain the spell each MP consume. @@ -1649,7 +2232,7 @@ void Entity::effectTimes() //messagePlayer(player, MESSAGE_STATUS, Language::get(2449)); } } - unsustainSpell = vampiricAura_hijacked; + //unsustainSpell = vampiricAura_hijacked; list_RemoveNode(vampiricAura_hijacked->magic_effects_node); //Remove it from the entity's magic effects. This has the side effect of removing it from the sustained spells list too. //list_RemoveNode(reflectMagic_hijacked->sustain_node); //Remove it from the channeled spells list. } @@ -1673,10 +2256,16 @@ void Entity::effectTimes() case EFF_SLOW: messagePlayer(player, MESSAGE_STATUS, Language::get(604)); // "You return to your normal speed." break; + case EFF_PROJECT_SPIRIT: + messagePlayer(player, MESSAGE_STATUS, Language::get(6875)); + playSoundEntity(this, 400, 92); + createParticleDropRising(this, 593, 1.f); + serverSpawnMiscParticles(this, PARTICLE_EFFECT_RISING_DROP, 593); + break; case EFF_POLYMORPH: effectPolymorph = 0; serverUpdateEntitySkill(this, 50); - if ( !myStats->EFFECTS[EFF_SHAPESHIFT] ) + if ( !myStats->getEffectActive(EFF_SHAPESHIFT) ) { messagePlayer(player, MESSAGE_STATUS, Language::get(3185)); } @@ -1693,7 +2282,7 @@ void Entity::effectTimes() case EFF_SHAPESHIFT: effectShapeshift = 0; serverUpdateEntitySkill(this, 53); - if ( !myStats->EFFECTS[EFF_POLYMORPH] ) + if ( !myStats->getEffectActive(EFF_POLYMORPH) ) { messagePlayer(player, MESSAGE_STATUS, Language::get(3417)); } @@ -1713,10 +2302,52 @@ void Entity::effectTimes() break; case EFF_KNOCKBACK: break; + case EFF_MAGICIANS_ARMOR: + messagePlayer(player, MESSAGE_STATUS, Language::get(6863)); + updateClient = true; + break; + case EFF_DELAY_PAIN: + if ( effectStrength > 1 ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6629)); + modHP(-((Sint32)effectStrength - 1)); + setObituary(Language::get(6626)); + + // Give the Player feedback on being hurt + playSoundEntity(this, 28, 64); // "Damage.ogg" + + // Shake the Host's screen + if ( player >= 0 ) + { + auto& camera_shakex = cameravars[player].shakex; + auto& camera_shakey = cameravars[player].shakey; + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + camera_shakex += .3; + camera_shakey += 30; + } + else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + // Shake the Client's screen + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 30; // turns into .1 + net_packet->data[5] = 30; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + } + } + else + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6628)); + } + break; case EFF_WITHDRAWAL: if ( player >= 0 && player < MAXPLAYERS ) { - if ( myStats->EFFECTS[EFF_DRUNK] ) + if ( myStats->getEffectActive(EFF_DRUNK) ) { // we still drunk! no need for hangover just yet... // extend another 15 seconds. @@ -1736,24 +2367,58 @@ void Entity::effectTimes() messagePlayer(player, MESSAGE_STATUS, Language::get(3439)); updateClient = true; break; + case EFF_MAXIMISE: + case EFF_MINIMISE: + messagePlayer(player, MESSAGE_STATUS, Language::get(6898)); + updateClient = true; + break; case EFF_PACIFY: case EFF_SHADOW_TAGGED: case EFF_WEBBED: + case EFF_PINPOINT: + case EFF_PENANCE: + case EFF_DETECT_ENEMY: + case EFF_DEFY_FLESH: + updateClient = true; + break; + case EFF_DUSTED: + messagePlayer(player, MESSAGE_STATUS, Language::get(6751)); updateClient = true; break; + case EFF_DISORIENTED: + if ( behavior == &actMonster && monsterState == MONSTER_STATE_WAIT ) + { + if ( Entity* target = uidToEntity(monsterLastDistractedByNoisemaker) ) + { + if ( target->behavior == &actMonster ) + { + if ( Stat* targetStats = target->getStats() ) + { + if ( targetStats->type == DUCK_SMALL ) + { + if ( Entity* leader = uidToEntity(targetStats->leader_uid) ) + { + lookAtEntity(*leader); + } + } + } + } + } + } + break; default: break; } if ( player > 0 && multiplayer == SERVER ) { - serverUpdateEffects(player); + needServerUpdateEffects = true; } } else if ( myStats->EFFECTS_TIMERS[c] == ((TICKS_PER_SECOND * 5) - 1) ) { if ( player > 0 && multiplayer == SERVER ) { - serverUpdateEffects(player); + needServerUpdateEffects = true; } } } @@ -1774,6 +2439,11 @@ void Entity::effectTimes() unsustainSpell = nullptr; } + if ( needServerUpdateEffects ) + { + serverUpdateEffects(player); + } + if ( updateClient ) { //Only a select few effects have something that needs to be handled on the client's end. @@ -1795,15 +2465,32 @@ bool Entity::increaseSkill(int skill, bool notify) { Stat* myStats = this->getStats(); int player = -1; + if ( this->behavior == &actPlayer ) + { + player = this->skill[2]; + } + + if ( myStats == NULL ) + { + return false; + } - if ( myStats == NULL ) + if ( skill == PRO_LEGACY_SWIMMING ) + { + if ( !(svFlags & SV_FLAG_HUNGER) ) + { + // hunger off and swimming is raised. + serverUpdatePlayerGameplayStats(player, STATISTICS_HOT_TUB_TIME_MACHINE, 1); + } + return true; + } + if ( skill >= NUMPROFICIENCIES ) { return false; } + if ( this->behavior == &actPlayer ) { - player = this->skill[2]; - if ( gameModeManager.currentSession.challengeRun.isActive() && gameModeManager.currentSession.challengeRun.eventType == GameModeManager_t::CurrentSession_t::ChallengeRun_t::CHEVENT_NOSKILLS ) { @@ -1849,25 +2536,25 @@ bool Entity::increaseSkill(int skill, bool notify) break; } - if ( skill == PRO_SPELLCASTING && skillCapstoneUnlockedEntity(PRO_SPELLCASTING) ) - { - //Spellcasting capstone = free casting of Forcebolt. - //Give the player the spell if they haven't learned it yet. - if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) - { - strcpy((char*)net_packet->data, "ASPL"); - net_packet->data[4] = clientnum; - net_packet->data[5] = SPELL_FORCEBOLT; - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); - } - else if ( player >= 0 ) - { - addSpell(SPELL_FORCEBOLT, player, true); - } - } + //if ( skill == PRO_LEGACY_SPELLCASTING && skillCapstoneUnlockedEntity(PRO_LEGACY_SPELLCASTING) ) + //{ + // //Spellcasting capstone = free casting of Forcebolt. + // //Give the player the spell if they haven't learned it yet. + // if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + // { + // strcpy((char*)net_packet->data, "ASPL"); + // net_packet->data[4] = clientnum; + // net_packet->data[5] = SPELL_FORCEBOLT; + // net_packet->address.host = net_clients[player - 1].host; + // net_packet->address.port = net_clients[player - 1].port; + // net_packet->len = 6; + // sendPacketSafe(net_sock, -1, net_packet, player - 1); + // } + // else if ( player >= 0 ) + // { + // addSpell(SPELL_FORCEBOLT, player, true); + // } + //} if ( skill == PRO_STEALTH && myStats->getProficiency(skill) == 100 ) { @@ -1902,30 +2589,82 @@ bool Entity::increaseSkill(int skill, bool notify) } } - if ( skill == PRO_SWIMMING && !(svFlags & SV_FLAG_HUNGER) ) - { - // hunger off and swimming is raised. - serverUpdatePlayerGameplayStats(player, STATISTICS_HOT_TUB_TIME_MACHINE, 1); - } - - if ( skill == PRO_MAGIC && skillCapstoneUnlockedEntity(PRO_MAGIC) ) + if ( player >= 0 ) { - //magic capstone = bonus spell: Dominate. - if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + if ( myStats->getEffectActive(EFF_NIMBLENESS) + && (skill == PRO_LOCKPICKING + || skill == PRO_SWORD + || skill == PRO_RANGED + || skill == PRO_STEALTH) ) { - strcpy((char*)net_packet->data, "ASPL"); - net_packet->data[4] = clientnum; - net_packet->data[5] = SPELL_DOMINATE; - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); + int caster = ((myStats->getEffectActive(EFF_NIMBLENESS) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_NIMBLENESS, 100.0, 1.0, nullptr); + } + } + } + if ( myStats->getEffectActive(EFF_GREATER_MIGHT) + && (skill == PRO_POLEARM + || skill == PRO_AXE + || skill == PRO_MACE) ) + { + int caster = ((myStats->getEffectActive(EFF_GREATER_MIGHT) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_GREATER_MIGHT, 100.0, 1.0, nullptr); + } + } } - else if ( player >= 0 ) + if ( myStats->getEffectActive(EFF_COUNSEL) + && (skill == PRO_SORCERY + || skill == PRO_MYSTICISM) ) { - addSpell(SPELL_DOMINATE, player, true); + int caster = ((myStats->getEffectActive(EFF_COUNSEL) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_COUNSEL, 100.0, 1.0, nullptr); + } + } + } + if ( myStats->getEffectActive(EFF_STURDINESS) + && (skill == PRO_SHIELD) ) + { + int caster = ((myStats->getEffectActive(EFF_STURDINESS) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_PROF_STURDINESS, 100.0, 1.0, nullptr); + } + } } } + + //if ( skill == PRO_SORCERY && skillCapstoneUnlockedEntity(PRO_SORCERY) ) + //{ + // //magic capstone = bonus spell: Dominate. + // if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + // { + // strcpy((char*)net_packet->data, "ASPL"); + // net_packet->data[4] = clientnum; + // net_packet->data[5] = SPELL_DOMINATE; + // net_packet->address.host = net_clients[player - 1].host; + // net_packet->address.port = net_clients[player - 1].port; + // net_packet->len = 6; + // sendPacketSafe(net_sock, -1, net_packet, player - 1); + // } + // else if ( player >= 0 ) + // { + // addSpell(SPELL_DOMINATE, player, true); + // } + //} myStats->EXP += 2; if ( player >= 0 ) @@ -1934,6 +2673,21 @@ bool Entity::increaseSkill(int skill, bool notify) Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_XP_MAX_INSTANCE, "xp", 2); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_XP_SKILLS, "xp", 2); + if ( (myStats->playerRace == RACE_SALAMANDER && myStats->stat_appearance == 0) || myStats->type == SALAMANDER ) + { + Sint32 oldMP = myStats->MP; + int mpAmount = this->modMP(std::max(1, myStats->MAXMP / 50 + statGetCHR(myStats, this) / 10)); + this->playerInsectoidIncrementHungerToMP(mpAmount); + if ( oldMP < myStats->MP ) + { + if ( behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_RGN_MP_RUN, "rgn", myStats->MP - oldMP); + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_RGN_MP_SUM, "rgn", myStats->MP - oldMP); + } + } + } + const char* skillstr = Compendium_t::getSkillStringForCompendium(skill); if ( strcmp(skillstr, "") ) { @@ -2511,6 +3265,28 @@ void Entity::modHP(int amount) } } + if ( entitystats && entitystats->getEffectActive(EFF_STASIS) ) + { + amount = 0; + } + if ( entitystats && entitystats->getEffectActive(EFF_SALAMANDER_HEART) == 4 && entitystats->type == SALAMANDER ) + { + if ( amount < 0 ) + { + amount = 0; + } + } + + if ( entitystats && entitystats->type == DUCK_SMALL && amount < 0 ) + { + amount = 0; + if ( monsterAttack == 0 ) + { + // duck dodge + this->attack(local_rng.rand() % 2 ? MONSTER_POSE_MELEE_WINDUP2 : MONSTER_POSE_MELEE_WINDUP3, 0, nullptr); + } + } + if ( !entitystats || amount == 0 ) { if ( this->behavior == &actPlayer ) @@ -2520,6 +3296,34 @@ void Entity::modHP(int amount) return; } + if ( amount < 0 ) + { + if ( entitystats ) + { + if ( int effectStrength = entitystats->getEffectActive(EFF_DELAY_PAIN) ) + { + while ( amount < 0 && effectStrength < 251 ) + { + effectStrength++; + ++amount; + } + if ( effectStrength != entitystats->getEffectActive(EFF_DELAY_PAIN) ) + { + setEffect(EFF_DELAY_PAIN, (Uint8)effectStrength, entitystats->EFFECTS_TIMERS[EFF_DELAY_PAIN], false); + } + } + } + } + + if ( behavior == &actMonster && entitystats->type == AUTOMATON + && (monsterSpecialState == AUTOMATON_MALFUNCTION_START || monsterSpecialState == AUTOMATON_MALFUNCTION_RUN) ) + { + if ( amount > 0 ) + { + return; + } + } + Sint32 oldHP = entitystats->HP; this->setHP(entitystats->HP + amount); if ( oldHP > entitystats->HP ) @@ -2580,13 +3384,13 @@ modifies the MP of the given entity -------------------------------------------------------------------------------*/ -void Entity::modMP(int amount, bool updateClients) +int Entity::modMP(int amount, bool updateClients) { Stat* entitystats = this->getStats(); if ( !entitystats ) { - return; + return 0; } if ( this->behavior == &actPlayer && godmode && amount < 0 ) @@ -2595,10 +3399,12 @@ void Entity::modMP(int amount, bool updateClients) } if ( !entitystats || amount == 0 ) { - return; + return 0; } + Sint32 oldMP = entitystats->MP; this->setMP(entitystats->MP + amount, updateClients); + return entitystats->MP - oldMP; } int Entity::getMP() @@ -2743,6 +3549,14 @@ void Entity::drainMP(int amount, bool notifyOverexpend) tempStats->killer = KilledBy::FAILED_INVOCATION; } } + + if ( (oldMP - entitystats->MP) > 0 ) + { + if ( entitystats->getEffectActive(EFF_MAGIC_WELL) ) + { + entitystats->setEffectActive(EFF_MAGIC_WELL, std::min((Uint8)101, (Uint8)(entitystats->getEffectActive(EFF_MAGIC_WELL) + 1))); + } + } } /*------------------------------------------------------------------------------- @@ -2855,7 +3669,7 @@ int Entity::getHungerTickRate(Stat* myStats, bool isPlayer, bool checkItemsEffec int vampiricHunger = 0; if ( checkItemsEffects ) { - if ( myStats->EFFECTS[EFF_VAMPIRICAURA] ) + if ( myStats->getEffectActive(EFF_VAMPIRICAURA) ) { if ( myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) { @@ -2936,6 +3750,20 @@ int Entity::getHungerTickRate(Stat* myStats, bool isPlayer, bool checkItemsEffec } } } + + if ( myStats->getEffectActive(EFF_FOCI_LIGHT_PROVIDENCE) ) + { + real_t mult = 1.0; + mult += getSpellEffectDurationSecondaryFromID(SPELL_FOCI_LIGHT_PROVIDENCE, nullptr, nullptr, nullptr) / 100.0; + int tier = std::max(0, (myStats->getEffectActive(EFF_FOCI_LIGHT_PROVIDENCE) - 1)); + mult += tier * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_PROVIDENCE, nullptr, nullptr, nullptr) / 100.0; + hungerTickRate *= mult; + } + } + + if ( myStats->type == SALAMANDER || (myStats->playerRace == RACE_SALAMANDER && myStats->stat_appearance == 0) ) + { + hungerTickRate *= 0.75; } bool playerAutomaton = (myStats->type == AUTOMATON && isPlayer); @@ -2982,13 +3810,71 @@ static ConsoleVariable cvar_noxp("/noxp", false); void Entity::handleEffects(Stat* myStats) { int increasestat[3] = { 0, 0, 0 }; - int i, c; int player = -1; if ( !myStats ) { return; } + + for ( int i = 0; i < NUMEFFECTS; ++i ) + { + if ( myStats->getEffectActive(i) ) + { + myStats->EFFECTS_ACCRETION_TIME[i]++; + if ( myStats->EFFECTS_ACCRETION_TIME[i] % TICKS_PER_SECOND == 0 ) + { + if ( behavior == &actPlayer ) + { + if ( i == EFF_FOCI_LIGHT_JUSTICE ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, TOOL_FOCI_LIGHT_JUSTICE, TICKS_PER_SECOND); + } + else if ( i == EFF_FOCI_LIGHT_PEACE ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, TOOL_FOCI_LIGHT_PEACE, TICKS_PER_SECOND); + } + else if ( i == EFF_FOCI_LIGHT_PROVIDENCE ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, TOOL_FOCI_LIGHT_PROVIDENCE, TICKS_PER_SECOND); + } + else if ( i == EFF_FOCI_LIGHT_PURITY ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, TOOL_FOCI_LIGHT_PURITY, TICKS_PER_SECOND); + } + else if ( i == EFF_FOCI_LIGHT_SANCTUARY ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, TOOL_FOCI_LIGHT_SANCTUARY, TICKS_PER_SECOND); + } + else if ( i == EFF_ENSEMBLE_DRUM ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, INSTRUMENT_DRUM, TICKS_PER_SECOND); + } + else if ( i == EFF_ENSEMBLE_FLUTE ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, INSTRUMENT_FLUTE, TICKS_PER_SECOND); + } + else if ( i == EFF_ENSEMBLE_LUTE ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, INSTRUMENT_LUTE, TICKS_PER_SECOND); + } + else if ( i == EFF_ENSEMBLE_LYRE ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, INSTRUMENT_LYRE, TICKS_PER_SECOND); + } + else if ( i == EFF_ENSEMBLE_HORN ) + { + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_EFFECT_DURATION, INSTRUMENT_HORN, TICKS_PER_SECOND); + } + } + } + } + else + { + myStats->EFFECTS_ACCRETION_TIME[i] = 0; + } + } + if ( this->behavior == &actPlayer ) { player = this->skill[2]; @@ -3009,7 +3895,9 @@ void Entity::handleEffects(Stat* myStats) if ( myStats->defending ) { - if ( myStats->shield && !myStats->EFFECTS[EFF_SHAPESHIFT] ) + if ( myStats->shield + && (!myStats->getEffectActive(EFF_SHAPESHIFT) + || (myStats->getEffectActive(EFF_SHAPESHIFT) && effectShapeshift == CREATURE_IMP && itemTypeIsFoci(myStats->shield->type))) ) { if ( players[player]->mechanics.defendTicks == 0 ) { @@ -3025,6 +3913,34 @@ void Entity::handleEffects(Stat* myStats) { players[player]->mechanics.defendTicks = 0; } + + if ( myStats->type == MYCONID || myStats->type == DRYAD ) + { + if ( !myStats->helmet ) + { + if ( !stats[player]->getEffectActive(EFF_GROWTH) || stats[player]->EFFECTS_TIMERS[EFF_GROWTH] == -1 ) + { + setEffect(EFF_GROWTH, (Uint8)1, 15 * TICKS_PER_SECOND, false); + } + } + } + + if ( myStats->type == GREMLIN || (myStats->playerRace == RACE_GREMLIN && myStats->stat_appearance == 0) ) + { + if ( getUID() % 10 * TICKS_PER_SECOND == ticks % 10 * TICKS_PER_SECOND ) + { + players[player]->mechanics.updateBreakableCounterServer(); + } + } + } + + if ( myStats->parrying > 0 ) + { + --myStats->parrying; + if ( !myStats->weapon || (myStats->weapon && itemCategory(myStats->weapon) != WEAPON) ) + { + myStats->parrying = 0; + } } if ( myStats->HP > 0 ) @@ -3042,6 +3958,10 @@ void Entity::handleEffects(Stat* myStats) maxMPMod += 10 * std::min(2, (int)abs(myStats->ring->beatitude)); } } + if ( myStats->getEffectActive(EFF_STURDINESS) ) + { + maxHPMod += 10; + } if ( maxHPMod != myStats->MISC_FLAGS[STAT_FLAG_HP_BONUS] ) { @@ -3082,7 +4002,7 @@ void Entity::handleEffects(Stat* myStats) auto& camera_shakey2 = cameravars[player >= 0 ? player : 0].shakey2; // sleep Zs - if ( myStats->EFFECTS[EFF_ASLEEP] && ticks % 30 == 0 ) + if ( (myStats->getEffectActive(EFF_ASLEEP) || myStats->getEffectActive(EFF_PROJECT_SPIRIT)) && ticks % 30 == 0 ) { spawnSleepZ(this->x + cos(this->yaw) * 2, this->y + sin(this->yaw) * 2, this->z); } @@ -3100,7 +4020,7 @@ void Entity::handleEffects(Stat* myStats) else { Stat* tagStats = tagged->getStats(); - if ( tagStats && !tagStats->EFFECTS[EFF_SHADOW_TAGGED] ) // effect timed out. + if ( tagStats && !tagStats->getEffectActive(EFF_SHADOW_TAGGED) ) // effect timed out. { creatureShadowTaggedThisUid = 0; serverUpdateEntitySkill(this, 54); @@ -3108,27 +4028,73 @@ void Entity::handleEffects(Stat* myStats) } } + + if ( Uint8 effectStrength = myStats->getEffectActive(EFF_DETECT_ENEMY) ) + { + if ( ticks % 60 == 0 && behavior == &actMonster ) + { + Entity* caster = nullptr; + if ( effectStrength >= 1 && effectStrength < 1 + MAXPLAYERS ) + { + int pnum = effectStrength - 1; + if ( players[pnum] && players[pnum]->entity ) + { + if ( !strcmp(myStats->name, "") ) + { + updateEnemyBar(players[pnum]->entity, this, getMonsterLocalizedName(myStats->type).c_str(), myStats->HP, myStats->MAXHP, + false, DMG_DETECT_MONSTER); + } + else + { + updateEnemyBar(players[pnum]->entity, this, myStats->name, myStats->HP, myStats->MAXHP, + false, DMG_DETECT_MONSTER); + } + } + } + } + } + if ( *cvar_noxp ) { myStats->EXP = 0; } + int expToLevel = 100/* + 10 * ((myStats->LVL) / 10)*/; + // level ups - if ( myStats->EXP >= 100 ) + if ( myStats->EXP >= expToLevel ) { - myStats->EXP -= 100; + myStats->EXP -= expToLevel; if ( player >= 0 ) { if ( myStats->LVL < 255 ) { myStats->LVL++; } + players[player]->mechanics.baseSpellIncrementMP(stats[player]->getProficiency(PRO_SORCERY), PRO_SORCERY); + players[player]->mechanics.baseSpellIncrementMP(stats[player]->getProficiency(PRO_MYSTICISM), PRO_MYSTICISM); + players[player]->mechanics.baseSpellIncrementMP(stats[player]->getProficiency(PRO_THAUMATURGY), PRO_THAUMATURGY); } else { myStats->LVL++; } + if ( player >= 0 ) + { + if ( myStats->getEffectActive(EFF_ENSEMBLE_FLUTE) + || myStats->getEffectActive(EFF_ENSEMBLE_LUTE) + || myStats->getEffectActive(EFF_ENSEMBLE_LYRE) + || myStats->getEffectActive(EFF_ENSEMBLE_HORN) + || myStats->getEffectActive(EFF_ENSEMBLE_DRUM) ) + { + serverUpdatePlayerGameplayStats(player, STATISTICS_BARDIC_INSPIRATION, 1); + } + else + { + serverUpdatePlayerGameplayStats(player, STATISTICS_BARDIC_INSPIRATION, 0); + } + } if ( player >= 0 && players[player]->isLocalPlayer() ) { players[player]->hud.xpBar.animateState = Player::HUD_t::AnimateStates::ANIMATE_LEVELUP_RISING; @@ -3139,29 +4105,7 @@ void Entity::handleEffects(Stat* myStats) messagePlayerColor(player, MESSAGE_SPAM_MISC, color, Language::get(622)); static ConsoleVariable cvar_lvlup_ally_sfx("/lvlup_ally_sfx", 520); - - // increase MAXHP/MAXMP - myStats->MAXHP += HP_MOD; - modHP(getHPRestoreOnLevelUp()); - myStats->HP = std::min(myStats->HP, myStats->MAXHP); - if ( !(behavior == &actMonster && monsterAllySummonRank != 0) ) - { - myStats->MP += MP_MOD; - myStats->MAXMP += MP_MOD; - if ( behavior == &actPlayer && myStats->playerRace == RACE_INSECTOID && myStats->stat_appearance == 0 ) - { - myStats->MAXMP = std::min(100, myStats->MAXMP); - if ( svFlags & SV_FLAG_HUNGER ) - { - Sint32 hungerPointPerMana = playerInsectoidHungerValueOfManaPoint(*myStats); - myStats->HUNGER += MP_MOD * hungerPointPerMana; - myStats->HUNGER = std::min(1000, myStats->HUNGER); - serverUpdateHunger(skill[2]); - } - } - myStats->MP = std::min(myStats->MP, myStats->MAXMP); - } - + bool alreadyProcessedMaxHPIncrease = false; // now pick three attributes to increase if ( player >= 0 ) @@ -3170,8 +4114,30 @@ void Entity::handleEffects(Stat* myStats) this->playerStatIncrease(client_classes[player], increasestat); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_LVL_GAINED, "leveling up", 1); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_LVL_MAX, "leveling up", myStats->LVL); + + if ( this->effectShapeshift != NOTHING ) + { + switch ( stats[player]->type ) + { + case RAT: + magicOnSpellCastEvent(this, this, nullptr, SPELL_RAT_FORM, spell_t::SPELL_LEVEL_EVENT_SHAPESHIFT, 1); + break; + case SPIDER: + case CRAB: + magicOnSpellCastEvent(this, this, nullptr, SPELL_SPIDER_FORM, spell_t::SPELL_LEVEL_EVENT_SHAPESHIFT, 1); + break; + case TROLL: + magicOnSpellCastEvent(this, this, nullptr, SPELL_TROLL_FORM, spell_t::SPELL_LEVEL_EVENT_SHAPESHIFT, 1); + break; + case CREATURE_IMP: + magicOnSpellCastEvent(this, this, nullptr, SPELL_IMP_FORM, spell_t::SPELL_LEVEL_EVENT_SHAPESHIFT, 1); + break; + default: + break; + } + } } - else if ( behavior == &actMonster && monsterAllySummonRank != 0 ) + else if ( behavior == &actMonster && monsterAllySummonRank != 0 && myStats->type == SKELETON ) { bool secondSummon = false; if ( MonsterData_t::nameMatchesSpecialNPCName(*myStats, "skeleton knight") ) @@ -3204,7 +4170,7 @@ void Entity::handleEffects(Stat* myStats) } } - for ( i = 0; i < 3; i++ ) + for ( int i = 0; i < 3; i++ ) { switch ( increasestat[i] ) { @@ -3239,6 +4205,8 @@ void Entity::handleEffects(Stat* myStats) { if ( !secondSummon ) { + myStats->MAXHP += HP_MOD; + alreadyProcessedMaxHPIncrease = true; leaderStats->playerSummonLVLHP = (myStats->LVL << 16); leaderStats->playerSummonLVLHP |= (myStats->MAXHP); @@ -3253,6 +4221,8 @@ void Entity::handleEffects(Stat* myStats) } else { + myStats->MAXHP += HP_MOD; + alreadyProcessedMaxHPIncrease = true; leaderStats->playerSummon2LVLHP = (myStats->LVL << 16); leaderStats->playerSummon2LVLHP |= (myStats->MAXHP); @@ -3283,7 +4253,7 @@ void Entity::handleEffects(Stat* myStats) // monsters use this. Entity::monsterRollLevelUpStats(increasestat); - for ( i = 0; i < 3; i++ ) + for ( int i = 0; i < 3; i++ ) { switch ( increasestat[i] ) { @@ -3296,45 +4266,28 @@ void Entity::handleEffects(Stat* myStats) case STAT_CON: myStats->CON++; break; - case STAT_INT: - myStats->INT++; - break; - case STAT_PER: - myStats->PER++; - break; - case STAT_CHR: - myStats->CHR++; - break; - default: - break; - } - } - } - - if ( behavior == &actMonster ) - { - if ( myStats->leader_uid ) - { - Entity* leader = uidToEntity(myStats->leader_uid); - if ( leader ) - { - for ( i = 0; i < MAXPLAYERS; ++i ) - { - if ( players[i] && players[i]->entity == leader ) - { - color = makeColorRGB(0, 255, 0); - messagePlayerMonsterEvent(i, color, *myStats, Language::get(2379), Language::get(2379), MSG_GENERIC); - playSoundEntity(this, *cvar_lvlup_ally_sfx, 128); - serverUpdateAllyStat(i, getUID(), myStats->LVL, myStats->HP, myStats->MAXHP, myStats->type); - } - } + case STAT_INT: + myStats->INT++; + break; + case STAT_PER: + myStats->PER++; + break; + case STAT_CHR: + myStats->CHR++; + break; + default: + break; } } } + int hpMod = HP_MOD; + int mpMod = MP_MOD; + int statIncrease[NUMSTATS] = { 0, 0, 0, 0, 0, 0 }; + if ( player >= 0 ) { - for ( i = 0; i < NUMSTATS * 2; ++i ) + for ( int i = 0; i < NUMSTATS * 2; ++i ) { myStats->PLAYER_LVL_STAT_TIMER[i] = 0; } @@ -3343,7 +4296,7 @@ void Entity::handleEffects(Stat* myStats) int statIconTicks = 250; std::vector StatUps; - for ( i = 0; i < 3; i++ ) + for ( int i = 0; i < 3; i++ ) { messagePlayerColor(player, MESSAGE_SPAM_MISC, color, Language::get(623 + increasestat[i])); switch ( increasestat[i] ) @@ -3370,6 +4323,7 @@ void Entity::handleEffects(Stat* myStats) } } } + statIncrease[increasestat[i]] += increment; myStats->STR += increment; Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_STAT_INCREASES, "str", 1); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_STAT_STR_MAX, "str", myStats->STR); @@ -3398,6 +4352,7 @@ void Entity::handleEffects(Stat* myStats) } } } + statIncrease[increasestat[i]] += increment; myStats->DEX += increment; Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_STAT_INCREASES, "dex", increment); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_STAT_DEX_MAX, "dex", myStats->DEX); @@ -3426,6 +4381,7 @@ void Entity::handleEffects(Stat* myStats) } } } + statIncrease[increasestat[i]] += increment; myStats->CON += increment; Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_STAT_INCREASES, "con", increment); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_STAT_CON_MAX, "con", myStats->CON); @@ -3454,6 +4410,7 @@ void Entity::handleEffects(Stat* myStats) } } } + statIncrease[increasestat[i]] += increment; myStats->INT += increment; Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_STAT_INCREASES, "int", increment); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_STAT_INT_MAX, "int", myStats->INT); @@ -3481,6 +4438,7 @@ void Entity::handleEffects(Stat* myStats) } } } + statIncrease[increasestat[i]] += increment; myStats->PER += increment; Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_STAT_INCREASES, "per", increment); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_STAT_PER_MAX, "per", myStats->PER); @@ -3509,6 +4467,7 @@ void Entity::handleEffects(Stat* myStats) } } } + statIncrease[increasestat[i]] += increment; myStats->CHR += increment; Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_STAT_INCREASES, "chr", increment); Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_STAT_CHR_MAX, "chr", myStats->CHR); @@ -3520,7 +4479,7 @@ void Entity::handleEffects(Stat* myStats) } } - for ( i = 0; i < MAXPLAYERS; ++i ) + for ( int i = 0; i < MAXPLAYERS; ++i ) { // broadcast a player levelled up to other players. if ( i != player ) @@ -3537,6 +4496,60 @@ void Entity::handleEffects(Stat* myStats) { levelUpAnimation[player].addLevelUp(stats[player]->LVL, 1, StatUps); } + + auto& baseGrowths = ClassBaseGrowths::getClassBaseGrowths(client_classes[player]); + hpMod = baseGrowths.baseHP; + mpMod = baseGrowths.baseMP; + for ( int i = 0; i < 3; ++i ) + { + hpMod += ClassBaseGrowths::hpStatGrowths[increasestat[i]] * statIncrease[increasestat[i]]; + mpMod += ClassBaseGrowths::mpStatGrowths[increasestat[i]] * statIncrease[increasestat[i]]; + } + } + + // increase MAXHP/MAXMP + if ( !alreadyProcessedMaxHPIncrease ) + { + myStats->MAXHP += hpMod; + } + int hpRestore = Entity::getHPRestoreOnLevelUp(this, myStats, hpMod); + int mpRestore = Entity::getMPRestoreOnLevelUp(this, myStats, mpMod); + + modHP(hpRestore); + + myStats->HP = std::min(myStats->HP, myStats->MAXHP); + if ( !(behavior == &actMonster && monsterAllySummonRank != 0) ) + { + Sint32 oldMP = myStats->MP; + myStats->MP += mpRestore; + myStats->MAXMP += mpMod; + if ( behavior == &actPlayer && myStats->playerRace == RACE_INSECTOID && myStats->stat_appearance == 0 ) + { + myStats->MAXMP = std::min(100, myStats->MAXMP); + this->playerInsectoidIncrementHungerToMP(myStats->MP - oldMP); + } + myStats->MP = std::min(myStats->MP, myStats->MAXMP); + } + + if ( behavior == &actMonster ) + { + if ( myStats->leader_uid ) + { + Entity* leader = uidToEntity(myStats->leader_uid); + if ( leader ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && players[i]->entity == leader ) + { + color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(i, color, *myStats, Language::get(2379), Language::get(2379), MSG_GENERIC); + playSoundEntity(this, *cvar_lvlup_ally_sfx, 128); + serverUpdateAllyStat(i, getUID(), myStats->LVL, myStats->HP, myStats->MAXHP, myStats->type); + } + } + } + } } // inform clients of stat changes @@ -3585,7 +4598,25 @@ void Entity::handleEffects(Stat* myStats) serverUpdatePlayerLVL(); // update all clients of party levels. } - for ( i = 0; i < NUMSTATS; ++i ) + if ( behavior == &actPlayer ) + { + Uint8 effectStrength = myStats->getEffectActive(EFF_GROWTH); + if ( effectStrength >= 1 ) + { + int stages = 1; + setEffect(EFF_GROWTH, (Uint8)(std::min(4, effectStrength + stages)), 15 * TICKS_PER_SECOND, false); + if ( myStats->getEffectActive(EFF_GROWTH) - effectStrength == 2 ) + { + messagePlayerColor(skill[2], MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6924)); + } + else if ( myStats->getEffectActive(EFF_GROWTH) > effectStrength ) + { + messagePlayerColor(skill[2], MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6925)); + } + } + } + + for ( int i = 0; i < NUMSTATS; ++i ) { myStats->PLAYER_LVL_STAT_BONUS[i] = -1; } @@ -3594,7 +4625,7 @@ void Entity::handleEffects(Stat* myStats) // hunger int hungerTickRate = Entity::getHungerTickRate(myStats, behavior == &actPlayer, true); int vampiricHunger = 0; - if ( myStats->EFFECTS[EFF_VAMPIRICAURA] ) + if ( myStats->getEffectActive(EFF_VAMPIRICAURA) ) { if ( myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) { @@ -3718,46 +4749,53 @@ void Entity::handleEffects(Stat* myStats) if ( ticks % hungerTickRate == 0 ) { //messagePlayer(0, "hungertick %d, curr %d, players: %d", hungerTickRate, myStats->HUNGER, playerCount); - myStats->HUNGER--; Sint32 noLongerFull = getEntityHungerInterval(player, this, myStats, HUNGER_INTERVAL_OVERSATIATED); Sint32 youFeelHungry = getEntityHungerInterval(player, this, myStats, HUNGER_INTERVAL_HUNGRY); Sint32 youFeelWeak = getEntityHungerInterval(player, this, myStats, HUNGER_INTERVAL_WEAK); Sint32 youFeelFaint = getEntityHungerInterval(player, this, myStats, HUNGER_INTERVAL_STARVING); - if ( myStats->HUNGER == noLongerFull ) + if ( myStats->HUNGER <= youFeelWeak && myStats->getEffectActive(EFF_FOCI_LIGHT_PROVIDENCE) ) { - if ( !myStats->EFFECTS[EFF_VOMITING] ) - { - messagePlayer(player, MESSAGE_STATUS, Language::get(629)); - } - serverUpdateHunger(player); + // stall hunger } - else if ( myStats->HUNGER == youFeelHungry ) + else { - if ( !myStats->EFFECTS[EFF_VOMITING] ) + myStats->HUNGER--; + if ( myStats->HUNGER == noLongerFull ) { - messagePlayer(player, MESSAGE_STATUS, Language::get(630)); - playSoundPlayer(player, 32, 128); + if ( !myStats->getEffectActive(EFF_VOMITING) ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(629)); + } + serverUpdateHunger(player); } - serverUpdateHunger(player); - } - else if ( myStats->HUNGER == youFeelWeak ) - { - if ( !myStats->EFFECTS[EFF_VOMITING] ) + else if ( myStats->HUNGER == youFeelHungry ) { - messagePlayer(player, MESSAGE_STATUS, Language::get(631)); - playSoundPlayer(player, 32, 128); + if ( !myStats->getEffectActive(EFF_VOMITING) ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(630)); + playSoundPlayer(player, 32, 128); + } + serverUpdateHunger(player); } - serverUpdateHunger(player); - } - else if ( myStats->HUNGER == youFeelFaint ) - { - if ( !myStats->EFFECTS[EFF_VOMITING] ) + else if ( myStats->HUNGER == youFeelWeak ) { - messagePlayer(player, MESSAGE_STATUS, Language::get(632)); - playSoundPlayer(player, 32, 128); + if ( !myStats->getEffectActive(EFF_VOMITING) ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(631)); + playSoundPlayer(player, 32, 128); + } + serverUpdateHunger(player); + } + else if ( myStats->HUNGER == youFeelFaint ) + { + if ( !myStats->getEffectActive(EFF_VOMITING) ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(632)); + playSoundPlayer(player, 32, 128); + } + serverUpdateHunger(player); } - serverUpdateHunger(player); } } } @@ -3786,7 +4824,7 @@ void Entity::handleEffects(Stat* myStats) } // Deal Hunger damage every three seconds - if ( doStarvation && !myStats->EFFECTS[EFF_VOMITING] && ticks % 150 == 0 ) + if ( doStarvation && !myStats->getEffectActive(EFF_VOMITING) && ticks % 150 == 0 && !myStats->getEffectActive(EFF_FOCI_LIGHT_PROVIDENCE) ) { serverUpdateHunger(player); bool allowStarve = true; @@ -3877,63 +4915,84 @@ void Entity::handleEffects(Stat* myStats) || myStats->type == INSECTOID || (behavior == &actPlayer && myStats->playerRace == RACE_INSECTOID && myStats->stat_appearance == 0)) ) { - if ( ticks % (HEAL_TIME) == 0 ) + if ( ticks % (HEAL_TIME / 4) == 0 ) { steamAchievementEntity(this, "BARONY_ACH_SMOKIN"); - int damage = 1 + local_rng.rand() % 3; - this->modHP(-damage); - if ( myStats->HP <= 0 ) + int damage = 1; + bool insect = false; + if ( myStats->type == INSECTOID + || (behavior == &actPlayer && myStats->playerRace == RACE_INSECTOID && myStats->stat_appearance == 0) ) { - this->setObituary(Language::get(1534)); // choked to death - myStats->killer = KilledBy::ITEM; - myStats->killer_item = MASK_PIPE; + insect = true; + damage = 1 + local_rng.rand() % 3; } - - // Give the Player feedback on being hurt - playSoundEntity(this, 28, 32); // "Damage.ogg" - - if ( myStats->HP > 0 ) + else { - messagePlayer(player, MESSAGE_STATUS, Language::get(6091)); + if ( local_rng.rand() % 4 > 0 ) + { + damage = 0; + } + } - // Shake the Host's screen - if ( myStats->HP <= 10 ) + if ( damage > 0 ) + { + this->modHP(-damage); + if ( myStats->HP <= 0 ) { - if ( player >= 0 && players[player]->isLocalPlayer() ) - { - camera_shakex += .1; - camera_shakey += 10; - } - else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) - { - // Shake the Client's screen - strcpy((char*)net_packet->data, "SHAK"); - net_packet->data[4] = 10; // turns into .1 - net_packet->data[5] = 10; - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); - } + this->setObituary(Language::get(1534)); // choked to death + myStats->killer = KilledBy::ITEM; + myStats->killer_item = MASK_PIPE; } - else + } + + if ( (insect && damage > 0) || (damage > 0 && myStats->HP < 10) ) + { + // Give the Player feedback on being hurt + playSoundEntity(this, 28, 32); // "Damage.ogg" + + if ( myStats->HP > 0 ) { - if ( player >= 0 && players[player]->isLocalPlayer() ) + messagePlayer(player, MESSAGE_STATUS, Language::get(6091)); + + // Shake the Host's screen + if ( myStats->HP <= 10 ) { - camera_shakex += .04; - camera_shakey += 5; + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + camera_shakex += .1; + camera_shakey += 10; + } + else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + // Shake the Client's screen + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 10; // turns into .1 + net_packet->data[5] = 10; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } } - else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + else { - // Shake the Client's screen - strcpy((char*)net_packet->data, "SHAK"); - net_packet->data[4] = 4; // turns into .1 - net_packet->data[5] = 5; - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + camera_shakex += .04; + camera_shakey += 5; + } + else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + // Shake the Client's screen + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 4; // turns into .1 + net_packet->data[5] = 5; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } } } } @@ -3942,7 +5001,7 @@ void Entity::handleEffects(Stat* myStats) } // "random" vomiting - if ( !this->char_gonnavomit && !myStats->EFFECTS[EFF_VOMITING] + if ( !this->char_gonnavomit && !myStats->getEffectActive(EFF_VOMITING) && this->entityCanVomit() ) { if ( myStats->HUNGER > 1500 && local_rng.rand() % 1000 == 0 ) @@ -3958,7 +5017,7 @@ void Entity::handleEffects(Stat* myStats) this->char_gonnavomit = 140 + local_rng.rand() % 60; } } - else if ( ticks % 60 == 0 && local_rng.rand() % 200 == 0 && myStats->EFFECTS[EFF_DRUNK] && myStats->type != GOATMAN ) + else if ( ticks % 60 == 0 && local_rng.rand() % 200 == 0 && myStats->getEffectActive(EFF_DRUNK) && myStats->type != GOATMAN ) { // drunkenness messagePlayer(player, MESSAGE_STATUS, Language::get(634)); @@ -3971,8 +5030,26 @@ void Entity::handleEffects(Stat* myStats) if ( this->char_gonnavomit == 0 && this->entityCanVomit() ) { messagePlayer(player, MESSAGE_STATUS, Language::get(635)); - myStats->EFFECTS[EFF_VOMITING] = true; + myStats->setEffectActive(EFF_VOMITING, 1); myStats->EFFECTS_TIMERS[EFF_VOMITING] = 50 + local_rng.rand() % 20; + + std::vector effectsToClear; + for ( int i = EFF_RATION_SPICY; i <= EFF_RATION_SWEET; ++i ) + { + effectsToClear.push_back(i); + } + effectsToClear.push_back(EFF_HP_MP_REGEN); + effectsToClear.push_back(EFF_BLESS_FOOD); + for ( auto effectID : effectsToClear ) + { + if ( myStats->getEffectActive(effectID) ) + { + myStats->clearEffect(effectID); + myStats->EFFECTS_TIMERS[effectID] = 0; + myStats->EFFECTS_ACCRETION_TIME[effectID] = 0; + } + } + serverUpdateEffects(player); if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -3999,7 +5076,7 @@ void Entity::handleEffects(Stat* myStats) } // vomiting - if ( myStats->EFFECTS[EFF_VOMITING] && ticks % 2 == 0 ) + if ( myStats->getEffectActive(EFF_VOMITING) && ticks % 2 == 0 ) { Entity* entity = spawnGib(this); if ( entity ) @@ -4079,13 +5156,32 @@ void Entity::handleEffects(Stat* myStats) } } } + + if ( myStats->type == GNOME ) + { + int chance = 0; + if ( behavior == &actPlayer ) + { + chance = 10 * players[skill[2]]->mechanics.getWealthTier(); + } + if ( chance > 0 ) + { + if ( local_rng.rand() % 100 < chance ) + { + if ( mpMod == 1 ) + { + mpMod += 1; + } + } + } + } + if ( int bonusFollowerRegen = getFollowerBonusHPRegen() ) { hpMod += abs(2 * bonusFollowerRegen); } // healing over time - int healring = 0; int healthRegenInterval = getHealthRegenInterval(this, *myStats, behavior == &actPlayer); bool naturalHeal = false; if ( healthRegenInterval >= 0 ) @@ -4122,7 +5218,98 @@ void Entity::handleEffects(Stat* myStats) } } - if ( myStats->EFFECTS[EFF_MARIGOLD] && (svFlags & SV_FLAG_HUNGER) + if ( myStats->getEffectActive(EFF_FOCI_LIGHT_PEACE) ) + { + int effectStrength = std::min(4, (int)myStats->getEffectActive(EFF_FOCI_LIGHT_PEACE)); + int interval = getSpellEffectDurationSecondaryFromID(SPELL_FOCI_LIGHT_PEACE, nullptr, nullptr, nullptr); + interval -= (effectStrength - 1) * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_PEACE, nullptr, nullptr, nullptr); + interval = std::max(1, interval); + if ( ((myStats->EFFECTS_ACCRETION_TIME[EFF_FOCI_LIGHT_PEACE]) % interval == 0) ) + { + Sint32 oldHP = myStats->HP; + this->modHP(1); + naturalHeal = true; + if ( behavior == &actPlayer ) + { + if ( oldHP < myStats->HP ) + { + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_HP_RUN, "rgn", myStats->HP - oldHP); + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_HP_SUM, "rgn", myStats->HP - oldHP); + } + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_HP_RATE_MAX, "rgn", healthRegenInterval); + } + } + } + + if ( myStats->getEffectActive(EFF_FOCI_LIGHT_PURITY) ) + { + int effectStrength = std::min(4, (int)myStats->getEffectActive(EFF_FOCI_LIGHT_PURITY)); + int interval = getSpellEffectDurationSecondaryFromID(SPELL_FOCI_LIGHT_PURITY, nullptr, nullptr, nullptr); + interval -= (effectStrength - 1) * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_PURITY, nullptr, nullptr, nullptr); + interval = std::max(1, interval); + + if ( ((myStats->EFFECTS_ACCRETION_TIME[EFF_FOCI_LIGHT_PURITY]) % interval == 0) ) + { + std::vector effectsToTry; + for ( int i = 0; i < NUMEFFECTS; ++i ) + { + if ( myStats->getEffectActive(i) && myStats->statusEffectRemovedByCureAilment(i, this) ) + { + effectsToTry.push_back(i); + } + } + if ( myStats->getEffectActive(EFF_WITHDRAWAL) ) + { + effectsToTry.push_back(EFF_WITHDRAWAL); + } + if ( flags[BURNING] ) + { + effectsToTry.push_back(StatusEffectQueue_t::kEffectBurning); // special case + } + + if ( effectsToTry.size() ) + { + int numToCure = effectStrength; + bool cured = false; + while ( effectsToTry.size() && numToCure > 0 ) + { + unsigned int pick = local_rng.rand() % effectsToTry.size(); + int effect = effectsToTry[pick]; + if ( effect == StatusEffectQueue_t::kEffectBurning ) + { + this->flags[BURNING] = false; + serverUpdateEntityFlag(this, BURNING); + } + else if ( effect == EFF_WITHDRAWAL ) + { + this->setEffect(EFF_WITHDRAWAL, false, EFFECT_WITHDRAWAL_BASE_TIME, true); + serverUpdatePlayerGameplayStats(player, STATISTICS_FUNCTIONAL, 1); + } + else + { + myStats->clearEffect(effect); + if ( myStats->EFFECTS_TIMERS[effect] > 0 ) + { + myStats->EFFECTS_TIMERS[effect] = 1; + } + } + cured = true; + --numToCure; + effectsToTry.erase(effectsToTry.begin() + pick); + } + + if ( cured ) + { + playSoundEntity(this, 168, 128); + spawnMagicEffectParticles(this->x, this->y, this->z, 169); + serverUpdateEffects(player); + messagePlayerColor(player, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(411)); + } + } + } + } + + if ( myStats->getEffectActive(EFF_MARIGOLD) && (svFlags & SV_FLAG_HUNGER) && myStats->EFFECTS_TIMERS[EFF_MARIGOLD] > 0 ) { if ( behavior == &actPlayer ) @@ -4157,20 +5344,65 @@ void Entity::handleEffects(Stat* myStats) } } + if ( myStats->getEffectActive(EFF_HP_MP_REGEN) && (svFlags & SV_FLAG_HUNGER) + && myStats->EFFECTS_TIMERS[EFF_HP_MP_REGEN] > 0 ) + { + if ( behavior == &actPlayer ) + { + int foodMod = 5; + int interval = std::max(1, (30 * TICKS_PER_SECOND) / foodMod); + if ( (myStats->EFFECTS_TIMERS[EFF_HP_MP_REGEN] + 1) % interval == 0 ) + { + if ( myStats->HP < myStats->MAXHP ) + { + Sint32 oldHP = myStats->HP; + this->modHP(1); + naturalHeal = true; + if ( behavior == &actPlayer ) + { + if ( oldHP < myStats->HP ) + { + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_HP_RUN, "rgn", myStats->HP - oldHP); + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_HP_SUM, "rgn", myStats->HP - oldHP); + } + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_HP_RATE_MAX, "rgn", healthRegenInterval); + } + } + } + } + } + // random teleportation if ( myStats->ring != NULL ) { - if ( myStats->ring->type == RING_TELEPORTATION ) + if ( myStats->ring->type == RING_TELEPORTATION ) + { + if ( local_rng.rand() % 1000 == 0 ) // .1% chance every frame + { + teleportRandom(); + } + } + } + + // regaining energy over time + if ( player >= 0 + && ((myStats->playerRace == RACE_SALAMANDER + && myStats->stat_appearance == 0) + || myStats->type == SALAMANDER) + && (myStats->getEffectActive(EFF_SALAMANDER_HEART) == 1 || myStats->getEffectActive(EFF_SALAMANDER_HEART) == 2) ) + { + this->char_energize++; + if ( this->char_energize >= TICKS_PER_SECOND * 3 ) { - if ( local_rng.rand() % 1000 == 0 ) // .1% chance every frame + this->char_energize = 0; + int decrease = std::max(1, myStats->MAXMP / 50); + if ( !this->safeConsumeMP(decrease) ) { - teleportRandom(); + this->modMP(-decrease); } } } - - // regaining energy over time - if ( myStats->type == AUTOMATON && player >= 0 ) + else if ( myStats->type == AUTOMATON && player >= 0 ) { int manaRegenInterval = Entity::getManaRegenInterval(this, *myStats, behavior == &actPlayer); this->char_energize++; @@ -4270,111 +5502,731 @@ void Entity::handleEffects(Stat* myStats) this->modMP(1); this->char_energize = 0; } - else if ( difference > 4 ) + else if ( difference > 4 ) + { + if ( this->char_energize >= 10 ) + { + this->modMP(1); + this->char_energize = 0; + } + } + else + { + if ( this->char_energize >= 15 ) + { + this->modMP(1); + this->char_energize = 0; + } + } + } + else + { + int difference = Sint32expectedMana - myStats->MP; + if ( this->char_energize % 50 == 0 ) // only update clients every 1 second. + { + this->modMP(std::min(difference, 5)); // jump by max of 5. + this->char_energize = 0; + } + } + } + else if ( myStats->MP > Sint32expectedMana ) + { + if ( this->char_energize % 50 == 0 ) + { + this->modMP(-1); // update MP decrease every second. + this->char_energize = 0; + } + } + else + { + this->char_energize = 0; + } + } + } + } + else if ( myStats->MP < myStats->MAXMP ) + { + int manaRegenInterval = Entity::getManaRegenInterval(this, *myStats, behavior == &actPlayer); + // summons don't regen MP. we use this to refund mana to the caster. + bool doManaRegen = true; + if ( this->behavior == &actMonster && this->monsterAllySummonRank != 0 ) + { + doManaRegen = false; + } + + if ( doManaRegen ) + { + this->char_energize++; + + if ( this->char_energize >= manaRegenInterval ) + { + this->char_energize = 0; + + if ( player >= 0 + && ((myStats->playerRace == RACE_SALAMANDER + && myStats->stat_appearance == 0) + || myStats->type == SALAMANDER) + && (myStats->getEffectActive(EFF_SALAMANDER_HEART) == 1 + || myStats->getEffectActive(EFF_SALAMANDER_HEART) == 2) ) + { + + } + else if ( mpMod > 0 ) + { + bool naturalManaRegen = true; + if ( player >= 0 + && ((myStats->playerRace == RACE_SALAMANDER + && myStats->stat_appearance == 0) + || myStats->type == SALAMANDER) ) + { + if ( myStats->MP >= myStats->MAXMP / 2 ) + { + naturalManaRegen = false; + } + } + + if ( naturalManaRegen ) + { + Sint32 oldMP = myStats->MP; + this->modMP(mpMod); + if ( behavior == &actPlayer ) + { + if ( oldMP < myStats->MP ) + { + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_RUN, "rgn", myStats->MP - oldMP); + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_SUM, "rgn", myStats->MP - oldMP); + } + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_RATE_MAX, "rgn", manaRegenInterval); + } + } + } + } + + if ( myStats->getEffectActive(EFF_HP_MP_REGEN) + && myStats->EFFECTS_TIMERS[EFF_HP_MP_REGEN] > 0 ) + { + if ( behavior == &actPlayer ) + { + int foodMod = 5; + int interval = std::max(1, (30 * TICKS_PER_SECOND) / foodMod); + if ( (myStats->EFFECTS_TIMERS[EFF_HP_MP_REGEN] + 1) % interval == 0 ) + { + if ( myStats->MP < myStats->MAXMP ) + { + Sint32 oldMP = myStats->MP; + int mpAmount = this->modMP(mpMod); + this->playerInsectoidIncrementHungerToMP(mpAmount); + if ( behavior == &actPlayer ) + { + if ( oldMP < myStats->MP ) + { + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_RUN, "rgn", myStats->MP - oldMP); + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_SUM, "rgn", myStats->MP - oldMP); + } + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_RATE_MAX, "rgn", manaRegenInterval); + } + } + } + } + } + } + else + { + this->char_energize = 0; + } + } + else + { + this->char_energize = 0; + } + + // effects of greasy fingers + if ( myStats->getEffectActive(EFF_GREASY) ) + { + // add some weird timing so it doesn't auto drop out of your hand immediately. + // intended to fix multiplayer duplication. + if ( ticks % 70 == 0 || ticks % 130 == 0 ) + { + if ( behavior == &actMonster && myStats->HP > 0 ) + { + if ( myStats->weapon != NULL && itemCategory(myStats->weapon) != SPELLBOOK ) + { + //messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(636)); + if ( Entity* dropped = dropItemMonster(myStats->weapon, this, myStats) ) + { + dropped->itemDelayMonsterPickingUp = TICKS_PER_SECOND * 2; + } + } + } + } + } + + int previousEnsemblePlaying = -1; + bool holySymbolCharging = false; + bool darkIconCharging = false; + bool fociCharging = false; + if ( behavior == &actPlayer ) + { + previousEnsemblePlaying = players[player]->mechanics.ensemblePlaying; + players[player]->mechanics.ensemblePlaying = -1; + if ( !myStats->defending ) + { + if ( players[player]->mechanics.ensembleRequireRecast ) + { + players[player]->mechanics.ensembleRequireRecast = false; + } + players[player]->mechanics.ensembleTakenInitialMP = false; + } + } + + if ( myStats->shield != NULL ) + { + if ( behavior == &actPlayer && (effectShapeshift == NOTHING || effectShapeshift == CREATURE_IMP) ) + { + if ( myStats->defending && players[player]->mechanics.defendTicks > 0 ) + { + if ( itemTypeIsFoci(myStats->shield->type) ) + { + int spellID = getSpellIDFromFoci(myStats->shield->type); + if ( spellID > SPELL_NONE ) + { + Uint32 defendTime = ::ticks - players[player]->mechanics.defendTicks; + if ( spellID == SPELL_FOCI_LIGHT_JUSTICE + || spellID == SPELL_FOCI_LIGHT_PEACE + || spellID == SPELL_FOCI_LIGHT_PROVIDENCE + || spellID == SPELL_FOCI_LIGHT_PURITY + || spellID == SPELL_FOCI_LIGHT_SANCTUARY ) + { + holySymbolCharging = true; + } + else if ( spellID == SPELL_FOCI_DARK_LIFE + || spellID == SPELL_FOCI_DARK_RIFT + || spellID == SPELL_FOCI_DARK_SILENCE + || spellID == SPELL_FOCI_DARK_SUPPRESS + || spellID == SPELL_FOCI_DARK_VENGEANCE ) + { + darkIconCharging = true; + } + fociCharging = true; + if ( players[player]->mechanics.lastFociHeldType != myStats->shield->type ) + { + players[player]->mechanics.fociDarkChargeTime = 0; + players[player]->mechanics.fociHolyChargeTime = 0; + } + players[player]->mechanics.lastFociHeldType = myStats->shield->type; + + static ConsoleVariable cvar_foci_charge_init("/foci_charge_init", 1.f); + int chargeTimeInit = (float)(TICKS_PER_SECOND / 4); + if ( svFlags & SV_FLAG_CHEATS ) + { + chargeTimeInit *= *cvar_foci_charge_init; + } + chargeTimeInit *= getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_FOCI_CAST_TIME, spellID, + this, myStats, this); + if ( defendTime >= chargeTimeInit ) + { + if ( defendTime == chargeTimeInit ) + { + if ( holySymbolCharging ) + { + playSoundEntity(this, 167, 128); // casting sound + createParticleFociLight(this, spellID, true); + } + else if ( darkIconCharging ) + { + playSoundEntity(this, 167, 128); // casting sound + } + } + + static ConsoleVariable cvar_foci_charge("/foci_charge", 1.f); + int chargeTime = getSpellPropertyFromID(spell_t::SPELLPROP_FOCI_REFIRE_TICKS, spellID, + this, myStats, this); + if ( svFlags & SV_FLAG_CHEATS ) + { + chargeTime *= *cvar_foci_charge; + } + + if ( (defendTime - chargeTimeInit) % chargeTime == 0 ) + { + if ( defendTime - chargeTimeInit > 0 ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CHARGING_TIME, "offhand casting", chargeTime); + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CHARGING_TIME_RUN, "offhand casting", chargeTime); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_OFFHAND_CHARGING_TIME, myStats->shield->type, chargeTime); + } + + if ( auto spell = getSpellFromID(spellID) ) + { + int mpcost = getCostOfSpell(spell, this); + int bless = shouldInvertEquipmentBeatitude(myStats) ? + abs(myStats->shield->beatitude) : + myStats->shield->beatitude; + if ( bless < 0 ) + { + mpcost += abs(bless) * mpcost; + } + + if ( (defendTime - chargeTimeInit) > 0 ) + { + // subsequent casts + mpcost = getSpellPropertyFromID(spell_t::SPELLPROP_FOCI_SECONDARY_MANA_COST, spellID, + this, myStats, this); + + if ( bless < 0 ) + { + mpcost += abs(bless) * mpcost; + } + + int tier = bless > 0 ? std::min(2, bless) : 0; + int cycle = (defendTime - chargeTimeInit) / chargeTime; + Sint32 CHR = statGetCHR(myStats, this); + if ( CHR > 0 ) + { + if ( CHR >= 3 && CHR <= 7 ) + { + tier += 1; + } + else if ( CHR >= 8 && CHR <= 14 ) + { + tier += 2; + } + else if ( CHR >= 15 && CHR <= 29 ) + { + tier += 3; + } + else if ( CHR >= 30 && CHR <= 59 ) + { + tier += 4; + } + else if ( CHR >= 60 ) + { + tier += 5; + } + } + + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CHR_MAX, "offhand casting", CHR); + + tier = std::min(tier, 5); + + switch ( tier ) + { + case 0: + default: + break; + case 1: + if ( cycle % 4 == 0 ) + { + mpcost = 0; + } + break; + case 2: + if ( cycle % 3 == 0 ) + { + mpcost = 0; + } + break; + case 3: + if ( cycle % 2 == 0 ) + { + mpcost = 0; + } + break; + case 4: + if ( cycle % 3 != 0 ) + { + mpcost = 0; + } + break; + case 5: + if ( cycle % 4 != 0 ) + { + mpcost = 0; + } + break; + } + } + + bool failedCast = false; + if ( this->safeConsumeMP(mpcost) || mpcost == 0 ) + { + if ( mpcost > 0 ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CASTING_MP, "offhand casting", mpcost); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_OFFHAND_CASTING_MP, myStats->shield->type, mpcost); + + if ( spell->skillID == PRO_SORCERY ) + { + if ( player >= 0 ) + { + achievementObserver.playerAchievements[player].sourceEngine += mpcost; + } + } + } + + CastSpellProps_t props; + if ( spell->ID == SPELL_FOCI_LIGHT_JUSTICE + || spell->ID == SPELL_FOCI_LIGHT_PEACE + || spell->ID == SPELL_FOCI_LIGHT_PROVIDENCE + || spell->ID == SPELL_FOCI_LIGHT_PURITY + || spell->ID == SPELL_FOCI_LIGHT_SANCTUARY ) + { + players[player]->mechanics.fociHolyChargeTime++; + if ( players[player]->mechanics.fociHolyChargeTime == 32 ) + { + messagePlayer(this->isEntityPlayer(), MESSAGE_INTERACTION, Language::get(6922)); + } + players[player]->mechanics.fociHolyChargeTime = std::min(32, players[player]->mechanics.fociHolyChargeTime); + props.optionalData = players[player]->mechanics.fociHolyChargeTime; + } + else if ( spellID == SPELL_FOCI_DARK_LIFE + || spellID == SPELL_FOCI_DARK_RIFT + || spellID == SPELL_FOCI_DARK_SILENCE + || spellID == SPELL_FOCI_DARK_SUPPRESS + || spellID == SPELL_FOCI_DARK_VENGEANCE ) + { + players[player]->mechanics.fociDarkChargeTime++; + if ( players[player]->mechanics.fociDarkChargeTime == 32 ) + { + messagePlayer(this->isEntityPlayer(), MESSAGE_INTERACTION, Language::get(6923)); + } + players[player]->mechanics.fociDarkChargeTime = std::min(32, players[player]->mechanics.fociDarkChargeTime); + props.optionalData = players[player]->mechanics.fociDarkChargeTime; + createParticleFociDark(this, spellID, true); + } + + if ( spellID == SPELL_FOCI_DARK_LIFE + || spellID == SPELL_FOCI_DARK_RIFT + || spellID == SPELL_FOCI_DARK_SILENCE + || spellID == SPELL_FOCI_DARK_SUPPRESS + || spellID == SPELL_FOCI_DARK_VENGEANCE ) + { + // no cast yet + } + else + { + castSpell(this->getUID(), spell, false, false, false, &props, true); + } + } + else + { + failedCast = true; + } + + if ( failedCast ) + { + if ( players[player]->isLocalPlayer() ) + { + messagePlayer(player, MESSAGE_MISC, Language::get(375)); + playSound(563, 64); + if ( players[player]->magic.noManaProcessedOnTick == 0 ) + { + players[player]->magic.flashNoMana(); + } + + Input& input = Input::inputs[player]; + if ( input.binaryToggle("Defend") ) + { + input.consumeBinaryToggle("Defend"); + } + } + else if ( multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "NOMP"); + SDLNet_Write32((Uint32)myStats->shield->type, &net_packet->data[4]); // force stop defending with shield + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + } + } + } + } + } + } + else if ( itemTypeIsInstrument(myStats->shield->type) ) + { + int chargeTimeInit = (float)(TICKS_PER_SECOND / 4); + Uint32 defendTime = ::ticks - players[player]->mechanics.defendTicks; + if ( defendTime >= chargeTimeInit ) + { + Sint32 CHR = statGetCHR(myStats, this); + int chargeTime = 4 * TICKS_PER_SECOND; // 4 sec base, -0.Xs per CHR + chargeTime = std::max(TICKS_PER_SECOND / 2, + chargeTime - (TICKS_PER_SECOND / 20) * CHR); + + int effect = -1; + int mpcost = 2; + switch ( myStats->shield->type ) + { + case INSTRUMENT_DRUM: + effect = EFF_ENSEMBLE_DRUM; + mpcost = 4; + break; + case INSTRUMENT_FLUTE: + effect = EFF_ENSEMBLE_FLUTE; + mpcost = 3; + break; + case INSTRUMENT_LUTE: + effect = EFF_ENSEMBLE_LUTE; + mpcost = 4; + break; + case INSTRUMENT_LYRE: + effect = EFF_ENSEMBLE_LYRE; + mpcost = 2; + break; + case INSTRUMENT_HORN: + effect = EFF_ENSEMBLE_HORN; + mpcost = 6; + break; + default: + break; + } + bool failedCast = false; + if ( players[player]->mechanics.ensembleRequireRecast ) { - if ( this->char_energize >= 10 ) + if ( (effect >= 0 && previousEnsemblePlaying >= 0 && previousEnsemblePlaying != effect) ) { - this->modMP(1); - this->char_energize = 0; + failedCast = true; } + effect = -1; } - else + else if ( defendTime == chargeTimeInit || (effect >= 0 && previousEnsemblePlaying >= 0 && previousEnsemblePlaying != effect) ) { - if ( this->char_energize >= 15 ) + // initial cast, or swapping equipped shield + bool takeMP = true; + /*if ( defendTime == chargeTimeInit ) { - this->modMP(1); - this->char_energize = 0; + if ( myStats->getEffectActive(effect) ) + { + takeMP = false; + } + }*/ + + if ( takeMP && !this->safeConsumeMP(mpcost) ) + { + effect = -1; + players[player]->mechanics.ensembleRequireRecast = true; + failedCast = true; + } + else + { + if ( takeMP ) + { + players[player]->mechanics.ensembleTakenInitialMP = true; + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CASTING_MP, "offhand casting", mpcost); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_OFFHAND_CASTING_MP, myStats->shield->type, mpcost); + } + if ( (effect >= 0 && previousEnsemblePlaying >= 0 && previousEnsemblePlaying != effect) ) + { + // swapping equipped shield, reset the defend ticks + players[player]->mechanics.defendTicks = ::ticks - chargeTimeInit; + defendTime = ::ticks - players[player]->mechanics.defendTicks; + } } } - } - else - { - int difference = Sint32expectedMana - myStats->MP; - if ( this->char_energize % 50 == 0 ) // only update clients every 1 second. + else if ( defendTime >= (chargeTime + chargeTimeInit) + && ((defendTime - (chargeTime + chargeTimeInit)) % chargeTime == 0) ) { - this->modMP(std::min(difference, 5)); // jump by max of 5. - this->char_energize = 0; + if ( !this->safeConsumeMP(mpcost) ) + { + effect = -1; + players[player]->mechanics.ensembleRequireRecast = true; + failedCast = true; + } + else + { + players[player]->mechanics.ensembleTakenInitialMP = true; + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CASTING_MP, "offhand casting", mpcost); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_OFFHAND_CASTING_MP, myStats->shield->type, mpcost); + } + + if ( defendTime - chargeTimeInit > 0 ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CHARGING_TIME, "offhand casting", chargeTime); + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CHARGING_TIME_RUN, "offhand casting", chargeTime); + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_OFFHAND_CHR_MAX, "offhand casting", CHR); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_OFFHAND_CHARGING_TIME, myStats->shield->type, chargeTime); + } } - } - } - else if ( myStats->MP > Sint32expectedMana ) - { - if ( this->char_energize % 50 == 0 ) - { - this->modMP(-1); // update MP decrease every second. - this->char_energize = 0; - } - } - else - { - this->char_energize = 0; - } - } - } - } - else if ( myStats->MP < myStats->MAXMP ) - { - int manaRegenInterval = Entity::getManaRegenInterval(this, *myStats, behavior == &actPlayer); - // summons don't regen MP. we use this to refund mana to the caster. - bool doManaRegen = true; - if ( this->behavior == &actMonster && this->monsterAllySummonRank != 0 ) - { - doManaRegen = false; - } - if ( doManaRegen ) - { - this->char_energize++; - if ( this->char_energize >= manaRegenInterval ) - { - this->char_energize = 0; - if ( mpMod > 0 ) - { - Sint32 oldMP = myStats->MP; - this->modMP(mpMod); - if ( behavior == &actPlayer ) - { - if ( oldMP < myStats->MP ) + if ( failedCast ) { - Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_RUN, "rgn", myStats->MP - oldMP); - Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_SUM, "rgn", myStats->MP - oldMP); + if ( players[player]->isLocalPlayer() ) + { + messagePlayer(player, MESSAGE_MISC, Language::get(375)); + playSound(563, 64); + if ( players[player]->magic.noManaProcessedOnTick == 0 ) + { + players[player]->magic.flashNoMana(); + } + + Input& input = Input::inputs[player]; + if ( input.binaryToggle("Defend") ) + { + input.consumeBinaryToggle("Defend"); + } + } + else if ( multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "NOMP"); + SDLNet_Write32((Uint32)myStats->shield->type, &net_packet->data[4]); // force stop defending with shield + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } } - Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_RATE_MAX, "rgn", manaRegenInterval); - } - } - } - } - else - { - this->char_energize = 0; - } - } - else - { - this->char_energize = 0; - } + if ( effect >= 0 && players[player]->mechanics.ensembleTakenInitialMP ) + { + players[player]->mechanics.ensemblePlaying = effect; + int skillLVL = std::max(0, myStats->getModifiedProficiency(PRO_APPRAISAL)); + int durationMinimum = TICKS_PER_SECOND; + durationMinimum += (skillLVL / 20) * TICKS_PER_SECOND; + int duration = 1; - // effects of greasy fingers - if ( myStats->EFFECTS[EFF_GREASY] == true ) - { - // add some weird timing so it doesn't auto drop out of your hand immediately. - // intended to fix multiplayer duplication. - if ( ticks % 70 == 0 || ticks % 130 == 0 ) - { - if ( behavior == &actMonster && myStats->HP > 0 ) - { - if ( myStats->weapon != NULL && itemCategory(myStats->weapon) != SPELLBOOK ) - { - //messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(636)); - dropItemMonster(myStats->weapon, this, myStats); + bool guarantee = (ticks % (TICKS_PER_SECOND / 2) == 0); + bool doParticle = false; + int castCycle = 1; + if ( defendTime >= (chargeTime + chargeTimeInit) ) + { + if ( (defendTime - (chargeTime + chargeTimeInit)) % chargeTime == 0 ) + { + duration += 4 * skillLVL * (TICKS_PER_SECOND / 25); + duration += (skillLVL / 20) * TICKS_PER_SECOND; + guarantee = true; + createEnsembleHUDParticleCircling(this); + doParticle = true; + castCycle += 1 + (defendTime - (chargeTime + chargeTimeInit)) / chargeTime; + if ( behavior == &actPlayer && !players[skill[2]]->isLocalPlayer() ) + { + serverSpawnMiscParticles(this, PARTICLE_EFFECT_ENSEMBLE_SELF_CAST, 0); + } + } + } + + if ( !myStats->getEffectActive(effect) ) + { + createEnsembleHUDParticleCircling(this); + if ( behavior == &actPlayer && !players[skill[2]]->isLocalPlayer() ) + { + serverSpawnMiscParticles(this, PARTICLE_EFFECT_ENSEMBLE_SELF_CAST, 0); + } + } + + static ConsoleVariable cvar_ensemble_debug_length("/ensemble_debug_length", false); + + Uint8 effectStrength = std::max(1, std::min(255, CHR + 1)); + if ( castCycle == 1 ) + { + effectStrength = 1; // no accumulated song, or between MP ticks + } + else + { + effectStrength = std::max(1, std::min(static_cast(effectStrength), (1 + 5 * (castCycle - 1)))); + } + + for ( node_t* node = map.creatures->first; node && effect >= 0; node = node->next ) + { + if ( Entity* entity = (Entity*)(node->element) ) + { + if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) + { + continue; + } + if ( Stat* entitystats = entity->getStats() ) + { + if ( entity == this || (entityDist(entity, this) <= HEAL_RADIUS && entity->checkFriend(this)) ) + { + bool wasActive = entitystats->getEffectActive(effect) > 0; + bool effectIncrease = effectStrength > entitystats->getEffectActive(effect); + + int durationTotal = entitystats->EFFECTS_TIMERS[effect] + duration; + static ConsoleVariable cvar_ensemble_max_length("/ensemble_max_length", (TICKS_PER_SECOND * 60 * 5)); + int durationMax = !(svFlags & SV_FLAG_CHEATS) ? (TICKS_PER_SECOND * 60 * 5) : *cvar_ensemble_max_length; + + if ( entity == this && durationTotal > durationMax + 5 ) + { + if ( entity->behavior == &actPlayer ) + { + messagePlayer(skill[2], MESSAGE_INTERACTION, Language::get(6431)); + } + } + durationTotal = std::min(durationTotal, durationMax); + durationTotal = std::max(durationMinimum, durationTotal); + + if ( duration > 1 || castCycle > 1 ) + { + if ( entity == this && (svFlags & SV_FLAG_CHEATS) ) + { + if ( *cvar_ensemble_debug_length ) + { + if ( entity->behavior == &actPlayer ) + { + Uint32 sec = (durationTotal / TICKS_PER_SECOND) % 60; + Uint32 min = ((durationTotal / TICKS_PER_SECOND) / 60) % 60; + Uint32 hour = ((durationTotal / TICKS_PER_SECOND) / 60) / 60; + float sec2 = fmod((duration / (float)TICKS_PER_SECOND), 60); + messagePlayer(skill[2], MESSAGE_INTERACTION, "Song Duration: %02dm:%02ds (+ by %.2fs)", min, sec, sec2); + } + } + } + } + + + if ( entity->behavior == &actPlayer ) + { + if ( guarantee || !wasActive || effectIncrease ) + { + entity->setEffect(effect, effectStrength, durationTotal, false); + } + else + { + // don't update clients all the time with setEffect + entitystats->setEffectActive(effect, effectStrength); + entitystats->EFFECTS_TIMERS[effect] = durationTotal; + } + } + else + { + entity->setEffect(effect, effectStrength, durationTotal, false); + } + + if ( entitystats->getEffectActive(effect) ) + { + if ( !wasActive ) + { + //spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); + if ( entity->behavior == &actPlayer ) + { + playSoundEntity(entity, 168, 64); + } + //if ( entity != this ) + { + createEnsembleTargetParticleCircling(entity); + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_ENSEMBLE_OTHER_CAST, 0); + } + } + else if ( doParticle ) + { + //if ( entity != this ) + { + createEnsembleTargetParticleCircling(entity); + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_ENSEMBLE_OTHER_CAST, 0); + } + } + } + } + } + } + } + } + } } } } - } - // torches/lamps burn down - if ( myStats->shield != NULL ) - { + // torches/lamps burn down if ( myStats->shield->type == TOOL_TORCH || myStats->shield->type == TOOL_LANTERN ) { this->char_torchtime++; @@ -4385,6 +6237,10 @@ void Entity::handleEffects(Stat* myStats) { // do nothing, shapeshifted } + else if ( spellEffectPreserveItem(myStats->shield) ) + { + // preserved + } else if ( myStats->shield->type == TOOL_TORCH && player >= 0 ) { std::string itemName = myStats->shield->getName(); @@ -4443,9 +6299,10 @@ void Entity::handleEffects(Stat* myStats) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 4; net_packet->data[5] = myStats->shield->status; + SDLNet_Write16((int)myStats->shield->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -4453,14 +6310,139 @@ void Entity::handleEffects(Stat* myStats) } } + if ( behavior == &actPlayer ) + { + if ( !holySymbolCharging && players[player]->mechanics.fociHolyChargeTime > 0 ) + { + int chargeTime = players[player]->mechanics.fociHolyChargeTime; + players[player]->mechanics.fociHolyChargeTime = 0; + if ( myStats->shield != NULL && (effectShapeshift == NOTHING || effectShapeshift == CREATURE_IMP) ) + { + if ( itemTypeIsFoci(myStats->shield->type) ) + { + if ( myStats->shield->type == players[player]->mechanics.lastFociHeldType ) + { + if ( auto spellID = getSpellIDFromFoci(myStats->shield->type) ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + CastSpellProps_t props; + props.optionalData = std::min(127, chargeTime); + props.optionalData |= (1 << 7); + castSpell(this->getUID(), spell, false, false, false, &props, true); + } + } + } + } + } + } + if ( !darkIconCharging && players[player]->mechanics.fociDarkChargeTime > 0 ) + { + int chargeTime = players[player]->mechanics.fociDarkChargeTime; + players[player]->mechanics.fociDarkChargeTime = 0; + if ( myStats->shield != NULL && (effectShapeshift == NOTHING || effectShapeshift == CREATURE_IMP) ) + { + if ( itemTypeIsFoci(myStats->shield->type) ) + { + if ( myStats->shield->type == players[player]->mechanics.lastFociHeldType ) + { + if ( auto spellID = getSpellIDFromFoci(myStats->shield->type) ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + CastSpellProps_t props; + props.optionalData = std::min(127, chargeTime); + props.optionalData |= (1 << 7); + castSpell(this->getUID(), spell, false, false, false, &props, true); + } + } + } + } + } + } + if ( !fociCharging ) + { + players[player]->mechanics.lastFociHeldType = 0; + } + } + + if ( myStats->getEffectActive(EFF_SALAMANDER_HEART) && myStats->type != SALAMANDER ) + { + this->setEffect(EFF_SALAMANDER_HEART, false, 0, true, true, true); + } + if ( behavior == &actPlayer && myStats->type == SALAMANDER ) + { + Uint8 effectStrength = myStats->getEffectActive(EFF_SALAMANDER_HEART); + int particle = 0; + // check stone form + if ( myStats->MP <= myStats->MAXMP * 0.25 && effectStrength <= 2 ) + { + // radiant stone + this->setEffect(EFF_SALAMANDER_HEART, (Uint8)4, 5 * TICKS_PER_SECOND, true, true, true); + messagePlayerColor(skill[2], MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6919)); + playSoundEntity(this, 826, 128); + //createParticleDropRising(this, 593, 1.f); + //serverSpawnMiscParticles(this, PARTICLE_EFFECT_RISING_DROP, 593); + particle = 1; + } + else if ( effectStrength == 3 ) + { + if ( myStats->MP >= myStats->MAXMP * 0.4 ) + { + this->setEffect(EFF_SALAMANDER_HEART, false, 0, true, true, true); + messagePlayerColor(skill[2], MESSAGE_STATUS, makeColorRGB(255, 255, 255), Language::get(6920)); + playSoundEntity(this, 827, 128); + particle = 2; + } + } + else if ( effectStrength == 1 || effectStrength == 2 ) + { + if ( myStats->MP <= myStats->MAXMP * 0.6 ) + { + this->setEffect(EFF_SALAMANDER_HEART, false, 0, true, true, true); + messagePlayerColor(skill[2], MESSAGE_STATUS, makeColorRGB(255, 255, 255), Language::get(6920)); + playSoundEntity(this, 827, 128); + particle = 2; + } + } + + if ( particle ) + { + for ( int i = 0; i < 2; ++i ) + { + if ( Entity* fx = createParticleAestheticOrbit(this, 263, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_HEAT_ORBIT_SPIN) ) + { + fx->flags[SPRITE] = true; + fx->x = this->x; + fx->y = this->y; + fx->z = 7.5; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->vel_z = -0.5; + fx->actmagicOrbitDist = 5; + fx->fskill[2] = this->yaw + PI / 4.0 + i * PI; + fx->yaw = fx->fskill[2]; + fx->fskill[4] = 0.25; + if ( particle == 1 ) + { + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + fx->actmagicNoLight = 1; + } + + serverSpawnMiscParticles(this, PARTICLE_EFFECT_HEAT_ORBIT_SPIN, 263, particle, fx->skill[0]); + } + } + } + } + // effects of being poisoned - if ( myStats->EFFECTS[EFF_POISONED] ) + if ( myStats->getEffectActive(EFF_POISONED) ) { if ( myStats->type == INSECTOID ) { messagePlayer(player, MESSAGE_STATUS, Language::get(640)); myStats->EFFECTS_TIMERS[EFF_POISONED] = 0; - myStats->EFFECTS[EFF_POISONED] = false; + myStats->clearEffect(EFF_POISONED); serverUpdateEffects(player); this->char_poison = 0; } @@ -4469,13 +6451,65 @@ void Entity::handleEffects(Stat* myStats) messagePlayer(player, MESSAGE_EQUIPMENT | MESSAGE_HINT, Language::get(639)); messagePlayer(player, MESSAGE_STATUS, Language::get(640)); myStats->EFFECTS_TIMERS[EFF_POISONED] = 0; - myStats->EFFECTS[EFF_POISONED] = false; + myStats->clearEffect(EFF_POISONED); serverUpdateEffects(player); this->char_poison = 0; + + this->degradeAmuletProc(myStats, AMULET_POISONRESISTANCE); + } + + this->char_poison++; + int poisonDamageBonus = 0; + if ( myStats->poisonKiller != getUID() ) + { + Entity* poisonKillerInflicted = uidToEntity(myStats->poisonKiller); + if ( poisonKillerInflicted && poisonKillerInflicted->behavior == &actPlayer + && stats[poisonKillerInflicted->skill[2]]->type == MYCONID + && stats[poisonKillerInflicted->skill[2]]->getEffectActive(EFF_GROWTH) >= 2 ) + { + Uint8 effectStrength = stats[poisonKillerInflicted->skill[2]]->getEffectActive(EFF_GROWTH); + if ( effectStrength == 2 ) + { + if ( ticks % 5 == 0 ) + { + this->char_poison++; + } + poisonDamageBonus += 1; + } + else if ( effectStrength == 3 ) + { + if ( ticks % 2 == 0 ) + { + this->char_poison++; + } + poisonDamageBonus += 2; + } + else if ( effectStrength == 4 ) + { + this->char_poison++; + poisonDamageBonus += 3; + } + } } - this->char_poison++; - if ( this->char_poison > 150 ) // three seconds + if ( myStats->getEffectActive(EFF_BLOOD_WARD) ) + { + if ( this->char_poison > 150 ) + { + messagePlayerColor(player, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6934)); + myStats->EFFECTS_TIMERS[EFF_POISONED] = 0; + myStats->clearEffect(EFF_POISONED); + serverUpdateEffects(player); + this->char_poison = 0; + + if ( behavior == &actPlayer ) + { + players[skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_BLOOD_WARD, 128.0, 1.0, nullptr); + this->safeConsumeMP(1); + } + } + } + else if ( this->char_poison > 150 ) // three seconds { this->char_poison = 0; int poisonhurt = std::max(3, (myStats->MAXHP / 20)); @@ -4486,6 +6520,7 @@ void Entity::handleEffects(Stat* myStats) { poisonhurt = std::min(poisonhurt, 15); // prevent doing 50+ dmg } + poisonhurt += poisonDamageBonus; if ( poisonhurt > 3 ) { poisonhurt -= local_rng.rand() % (std::max(1, poisonhurt / 4)); @@ -4511,7 +6546,7 @@ void Entity::handleEffects(Stat* myStats) } } } - if ( killer && killer->behavior == &actPlayer ) + if ( killer /*&& killer->behavior == &actPlayer*/ ) { bool lowPriority = true; // update enemy bar for attacker @@ -4556,7 +6591,7 @@ void Entity::handleEffects(Stat* myStats) { messagePlayer(player, MESSAGE_STATUS, Language::get(641)); myStats->EFFECTS_TIMERS[EFF_POISONED] = 0; - myStats->EFFECTS[EFF_POISONED] = false; + myStats->clearEffect(EFF_POISONED); serverUpdateEffects(player); } } @@ -4568,153 +6603,630 @@ void Entity::handleEffects(Stat* myStats) myStats->poisonKiller = 0; } - if ( !myStats->EFFECTS[EFF_WEBBED] ) - { - if ( creatureWebbedSlowCount > 0 ) + if ( !myStats->getEffectActive(EFF_WEBBED) ) + { + if ( creatureWebbedSlowCount > 0 ) + { + creatureWebbedSlowCount = 0; // reset counter. + if ( behavior == &actPlayer ) + { + serverUpdateEntitySkill(this, 52); // update player. + } + } + } + + // bleeding + if ( myStats->getEffectActive(EFF_BLEEDING) ) + { + if ( ticks % 120 == 0 ) + { + if ( myStats->getEffectActive(EFF_BLOOD_WARD) ) + { + messagePlayerColor(player, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6935)); + myStats->clearEffect(EFF_BLEEDING); + myStats->EFFECTS_TIMERS[EFF_BLEEDING] = 0; + serverUpdateEffects(player); + + if ( behavior == &actPlayer ) + { + players[skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_BLOOD_WARD, 128.0, 1.0, nullptr); + this->safeConsumeMP(1); + } + } + else if ( myStats->HP > 5 + (std::max(0, getCON())) ) // CON increases when bleeding stops. + { + int bleedhurt = 1 + myStats->MAXHP / 30; + if ( bleedhurt > 1 ) + { + bleedhurt -= local_rng.rand() % (std::max(1, bleedhurt / 2)); + } + if ( getCON() > 0 ) + { + bleedhurt -= (getCON() / 5); + } + if ( myStats->type == LICH_ICE + || myStats->type == LICH_FIRE + || myStats->type == LICH + || myStats->type == DEVIL ) + { + bleedhurt = std::min(bleedhurt, 15); // prevent doing 50+ dmg + } + bleedhurt = std::max(1, bleedhurt); + this->modHP(-bleedhurt); + this->setObituary(Language::get(1532)); + myStats->killer = KilledBy::BLEEDING; + Entity* gib = spawnGib(this); + serverSpawnGibForClient(gib); + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + camera_shakex -= .03; + camera_shakey += 3; + } + else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = -3; // turns into -.03 + net_packet->data[5] = 3; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + messagePlayer(player, MESSAGE_STATUS, Language::get(642)); + + { + Entity* entity = nullptr; + if ( spawn_blood && gibtype[myStats->type] == 1 ) + { + entity = newEntity(203, 1, map.entities, nullptr); //Blood entity. + } + else if ( gibtype[myStats->type] == 2 ) + { + entity = newEntity(213, 1, map.entities, nullptr); //Blood entity. + } + else if ( gibtype[myStats->type] == 4 ) + { + entity = newEntity(682, 1, map.entities, nullptr); //Blood entity. + } + if ( entity != NULL ) + { + entity->x = this->x; + entity->y = this->y; + entity->z = 8.0 + (local_rng.rand() % 20) / 100.0; + entity->parent = this->uid; + entity->sizex = 2; + entity->sizey = 2; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + } + } + + Entity* killer = uidToEntity(static_cast(myStats->bleedInflictedBy)); + if ( killer /*&& killer->behavior == &actPlayer*/ ) + { + bool lowPriority = true; + // update enemy bar for attacker + if ( !strcmp(myStats->name, "") ) + { + updateEnemyBar(killer, this, getMonsterLocalizedName(myStats->type).c_str(), myStats->HP, myStats->MAXHP, lowPriority, + DamageGib::DMG_BLEED); + } + else + { + updateEnemyBar(killer, this, myStats->name, myStats->HP, myStats->MAXHP, lowPriority, + DamageGib::DMG_BLEED); + } + } + } + else + { + messagePlayer(player, MESSAGE_STATUS, Language::get(643)); + myStats->clearEffect(EFF_BLEEDING); + myStats->EFFECTS_TIMERS[EFF_BLEEDING] = 0; + serverUpdateEffects(player); + } + } + } + else + { + myStats->bleedInflictedBy = 0; + } + + // webbed + if ( myStats->getEffectActive(EFF_WEBBED) ) + { + if ( ticks % 25 == 0 ) + { + Entity* gib = spawnGib(this, 863); + serverSpawnGibForClient(gib); + } + if ( ticks % 40 == 0 ) + { + int x = this->x / 16; + int y = this->y / 16; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + if ( map.tiles[0 + y * MAPLAYERS + x * MAPLAYERS * map.height] ) + { + Entity* entity = newEntity(862, 1, map.entities, nullptr); //Web pool entity. + if ( entity != NULL ) + { + entity->x = this->x; + entity->y = this->y; + entity->z = 8.0 + (local_rng.rand() % 20) / 100.0; + entity->parent = this->uid; + entity->sizex = 2; + entity->sizey = 2; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + real_t scale = 0.75 + 0.25 * (local_rng.rand() % 100) / 100.f; + entity->scalex = scale; + entity->scaley = scale; + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + } + } + } + } + } + + if ( myStats->getEffectActive(EFF_THORNS) ) + { + if ( ticks % 25 == 0 ) + { + if ( Entity* particle = createParticleAestheticOrbit(this, 262, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_THORNS_ORBIT) ) + { + particle->sizex = 8; + particle->sizey = 8; + particle->flags[GENIUS] = true; + particle->z += (local_rng.rand() % 8) * 0.25; + particle->flags[SPRITE] = true; + particle->yaw = this->yaw -(3 * PI / 8) + ((ticks / 25) % 4) * (2 / 4.0) * PI / 2; + //particle->flags[INVISIBLE] = true; + } + } + } + + if ( myStats->getEffectActive(EFF_CONFUSED) ) + { + if ( ticks % 25 == 0 ) + { + if ( !strncmp(map.name, "Sanctum", 7) && getMonsterTypeFromSprite() == AUTOMATON && !monsterAllyGetPlayerLeader() ) + { + // boss automatons + } + else if ( Entity* particle = createParticleAestheticOrbit(this, 2203, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_CONFUSE_ORBIT) ) + { + particle->yaw = 0.0; + particle->actmagicOrbitDist = 4; + particle->scalex = 0.05; + particle->scaley = 0.05; + particle->scalez = 0.05; + particle->z -= 4; + particle->z = std::max(-6.0, particle->z); + } + } + } + + bool stasisDither = flags[STASIS_DITHER]; + if ( myStats->getEffectActive(EFF_STASIS) ) + { + if ( ticks % 25 == 0 ) + { + if ( Entity* particle = createParticleAestheticOrbit(this, 2178, TICKS_PER_SECOND, PARTICLE_EFFECT_STASIS_RIFT_ORBIT) ) + { + particle->flags[INVISIBLE] = true; + } + } + flags[STASIS_DITHER] = true; + } + else + { + flags[STASIS_DITHER] = false; + } + if ( flags[STASIS_DITHER] != stasisDither ) + { + serverUpdateEntityFlag(this, STASIS_DITHER); + } + + if ( myStats->getEffectActive(EFF_STATIC) ) + { + int interval = 40; + if ( ticks % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 1758, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * ((ticks / interval) % 3); + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += ((ticks / interval) % 3) * 2 * PI / 3; + fx->actmagicNoLight = 1; + } + } + + if ( myStats->getEffectActive(EFF_MAXIMISE) ) + { + int interval = 40; + if ( (ticks + 20) % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 2335, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * ((ticks / interval) % 3); + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += ((ticks / interval) % 3) * 2 * PI / 3; + fx->actmagicNoLight = 1; + } + } + else if ( myStats->getEffectActive(EFF_MINIMISE) ) + { + int interval = 40; + if ( (ticks + 20) % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 2341, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * ((ticks / interval) % 3); + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += ((ticks / interval) % 3) * 2 * PI / 3; + fx->actmagicNoLight = 1; + } + } + + int bodypart = 0; + for ( auto node = this->children.first; node; node = node->next, bodypart++ ) + { + if ( behavior == &actPlayer ) + { + if ( bodypart < 1 ) + { + continue; + } + } + else + { + if ( bodypart < 2 ) + { + continue; + } + } + if ( Entity* entity = (Entity*)node->element ) + { + entity->flags[STASIS_DITHER] = flags[STASIS_DITHER]; + } + } + + if ( myStats->getEffectActive(EFF_MAGICIANS_ARMOR) ) + { + int interval = 80; + if ( ticks % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 276, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_MAGICIANS_ARMOR_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + //fx->flags[GENIUS] = true; + fx->scalex = 0.0125; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + + if ( Entity* fx = createParticleAOEIndicator(this, this->x, this->y, 0.0, 2 * TICKS_PER_SECOND, 16.0) ) + { + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(101, 16, 145); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 2; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + indicator->cacheType = AOEIndicators_t::CACHE_MAGICIANS_ARMOR; + } + } + } + } + + if ( myStats->getEffectActive(EFF_BLOOD_WARD) ) + { + int interval = 80; + if ( ticks % interval == 10 ) + { + if ( Entity* fx = createParticleAOEIndicator(this, this->x, this->y, 0.0, 2 * TICKS_PER_SECOND, 16.0) ) + { + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(128, 0, 0); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 2; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + indicator->cacheType = AOEIndicators_t::CACHE_THAUM_ARMOR; + } + } + + Entity* fx = createParticleAestheticOrbit(this, 287, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_BLOOD_WARD_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + fx->flags[GENIUS] = true; + fx->scalex = 0.05; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + } + } + + if ( myStats->getEffectActive(EFF_GUARD_BODY) || myStats->getEffectActive(EFF_GUARD_SPIRIT) || myStats->getEffectActive(EFF_DIVINE_GUARD) ) + { + int interval = 80; + if ( ticks % interval == 0 ) + { + if ( Entity* fx = createParticleAOEIndicator(this, this->x, this->y, 0.0, 2 * TICKS_PER_SECOND, 16.0) ) + { + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(255, 255, 255); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 2; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + indicator->cacheType = AOEIndicators_t::CACHE_THAUM_ARMOR; + } + } + } + + if ( myStats->getEffectActive(EFF_GUARD_BODY) ) + { + if ( ticks % interval == 20 ) + { + Entity* fx = createParticleAestheticOrbit(this, 280, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_GUARD_BODY_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + fx->flags[GENIUS] = true; + fx->scalex = 0.05; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + } + } + if ( myStats->getEffectActive(EFF_GUARD_SPIRIT) ) + { + if ( ticks % interval == 40 ) + { + Entity* fx = createParticleAestheticOrbit(this, 281, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_GUARD_SPIRIT_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + fx->flags[GENIUS] = true; + fx->scalex = 0.05; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + } + } + if ( myStats->getEffectActive(EFF_DIVINE_GUARD) ) + { + if ( ticks % interval == 60 ) + { + Entity* fx = createParticleAestheticOrbit(this, 282, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_GUARD_DIVINE_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + fx->flags[GENIUS] = true; + fx->scalex = 0.05; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + } + } + } + + if ( myStats->getEffectActive(EFF_FLAME_CLOAK) || + (myStats->type == SALAMANDER && myStats->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && myStats->getEffectActive(EFF_SALAMANDER_HEART) <= 2) ) + { + int interval = 40; + if ( ticks % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 233, TICKS_PER_SECOND, PARTICLE_EFFECT_IGNITE_ORBIT_FOLLOW); + fx->flags[SPRITE] = true; + fx->z = this->z;// 7.5 - 2.0 * ((ticks / interval) % 3); + fx->vel_z = 0.25; + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 3; + fx->fskill[2] = this->yaw + PI; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 0; + } + if ( (myStats->type == SALAMANDER && myStats->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && myStats->getEffectActive(EFF_SALAMANDER_HEART) <= 2) ) + { + if ( ((ticks % 10 == 0) && (abs(this->vel_x) > 0.1 || abs(this->vel_y) > 0.1)) ) + { + //if ( abs(this->vel_x) > 0.05 || abs(this->vel_y) > 0.05 ) + { + Entity* fx = createParticleAestheticOrbit(this, 233, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_RADIANT_ORBIT_FOLLOW); + fx->flags[SPRITE] = true; + fx->z = this->z;// 7.5 - 2.0 * ((ticks / interval) % 3); + fx->vel_z = 0.25; + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 1; + fx->z += 8; + fx->fskill[2] = this->yaw + PI; + fx->fskill[4] = 0.1; // rotate + fx->x = this->x + fx->actmagicOrbitDist * cos(this->yaw + PI); + fx->y = this->y + fx->actmagicOrbitDist * sin(this->yaw + PI); + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + if ( (abs(this->vel_x) > 0.05 || abs(this->vel_y) > 0.05) ) + { + fx->actmagicNoLight = 0; + } + else + { + fx->actmagicNoLight = 1; + } + } + } + } + + if ( myStats->getEffectActive(EFF_FLAME_CLOAK) && (ticks % interval == 0) ) { - creatureWebbedSlowCount = 0; // reset counter. - if ( behavior == &actPlayer ) + if ( Entity* fx = createParticleAOEIndicator(this, this->x, this->y, 0.0, TICKS_PER_SECOND, 16.0) ) { - serverUpdateEntitySkill(this, 52); // update player. + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(255, 128, 0); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 2; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + indicator->cacheType = AOEIndicators_t::CACHE_FLAME_CLOAK; + } } } - } - // bleeding - if ( myStats->EFFECTS[EFF_BLEEDING] ) - { - if ( ticks % 120 == 0 ) + int x = this->x / 16; + int y = this->y / 16; + + if ( x >= 0 && x < map.width && y >= 0 && y < map.height && myStats->getEffectActive(EFF_FLAME_CLOAK) ) { - if ( myStats->HP > 5 + (std::max(0, getCON())) ) // CON increases when bleeding stops. + int mapIndex = y * MAPLAYERS + x * MAPLAYERS * map.height; + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 2); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) { - int bleedhurt = 1 + myStats->MAXHP / 30; - if ( bleedhurt > 1 ) - { - bleedhurt -= local_rng.rand() % (std::max(1, bleedhurt / 2)); - } - if ( getCON() > 0 ) - { - bleedhurt -= (getCON() / 5); - } - if ( myStats->type == LICH_ICE - || myStats->type == LICH_FIRE - || myStats->type == LICH - || myStats->type == DEVIL ) - { - bleedhurt = std::min(bleedhurt, 15); // prevent doing 50+ dmg - } - bleedhurt = std::max(1, bleedhurt); - this->modHP(-bleedhurt); - this->setObituary(Language::get(1532)); - myStats->killer = KilledBy::BLEEDING; - Entity* gib = spawnGib(this); - serverSpawnGibForClient(gib); - if ( player >= 0 && players[player]->isLocalPlayer() ) - { - camera_shakex -= .03; - camera_shakey += 3; - } - else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) - { - strcpy((char*)net_packet->data, "SHAK"); - net_packet->data[4] = -3; // turns into -.03 - net_packet->data[5] = 3; - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); - } - messagePlayer(player, MESSAGE_STATUS, Language::get(642)); - + list_t* currentList = *it; + for ( node_t* node = currentList->first; node != nullptr; node = node->next ) { - Entity* entity = nullptr; - if ( spawn_blood && gibtype[myStats->type] == 1 ) - { - entity = newEntity(203, 1, map.entities, nullptr); //Blood entity. - } - else if ( gibtype[myStats->type] == 2 ) - { - entity = newEntity(213, 1, map.entities, nullptr); //Blood entity. - } - else if ( gibtype[myStats->type] == 4 ) - { - entity = newEntity(682, 1, map.entities, nullptr); //Blood entity. - } - if ( entity != NULL ) + Entity* entity = (Entity*)node->element; + if ( entity == this ) { - entity->x = this->x; - entity->y = this->y; - entity->z = 8.0 + (local_rng.rand() % 20) / 100.0; - entity->parent = this->uid; - entity->sizex = 2; - entity->sizey = 2; - entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->flags[UPDATENEEDED] = true; - entity->flags[PASSABLE] = true; + continue; } - } - Entity* killer = uidToEntity(static_cast(myStats->bleedInflictedBy)); - if ( killer && killer->behavior == &actPlayer ) - { - bool lowPriority = true; - // update enemy bar for attacker - if ( !strcmp(myStats->name, "") ) + if ( (entity->behavior == &actCampfire && entity->skill[3] > 0) || entity->behavior == &actTorch ) { - updateEnemyBar(killer, this, getMonsterLocalizedName(myStats->type).c_str(), myStats->HP, myStats->MAXHP, lowPriority, - DamageGib::DMG_BLEED); + if ( entityInsideEntity(entity, this) ) + { + if ( this->SetEntityOnFire(nullptr) ) + { + myStats->burningInflictedBy = 0; + } + } } - else + else if ( entity->flags[BURNING] && entity->flags[BURNABLE] ) { - updateEnemyBar(killer, this, myStats->name, myStats->HP, myStats->MAXHP, lowPriority, - DamageGib::DMG_BLEED); + if ( !entity->getStats() || (entity->behavior == &actMonster || entity->behavior == &actPlayer) ) + { + if ( entityInsideEntity(entity, this) ) + { + if ( this->SetEntityOnFire(entity) ) + { + myStats->burningInflictedBy = 0; + if ( entity->getStats() ) + { + myStats->burningInflictedBy = entity->getUID(); + } + } + } + } } } } - else - { - messagePlayer(player, MESSAGE_STATUS, Language::get(643)); - myStats->EFFECTS[EFF_BLEEDING] = false; - myStats->EFFECTS_TIMERS[EFF_BLEEDING] = 0; - serverUpdateEffects(player); - } } } - else - { - myStats->bleedInflictedBy = 0; - } - - // webbed - if ( myStats->EFFECTS[EFF_WEBBED] ) + + if ( myStats->getEffectActive(EFF_MAGIC_GREASE) ) { if ( ticks % 25 == 0 ) { - Entity* gib = spawnGib(this, 863); - serverSpawnGibForClient(gib); - } - if ( ticks % 40 == 0 ) - { - Entity* entity = newEntity(862, 1, map.entities, nullptr); //Web pool entity. - if ( entity != NULL ) - { - entity->x = this->x; - entity->y = this->y; - entity->z = 8.0 + (local_rng.rand() % 20) / 100.0; - entity->parent = this->uid; - entity->sizex = 2; - entity->sizey = 2; - entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - real_t scale = 0.75 + 0.25 * (local_rng.rand() % 100) / 100.f; - entity->scalex = scale; - entity->scaley = scale; - entity->flags[UPDATENEEDED] = true; - entity->flags[PASSABLE] = true; + if ( Entity* fx = spawnMagicParticleCustom(this, 245, 1.0, 0.5) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(this->vel_y, this->vel_x); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(this->vel_x * this->vel_x + this->vel_y * this->vel_y); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->x += 2.0 * cos(dir); + fx->y += 2.0 * sin(dir); + fx->flags[BRIGHT] = true; + fx->pitch = PI / 2; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + fx->flags[SPRITE] = true; } } } - if ( player >= 0 && (myStats->EFFECTS[EFF_LEVITATING] || myStats->EFFECTS[EFF_FLUTTER]) && MFLAG_DISABLELEVITATION) + if ( player >= 0 && (myStats->getEffectActive(EFF_LEVITATING) || myStats->getEffectActive(EFF_FLUTTER)) && MFLAG_DISABLELEVITATION) { Uint32 color = makeColorRGB(255, 0, 255); messagePlayerColor(player, MESSAGE_HINT, color, Language::get(2382)); // disabled levitation. @@ -4722,17 +7234,17 @@ void Entity::handleEffects(Stat* myStats) this->setEffect(EFF_FLUTTER, false, 0, true); } - if ( myStats->EFFECTS[EFF_MAGICREFLECT] ) + if ( myStats->getEffectActive(EFF_MAGICREFLECT) ) { spawnAmbientParticles(80, 579, 10 + local_rng.rand() % 40, 1.0, false); } - if (myStats->EFFECTS[EFF_VAMPIRICAURA]) + if (myStats->getEffectActive(EFF_VAMPIRICAURA)) { spawnAmbientParticles(40, 600, 20 + local_rng.rand() % 30, 0.5, true); } - if ( myStats->EFFECTS[EFF_FEAR] ) + if ( myStats->getEffectActive(EFF_FEAR) ) { if ( ticks % 25 == 0 || ticks % 40 == 0 ) { @@ -4740,12 +7252,12 @@ void Entity::handleEffects(Stat* myStats) } } - if ( myStats->EFFECTS[EFF_TROLLS_BLOOD] ) + if ( myStats->getEffectActive(EFF_TROLLS_BLOOD) ) { spawnAmbientParticles(80, 169, 20 + local_rng.rand() % 10, 0.5, true); } - if ( myStats->EFFECTS[EFF_PACIFY] ) + if ( myStats->getEffectActive(EFF_PACIFY) ) { if ( ticks % 25 == 0 || ticks % 40 == 0 ) { @@ -4760,7 +7272,7 @@ void Entity::handleEffects(Stat* myStats) } } - if ( myStats->EFFECTS[EFF_SHADOW_TAGGED] ) + if ( myStats->getEffectActive(EFF_SHADOW_TAGGED) ) { if ( ticks % 25 == 0 || ticks % 40 == 0 ) { @@ -4768,7 +7280,7 @@ void Entity::handleEffects(Stat* myStats) } } - if ( myStats->EFFECTS[EFF_POLYMORPH] ) + if ( myStats->getEffectActive(EFF_POLYMORPH) ) { if ( ticks % 25 == 0 || ticks % 40 == 0 ) { @@ -4776,12 +7288,28 @@ void Entity::handleEffects(Stat* myStats) } } - if ( myStats->EFFECTS[EFF_INVISIBLE] && myStats->type == SHADOW ) + if ( myStats->getEffectActive(EFF_INVISIBLE) && myStats->type == SHADOW ) { spawnAmbientParticles(20, 175, 20 + local_rng.rand() % 30, 0.5, true); } - //if ( myStats->EFFECTS[EFF_BLIND] ) + if ( myStats->getEffectActive(EFF_DUSTED) ) + { + if ( ticks % 25 == 0 || ticks % 40 == 0 ) + { + if ( Entity* fx = spawnAmbientParticles(1, local_rng.rand() % 2 ? 156 : 155, 20 + local_rng.rand() % 10, 1.0, true) ) + { + fx->flags[SPRITE] = true; + fx->x = this->x + (-4 + local_rng.rand() % 9); + fx->y = this->y + (-4 + local_rng.rand() % 9); + fx->vel_z = -0.25; + fx->z = 4.0; + fx->flags[BRIGHT] = true; + } + } + } + + //if ( myStats->getEffectActive(EFF_BLIND) ) //{ // spawnAmbientParticles2(2, 175, 20, 0.5, true); // maybe some black clouds //} @@ -4791,6 +7319,34 @@ void Entity::handleEffects(Stat* myStats) { this->char_fire--; // Decrease the fire counter + if ( myStats->getEffectActive(EFF_FLAME_CLOAK) ) + { + // sustain the fire effect + int chance = local_rng.rand() % 100; + if ( myStats->getEffectActive(EFF_FLAME_CLOAK) < chance ) + { + this->char_fire++; + } + } + + if ( myStats->type == SKELETON ) + { + this->char_fire = 0; + } + if ( myStats->type == AUTOMATON ) + { + this->char_fire = 0; + } + if ( myStats->breastplate && myStats->breastplate->type == MACHINIST_APRON ) + { + this->char_fire = 0; + } + if ( myStats->amulet && myStats->amulet->type == AMULET_BURNINGRESIST ) + { + this->char_fire = 0; + this->degradeAmuletProc(myStats, AMULET_BURNINGRESIST); + } + // Check to see if time has run out if ( this->char_fire <= 0 ) { @@ -4801,10 +7357,14 @@ void Entity::handleEffects(Stat* myStats) else { // If 0.6 seconds have passed (30 ticks), process the Burning Status Effect - if ( (this->char_fire % TICKS_TO_PROCESS_FIRE) == 0 ) + if ( myStats->getEffectActive(EFF_FLAME_CLOAK) ) + { + // sustain the fire effect without damage + } + else if ( (this->char_fire % TICKS_TO_PROCESS_FIRE) == 0 ) { bool warmHat = false; - + int oldHP = myStats->HP; // Buddha should not die to fire if ( buddhamode ) { @@ -4823,7 +7383,38 @@ void Entity::handleEffects(Stat* myStats) else { // Player is not Buddha, process fire damage normally + Entity* killer = uidToEntity(static_cast(myStats->burningInflictedBy)); int damage = -2 - local_rng.rand() % 4; // Deal between -2 to -5 damage + if ( killer ) + { + if ( Stat* killerStats = killer->getStats() ) + { + if ( killerStats->type == MOTH_SMALL && killerStats->getAttribute("special_npc") == "fire sprite" ) + { + int damageCap = -(2 + killerStats->LVL / 2); + damage = std::max(damage, damageCap); + } + if ( killerStats->getEffectActive(EFF_FLAME_CLOAK) ) + { + int damageCap = -3; + damage = std::max(damage, damageCap); + } + } + } + + if ( myStats->type == DRYAD ) + { + int extraDmg = 1; + int extraDmgRoll = 1; + if ( !myStats->helmet && myStats->getEffectActive(EFF_GROWTH) > 1 ) + { + int bonus = std::min(3, myStats->getEffectActive(EFF_GROWTH) - 1); + extraDmgRoll += bonus; + } + damage -= (extraDmg + local_rng.rand() % extraDmgRoll); + int damageCap = -5 + local_rng.rand() % 2; // -4 to -5, as we're pushing average up + damage = std::max(damageCap, damage); // set a cap + } real_t fireMultiplier = 1.0; if ( myStats->helmet && myStats->helmet->type == HAT_WARM && local_rng.rand() % 4 == 0 ) @@ -4841,13 +7432,24 @@ void Entity::handleEffects(Stat* myStats) warmHat = true; } } + if ( myStats->type == SALAMANDER ) + { + if ( myStats->getEffectActive(EFF_SALAMANDER_HEART) == 1 + || myStats->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + fireMultiplier = 0.0; + } + else + { + fireMultiplier *= 0.25; + } + } damage *= fireMultiplier; this->modHP(damage); // Deal between -2 to -5 damage - Entity* killer = uidToEntity(static_cast(myStats->burningInflictedBy)); // If the Entity died, handle experience - if ( myStats->HP <= 0 ) + if ( myStats->HP <= 0 && oldHP > myStats->HP ) { this->setObituary(Language::get(1533)); // "burns to a crisp." myStats->killer = KilledBy::BURNING_TO_CRISP; @@ -4865,7 +7467,7 @@ void Entity::handleEffects(Stat* myStats) } } - if ( killer && killer->behavior == &actPlayer ) + if ( killer /*&& killer->behavior == &actPlayer*/ ) { bool lowPriority = true; // update enemy bar for attacker @@ -4894,29 +7496,33 @@ void Entity::handleEffects(Stat* myStats) playSoundEntity(this, 28, 64); // "Damage.ogg" // Shake the Camera - if ( player >= 0 && players[player]->isLocalPlayer() ) - { - camera_shakey += 5; - } - else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + if ( oldHP > myStats->HP ) { - strcpy((char*)net_packet->data, "SHAK"); - net_packet->data[4] = 0; // turns into 0 - net_packet->data[5] = 5; - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + camera_shakey += 5; + } + else if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 0; // turns into 0 + net_packet->data[5] = 5; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } } // If the Entity has a Cloak, process dealing damage to the Entity's Cloak - if ( myStats->cloak != nullptr ) + if ( myStats->cloak != nullptr && oldHP > myStats->HP ) { // 1 in 10 chance of dealing damage to Entity's cloak if ( local_rng.rand() % 10 == 0 && myStats->cloak->type != ARTIFACT_CLOAK && myStats->cloak->type != CLOAK_GUARDIAN - && myStats->cloak->type != CLOAK_BACKPACK ) + && myStats->cloak->type != CLOAK_BACKPACK + && !spellEffectPreserveItem(myStats->cloak) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -4942,9 +7548,10 @@ void Entity::handleEffects(Stat* myStats) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 6; net_packet->data[5] = myStats->cloak->status; + SDLNet_Write16((int)myStats->cloak->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -4986,7 +7593,7 @@ void Entity::handleEffects(Stat* myStats) if ( mySummon && mySummon->monsterAllySummonRank != 0 ) { Stat* mySummonStats = mySummon->getStats(); - if ( mySummonStats ) + if ( mySummonStats && mySummonStats->type == SKELETON ) { if ( numSummonedAllies == 0 ) { @@ -5065,17 +7672,18 @@ void Entity::handleEffects(Stat* myStats) sendPacketSafe(net_sock, -1, net_packet, player - 1); } } - for ( c = 0; c < NUMEFFECTS; c++ ) + for ( int c = 0; c < NUMEFFECTS; c++ ) { if ( !(c == EFF_VAMPIRICAURA && myStats->EFFECTS_TIMERS[c] == -2) && c != EFF_WITHDRAWAL && c != EFF_SHAPESHIFT ) { - myStats->EFFECTS[c] = false; + myStats->clearEffect(c); myStats->EFFECTS_TIMERS[c] = 0; + myStats->EFFECTS_ACCRETION_TIME[c] = 0; } } - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 5 * TICKS_PER_SECOND; this->flags[BURNING] = false; @@ -5112,7 +7720,10 @@ void Entity::handleEffects(Stat* myStats) messagePlayerColor(player, MESSAGE_HINT, color, Language::get(3358)); int amount = 2 + local_rng.rand() % 2; int oldMP = myStats->MP; - this->modMP(amount); + + int mpAmount = this->modMP(amount); + this->playerInsectoidIncrementHungerToMP(mpAmount); + if ( player >= 0 && stats[player]->stat_appearance == 0 ) { if ( stats[player]->playerRace == RACE_INCUBUS || stats[player]->playerRace == RACE_SUCCUBUS ) @@ -5146,9 +7757,10 @@ void Entity::handleEffects(Stat* myStats) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 7; net_packet->data[5] = myStats->amulet->status; + SDLNet_Write16((int)myStats->amulet->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -5167,7 +7779,7 @@ void Entity::handleEffects(Stat* myStats) sendPacketSafe(net_sock, -1, net_packet, player - 1); } } - else + else if ( !spellEffectPreserveItem(myStats->amulet) ) { messagePlayer(player, MESSAGE_STATUS | MESSAGE_EQUIPMENT, Language::get(649)); messagePlayer(player, MESSAGE_STATUS | MESSAGE_EQUIPMENT, Language::get(650)); @@ -5188,9 +7800,10 @@ void Entity::handleEffects(Stat* myStats) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 7; net_packet->data[5] = myStats->amulet->status; + SDLNet_Write16((int)myStats->amulet->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -5252,13 +7865,14 @@ void Entity::handleEffects(Stat* myStats) real_t hpRestore = 100 * (0.5 + std::min(2, abs(myStats->amulet->beatitude)) * 0.5); this->setHP(std::max((int)hpRestore, 5)); - for ( c = 0; c < NUMEFFECTS; c++ ) + for ( int c = 0; c < NUMEFFECTS; c++ ) { if ( !(c == EFF_VAMPIRICAURA && myStats->EFFECTS_TIMERS[c] == -2) - && c != EFF_WITHDRAWAL && c != EFF_SHAPESHIFT ) + && c != EFF_WITHDRAWAL && c != EFF_SHAPESHIFT && c != EFF_PRESERVE) { - myStats->EFFECTS[c] = false; + myStats->clearEffect(c); myStats->EFFECTS_TIMERS[c] = 0; + myStats->EFFECTS_ACCRETION_TIME[c] = 0; } } @@ -5279,7 +7893,7 @@ void Entity::handleEffects(Stat* myStats) // } // } //} - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 5 * TICKS_PER_SECOND; this->flags[BURNING] = false; @@ -5291,20 +7905,25 @@ void Entity::handleEffects(Stat* myStats) messagePlayer(player, MESSAGE_STATUS | MESSAGE_OBITUARY, Language::get(656)); messagePlayer(player, MESSAGE_STATUS | MESSAGE_OBITUARY, Language::get(657)); } - myStats->amulet->status = BROKEN; - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, myStats->amulet->type, 1); - playSoundEntity(this, 76, 64); - if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + + if ( !spellEffectPreserveItem(myStats->amulet) ) { - strcpy((char*)net_packet->data, "ARMR"); - net_packet->data[4] = 7; - net_packet->data[5] = myStats->amulet->status; - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); + myStats->amulet->status = BROKEN; + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, myStats->amulet->type, 1); + playSoundEntity(this, 76, 64); + if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "ARMR"); + net_packet->data[4] = 7; + net_packet->data[5] = myStats->amulet->status; + SDLNet_Write16((int)myStats->amulet->type, &net_packet->data[6]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + myStats->amulet = NULL; } - myStats->amulet = NULL; } } } @@ -5312,19 +7931,19 @@ void Entity::handleEffects(Stat* myStats) if ( player >= 0 && myStats->mask != nullptr ) { if ( myStats->mask->type == TOOL_BLINDFOLD_TELEPATHY - && (ticks % 45 == 0 || !myStats->EFFECTS[EFF_TELEPATH]) ) + && (ticks % 45 == 0 || !myStats->getEffectActive(EFF_TELEPATH)) ) { setEffect(EFF_TELEPATH, true, 60, true); } else if ( (myStats->mask->type == MASK_PLAGUE) && !(myStats->type != HUMAN && effectShapeshift != NOTHING) ) { - if ( ticks % 45 == 0 || !myStats->EFFECTS[EFF_NAUSEA_PROTECTION] ) + if ( ticks % 45 == 0 || !myStats->getEffectActive(EFF_NAUSEA_PROTECTION) ) { setEffect(EFF_NAUSEA_PROTECTION, true, 60, true); } if ( myStats->mask->type == MASK_PLAGUE ) { - if ( ticks % 45 == 0 || !myStats->EFFECTS[EFF_POISONED] ) + if ( ticks % 45 == 0 || !myStats->getEffectActive(EFF_POISONED) ) { if ( !(myStats->type == INSECTOID || (myStats->amulet && myStats->amulet->type == AMULET_POISONRESISTANCE)) ) { @@ -5335,9 +7954,9 @@ void Entity::handleEffects(Stat* myStats) } else { - bool poisoned = myStats->EFFECTS[EFF_POISONED]; + bool poisoned = myStats->getEffectActive(EFF_POISONED) > 0; setEffect(EFF_POISONED, true, (TICKS_PER_SECOND + 1) * 3, true); - if ( !poisoned && myStats->EFFECTS[EFF_POISONED] ) + if ( !poisoned && myStats->getEffectActive(EFF_POISONED) ) { myStats->poisonKiller = 0; messagePlayerColor(player, MESSAGE_STATUS, makeColorRGB(255, 0, 0), @@ -5354,7 +7973,7 @@ void Entity::handleEffects(Stat* myStats) if ( player >= 0 && myStats->mask != nullptr && (myStats->mask->type == TOOL_BLINDFOLD || myStats->mask->type == TOOL_BLINDFOLD_FOCUS || myStats->mask->type == TOOL_BLINDFOLD_TELEPATHY ) - && (ticks % 45 == 0 || !myStats->EFFECTS[EFF_BLIND]) ) + && (ticks % 45 == 0 || !myStats->getEffectActive(EFF_BLIND)) ) { setEffect(EFF_BLIND, true, 60, true); if ( myStats->mask->type == TOOL_BLINDFOLD_FOCUS ) @@ -5363,7 +7982,7 @@ void Entity::handleEffects(Stat* myStats) } } - if ( ticks % 45 == 0 && myStats->type == GOATMAN && myStats->EFFECTS[EFF_DRUNK] ) + if ( ticks % 45 == 0 && myStats->type == GOATMAN && myStats->getEffectActive(EFF_DRUNK) ) { freeAction = true; } @@ -5398,15 +8017,37 @@ void Entity::handleEffects(Stat* myStats) } // unparalyze certain boss characters - if ( myStats->EFFECTS[EFF_PARALYZED] && ((myStats->type >= LICH && myStats->type < KOBOLD) + if ( myStats->getEffectActive(EFF_PARALYZED) && ((myStats->type >= LICH && myStats->type < KOBOLD) || myStats->type == COCKATRICE || myStats->type == LICH_FIRE || myStats->type == LICH_ICE) ) { - myStats->EFFECTS[EFF_PARALYZED] = false; + myStats->clearEffect(EFF_PARALYZED); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 0; } + if ( myStats->getEffectActive(EFF_ATTRACT_ITEMS) ) + { + real_t dist = std::max(16, std::min(getSpellDamageSecondaryFromID(SPELL_ATTRACT_ITEMS, this, nullptr, this, 0.0, false), + getSpellDamageFromID(SPELL_ATTRACT_ITEMS, this, nullptr, this, 0.0, false))); + int tiles = 1 + (dist / 16.0); + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, tiles); + for ( auto it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + node_t* node; + for ( node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( !entity ) { continue; } + if ( entity && !entity->flags[INVISIBLE] && entity->behavior == &actItem && entityDist(entity, this) < dist ) + { + this->attractItem(*entity); + } + } + } + } + // wake up - if ( myStats->EFFECTS[EFF_ASLEEP] && (myStats->OLDHP > myStats->HP || (myStats->type >= LICH && myStats->type < KOBOLD) + if ( myStats->getEffectActive(EFF_ASLEEP) && (myStats->OLDHP > myStats->HP || (myStats->type >= LICH && myStats->type < KOBOLD) || myStats->type == COCKATRICE || myStats->type == LICH_FIRE || myStats->type == LICH_ICE) ) { messagePlayer(player, MESSAGE_STATUS, Language::get(658)); @@ -5415,32 +8056,75 @@ void Entity::handleEffects(Stat* myStats) // allies resting. if poison/bleed damage here, then ignore it (startingHPInHandleEffects will equal current HP) if ( !naturalHeal && startingHPInHandleEffects == myStats->HP ) { - myStats->EFFECTS[EFF_ASLEEP] = false; // wake up + myStats->clearEffect(EFF_ASLEEP); // wake up myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 0; - myStats->EFFECTS[EFF_HP_REGEN] = false; // stop regen + myStats->clearEffect(EFF_HP_REGEN); // stop regen myStats->EFFECTS_TIMERS[EFF_HP_REGEN] = 0; monsterAllySpecial = ALLY_SPECIAL_CMD_NONE; } } - else + else + { + myStats->clearEffect(EFF_ASLEEP); + myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 0; + } + serverUpdateEffects(player); + } + else if ( myStats->getEffectActive(EFF_ASLEEP) && monsterAllyGetPlayerLeader() && monsterAllySpecial == ALLY_SPECIAL_CMD_REST ) + { + if ( myStats->HP == myStats->MAXHP ) + { + myStats->clearEffect(EFF_ASLEEP); // wake up + myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 0; + myStats->clearEffect(EFF_HP_REGEN); // stop regen + myStats->EFFECTS_TIMERS[EFF_HP_REGEN] = 0; + monsterAllySpecial = ALLY_SPECIAL_CMD_NONE; + messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, *myStats, Language::get(3881), Language::get(3881), MSG_GENERIC); + } + } + + if ( myStats->OLDHP > myStats->HP && myStats->getEffectActive(EFF_PROJECT_SPIRIT) ) + { + myStats->EFFECTS_TIMERS[EFF_PROJECT_SPIRIT] = 1; + steamAchievementClient(player, "BARONY_ACH_DISTRACTED"); + serverUpdateEffects(player); + } + + if ( myStats->OLDHP > myStats->HP && myStats->getEffectActive(EFF_HEALING_WORD) ) + { + setEffect(EFF_HEALING_WORD, false, 0, false); + } + + if ( (myStats->type == DUMMYBOT || myStats->type == HUMAN) && myStats->getAttribute("dummy_target") != "" ) + { + if ( myStats->getAttribute("dummy_ticks") == "" ) + { + myStats->setAttribute("dummy_ticks", std::to_string(this->ticks)); + } + Uint32 dummyTick = std::stoi(myStats->getAttribute("dummy_ticks")); + int damage = std::stoi(myStats->getAttribute("dummy_target")); + if ( myStats->HP < myStats->OLDHP ) { - myStats->EFFECTS[EFF_ASLEEP] = false; - myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 0; + damage += myStats->OLDHP - myStats->HP; + myStats->setAttribute("dummy_target", std::to_string(damage)); } - serverUpdateEffects(player); - } - else if ( myStats->EFFECTS[EFF_ASLEEP] && monsterAllyGetPlayerLeader() && monsterAllySpecial == ALLY_SPECIAL_CMD_REST ) - { - if ( myStats->HP == myStats->MAXHP ) + if ( damage == 0 ) { - myStats->EFFECTS[EFF_ASLEEP] = false; // wake up - myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 0; - myStats->EFFECTS[EFF_HP_REGEN] = false; // stop regen - myStats->EFFECTS_TIMERS[EFF_HP_REGEN] = 0; - monsterAllySpecial = ALLY_SPECIAL_CMD_NONE; - messagePlayerMonsterEvent(monsterAllyIndex, 0xFFFFFFFF, *myStats, Language::get(3881), Language::get(3881), MSG_GENERIC); + myStats->setAttribute("dummy_ticks", std::to_string(this->ticks)); + } + if ( this->ticks - dummyTick >= 5 * TICKS_PER_SECOND ) + { + myStats->setAttribute("dummy_ticks", std::to_string(this->ticks)); + myStats->setAttribute("dummy_target", "0"); + int dps = damage / ((this->ticks - dummyTick) / (real_t)(5 * TICKS_PER_SECOND)); + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + messagePlayer(i, MESSAGE_HINT, "DPS: %d", dps); + } + spawnDamageGib(this, dps, DMG_STRONGER, DamageGibDisplayType::DMG_GIB_DPS_CHECK, true); } } + myStats->OLDHP = myStats->HP; } @@ -5451,7 +8135,7 @@ real_t Entity::getACEffectiveness(Entity* my, Stat* myStats, bool isPlayer, Enti return 1.0; } - if ( myStats->defending ) + if ( myStats->defending || (myStats->parrying && myStats->weapon && itemCategory(myStats->weapon) == WEAPON) ) { return 1.0; } @@ -5499,6 +8183,10 @@ real_t Entity::getACEffectiveness(Entity* my, Stat* myStats, bool isPlayer, Enti return std::max(0.0, std::min(1.0, .75 + 0.025 * blessings)); } +real_t Entity::PlayerAttackMeleeStatFactor = 0.055; +real_t Entity::PlayerAttackRangedStatFactor = 0.055; +real_t Entity::PlayerAttackThrownStatFactor = 0.0625; + /*------------------------------------------------------------------------------- Entity::getAttack @@ -5508,7 +8196,7 @@ base number -------------------------------------------------------------------------------*/ -Sint32 Entity::getAttack(Entity* my, Stat* myStats, bool isPlayer) +Sint32 Entity::getAttack(Entity* my, Stat* myStats, bool isPlayer, int chargeModifier, int* returnWeaponAttackValue) { Sint32 attack = 0; @@ -5526,6 +8214,25 @@ Sint32 Entity::getAttack(Entity* my, Stat* myStats, bool isPlayer) { attack = BASE_PLAYER_UNARMED_DAMAGE; attack += (myStats->getModifiedProficiency(PRO_UNARMED) / 20); // 0, 1, 2, 3, 4, 5 damage from total + if ( shapeshifted ) + { + if ( my->effectShapeshift == RAT ) + { + attack += 2; + } + else if ( my->effectShapeshift == SPIDER ) + { + attack += 3; + } + else if ( my->effectShapeshift == TROLL ) + { + attack += 5; + } + else if ( my->effectShapeshift == CREATURE_IMP ) + { + attack += 2; + } + } } if ( myStats->gloves && !shapeshifted ) { @@ -5554,15 +8261,84 @@ Sint32 Entity::getAttack(Entity* my, Stat* myStats, bool isPlayer) attack += myStats->weapon->weaponGetAttack(myStats); } - if ( !shapeshifted && myStats->weapon && myStats->weapon->type == TOOL_WHIP ) + if ( returnWeaponAttackValue ) + { + *returnWeaponAttackValue = attack; + } + + if ( !shapeshifted && myStats->weapon && myStats->weapon->type == RAPIER ) + { + int atk = statGetDEX(myStats, my); + if ( chargeModifier >= 0 && chargeModifier < Stat::getMaxAttackCharge(myStats) ) + { + atk /= 2; + } + if ( isPlayer ) + { + attack *= (1.0 + atk * Entity::PlayerAttackMeleeStatFactor); + } + else + { + attack += atk; + } + } + else if ( !shapeshifted && myStats->weapon && myStats->weapon->type == TOOL_WHIP ) { int atk = statGetSTR(myStats, my) + statGetDEX(myStats, my); atk = std::min(atk / 2, atk); - attack += atk; + if ( isPlayer ) + { + attack *= (1.0 + atk * Entity::PlayerAttackMeleeStatFactor); + } + else + { + attack += atk; + } + } + else if ( !shapeshifted && myStats->weapon && myStats->weapon->type == MAGICSTAFF_SCEPTER ) + { + int atk = statGetSTR(myStats, my); + atk = std::min(atk / 2, atk); + if ( isPlayer ) + { + attack *= (1.0 + atk * Entity::PlayerAttackMeleeStatFactor); + } + else + { + attack += atk; + } } else { - attack += statGetSTR(myStats, my); + int atk = statGetSTR(myStats, my); + if ( isPlayer ) + { + attack *= (1.0 + atk * Entity::PlayerAttackMeleeStatFactor); + } + else + { + attack += atk; + } + if ( !shapeshifted && myStats->weapon && myStats->weapon->type == STEEL_FLAIL ) + { + if ( (my && my->behavior == &actMonster) ) + { + if ( my->monsterAttack == MONSTER_POSE_FLAIL_SWING ) + { + attack *= (0.5 + (myStats->getModifiedProficiency(PRO_MACE) * 0.25 / 100.0)); + } + } + else if ( chargeModifier >= 0 && chargeModifier < Stat::getMaxAttackCharge(myStats) / 2 ) + { + attack *= (0.5 + (myStats->getModifiedProficiency(PRO_MACE) * 0.25 / 100.0)); + } + } + } + + if ( Uint8 effectStrength = myStats->getEffectActive(EFF_WEAKNESS) ) + { + real_t mult = std::min(0.9, 0.2 + (effectStrength - 1) * 0.1); + attack *= 1.0 - mult; } return attack; @@ -5577,10 +8353,10 @@ base number -------------------------------------------------------------------------------*/ -Sint32 Entity::getRangedAttack() +Sint32 Entity::getRangedAttack(int atkFromQuivers) { Stat* entitystats; - int attack = BASE_RANGED_DAMAGE; // base ranged attack strength + int attack = BASE_RANGED_DAMAGE + atkFromQuivers; // base ranged attack strength if ( (entitystats = this->getStats()) == nullptr ) { @@ -5590,7 +8366,14 @@ Sint32 Entity::getRangedAttack() if ( entitystats->weapon ) { attack += entitystats->weapon->weaponGetAttack(entitystats); - attack += getDEX(); + if ( behavior == &actPlayer ) + { + attack *= (1.0 + getDEX() * Entity::PlayerAttackRangedStatFactor); + } + else + { + attack += getDEX(); + } if ( behavior == &actMonster ) { attack += getPER(); // monsters take PER into their ranged attacks to avoid having to increase their speed. @@ -5601,6 +8384,12 @@ Sint32 Entity::getRangedAttack() { return 0; } + + /*if ( Uint8 effectStrength = entitystats->getEffectActive(EFF_WEAKNESS) ) + { + real_t mult = 0.2 + (effectStrength - 1) * 0.1; + }*/ + return attack; } @@ -5627,11 +8416,30 @@ Sint32 Entity::getThrownAttack() if ( entitystats->weapon ) { - if ( itemCategory(entitystats->weapon) == THROWN ) + real_t statToAtkRatio = Entity::PlayerAttackThrownStatFactor; + + if ( entitystats->weapon->type == BOLAS ) + { + attack = entitystats->weapon->weaponGetAttack(entitystats); + } + else if ( entitystats->weapon->type == DUST_BALL + || entitystats->weapon->type == SLOP_BALL + || entitystats->weapon->type == GREASE_BALL ) + { + attack = entitystats->weapon->weaponGetAttack(entitystats); + } + else if ( itemCategory(entitystats->weapon) == THROWN ) { - int dex = getDEX() / 4; - attack += dex; attack += entitystats->weapon->weaponGetAttack(entitystats); + if ( behavior == &actPlayer ) + { + attack *= (1.0 + (getDEX() / 4.0) * statToAtkRatio); + } + else + { + int dex = getDEX() / 4; + attack += dex; + } attack *= thrownDamageSkillMultipliers[std::min(skillLVL, 5)]; } else if ( itemCategory(entitystats->weapon) == POTION ) @@ -5644,9 +8452,16 @@ Sint32 Entity::getThrownAttack() } else { - int dex = getDEX() / 4; - attack += dex; attack += entitystats->weapon->weaponGetAttack(entitystats); + if ( behavior == &actPlayer ) + { + attack *= (1.0 + (getDEX() / 4.0) * statToAtkRatio); + } + else + { + int dex = getDEX() / 4; + attack += dex; + } attack += entitystats->getModifiedProficiency(PRO_RANGED) / 10; // 0 to 10 bonus attack. } } @@ -5654,6 +8469,12 @@ Sint32 Entity::getThrownAttack() { return 0; } + + /*if ( Uint8 effectStrength = entitystats->getEffectActive(EFF_WEAKNESS) ) + { + real_t mult = 0.2 + (effectStrength - 1) * 0.1; + }*/ + return attack; } @@ -5677,7 +8498,7 @@ Sint32 Entity::getBonusAttackOnTarget(Stat& hitstats) if ( entitystats->weapon ) { - if ( hitstats.EFFECTS[EFF_VAMPIRICAURA] ) + if ( hitstats.getEffectActive(EFF_VAMPIRICAURA) ) { // blessed weapons deal more damage under this effect. bonusAttack += entitystats->weapon->beatitude; @@ -5798,11 +8619,31 @@ Sint32 statGetSTR(Stat* entitystats, Entity* my) STR += (cursedItemIsBuff ? abs(entitystats->ring->beatitude) : entitystats->ring->beatitude); } } - if ( entitystats->EFFECTS[EFF_SHRINE_RED_BUFF] ) + if ( entitystats->getEffectActive(EFF_SHRINE_RED_BUFF) ) { STR += 8; } - if ( entitystats->EFFECTS[EFF_VAMPIRICAURA] && my && my->behavior == &actPlayer ) + if ( entitystats->getEffectActive(EFF_RATION_HEARTY) ) + { + STR += 4; + } + if ( entitystats->getEffectActive(EFF_RATION_SPICY) ) + { + STR += 4; + } + if ( entitystats->getEffectActive(EFF_COWARDICE) ) + { + STR -= entitystats->getEffectActive(EFF_COWARDICE); + } + if ( entitystats->getEffectActive(EFF_COURAGE) ) + { + STR += entitystats->getEffectActive(EFF_COURAGE); + } + if ( entitystats->getEffectActive(EFF_DEMESNE_DOOR) ) + { + STR += entitystats->getEffectActive(EFF_DEMESNE_DOOR); + } + if ( entitystats->getEffectActive(EFF_VAMPIRICAURA) && my && my->behavior == &actPlayer ) { if ( entitystats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) { @@ -5813,11 +8654,41 @@ Sint32 statGetSTR(Stat* entitystats, Entity* my) STR += (std::max(5, STR / 4)); } } - if ( entitystats->EFFECTS[EFF_POTION_STR] ) + + if ( entitystats->getEffectActive(EFF_MAXIMISE) ) + { + Uint8 effectStrength = entitystats->getEffectActive(EFF_MAXIMISE) & 0xF; + if ( my && my->behavior == &actPlayer ) + { + STR += std::max((real_t)effectStrength, STR * 0.1 * effectStrength); + } + else + { + STR += std::max((real_t)effectStrength, STR * 0.1 * effectStrength); + } + } + if ( entitystats->getEffectActive(EFF_MINIMISE) ) + { + Uint8 effectStrength = entitystats->getEffectActive(EFF_MINIMISE) & 0xF; + if ( my && my->behavior == &actPlayer ) + { + STR -= std::max((real_t)effectStrength, STR * 0.1 * effectStrength); + } + else + { + STR -= std::max((real_t)effectStrength, STR * 0.1 * effectStrength); + } + } + + if ( entitystats->getEffectActive(EFF_POTION_STR) ) { STR += (std::max(5, STR / 4)); } - if ( entitystats->EFFECTS[EFF_DRUNK] ) + if ( entitystats->getEffectActive(EFF_GREATER_MIGHT) ) + { + STR += entitystats->getThaumProficiencySpellStatBonus(STAT_STR, STR); + } + if ( entitystats->getEffectActive(EFF_DRUNK) ) { switch ( entitystats->type ) { @@ -5836,6 +8707,29 @@ Sint32 statGetSTR(Stat* entitystats, Entity* my) break; } } + + if ( entitystats->type == SALAMANDER ) + { + if ( Uint8 effectStrength = entitystats->getEffectActive(EFF_SALAMANDER_HEART) ) + { + if ( effectStrength == 3 || effectStrength == 4 ) + { + real_t ratio = (statGetCHR(entitystats, my) + 10) / 100.0; + STR += 3 + (STR * ratio); + } + } + } + + STR += (Sint32)entitystats->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_EFF_1); + + if ( entitystats->type == MIMIC ) + { + if ( entitystats->getEffectActive(EFF_MIMIC_VOID) ) + { + STR += 15; + } + } + return STR; } @@ -5868,11 +8762,11 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) } // paralyzed - if ( entitystats->EFFECTS[EFF_PARALYZED] ) + if ( entitystats->getEffectActive(EFF_PARALYZED) ) { return -10; } - if ( entitystats->EFFECTS[EFF_ASLEEP] ) + if ( entitystats->getEffectActive(EFF_ASLEEP) ) { return -10; } @@ -5908,7 +8802,7 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) } } - if ( entitystats->EFFECTS[EFF_VAMPIRICAURA] && !entitystats->EFFECTS[EFF_FAST] && !entitystats->EFFECTS[EFF_SLOW] ) + if ( entitystats->getEffectActive(EFF_VAMPIRICAURA) && !entitystats->getEffectActive(EFF_FAST) && !entitystats->getEffectActive(EFF_SLOW) ) { if ( entitystats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) { @@ -5927,7 +8821,7 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) } } } - else if ( entitystats->EFFECTS[EFF_FAST] && !entitystats->EFFECTS[EFF_SLOW] ) + else if ( entitystats->getEffectActive(EFF_FAST) && !entitystats->getEffectActive(EFF_SLOW) ) { if ( my && my->behavior == &actPlayer ) { @@ -5938,15 +8832,19 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) DEX += 10; } } - if ( entitystats->EFFECTS[EFF_STUNNED] ) + if ( entitystats->getEffectActive(EFF_STUNNED) ) { //DEX -= 5; } - if ( entitystats->EFFECTS[EFF_AGILITY] ) + if ( entitystats->getEffectActive(EFF_AGILITY) ) { DEX += (std::max(5, DEX / 4)); } + if ( entitystats->getEffectActive(EFF_NIMBLENESS) ) + { + DEX += entitystats->getThaumProficiencySpellStatBonus(STAT_DEX, DEX); + } if ( my && my->monsterAllyGetPlayerLeader() ) { @@ -5989,11 +8887,13 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) } } - if ( entitystats->EFFECTS[EFF_WEBBED] && !entitystats->EFFECTS[EFF_SLOW] ) + DEX += (Sint32)entitystats->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_EFF_1); + + if ( entitystats->getEffectActive(EFF_WEBBED) && !entitystats->getEffectActive(EFF_SLOW) ) { DEX = std::max(std::min(DEX, 2) - 2 * (my ? my->creatureWebbedSlowCount : 0), -4); } - if ( !entitystats->EFFECTS[EFF_FAST] && entitystats->EFFECTS[EFF_SLOW] ) + if ( !entitystats->getEffectActive(EFF_FAST) && entitystats->getEffectActive(EFF_SLOW) ) { if ( my && my->behavior == &actPlayer ) { @@ -6004,6 +8904,22 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) DEX = std::min(DEX - 3, -2); } } + + if ( !entitystats->getEffectActive(EFF_FAST) && entitystats->getEffectActive(EFF_DISRUPTED) ) + { + if ( my && my->behavior == &actMonster ) + { + if ( entitystats->getEffectActive(EFF_DISRUPTED) == 1 ) + { + DEX = std::min(DEX - 3, 1); + } + else + { + DEX = std::min(DEX - 3, -2); + } + } + } + if ( entitystats->shoes != nullptr ) { if ( entitystats->shoes->type == LEATHER_BOOTS_SPEED ) @@ -6037,7 +8953,12 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) DEX += (cursedItemIsBuff ? abs(entitystats->gloves->beatitude) : entitystats->gloves->beatitude); } } - if ( entitystats->EFFECTS[EFF_DRUNK] ) + if ( entitystats->getEffectActive(EFF_RATION_SPICY) ) + { + DEX += 4; + } + + if ( entitystats->getEffectActive(EFF_DRUNK) ) { switch ( entitystats->type ) { @@ -6050,6 +8971,18 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) } } + if ( entitystats->type == SALAMANDER ) + { + if ( Uint8 effectStrength = entitystats->getEffectActive(EFF_SALAMANDER_HEART) ) + { + if ( effectStrength == 3 || effectStrength == 4 ) + { + real_t ratio = (statGetCHR(entitystats, my) + 10) / 100.0; + DEX -= 3 + (DEX * ratio); + } + } + } + if ( !(svFlags & SV_FLAG_HUNGER) ) { if ( my && my->behavior == &actPlayer && entitystats->playerRace == RACE_INSECTOID && entitystats->stat_appearance == 0 ) @@ -6071,7 +9004,7 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) } } - if ( entitystats->EFFECTS[EFF_WITHDRAWAL] && !entitystats->EFFECTS[EFF_DRUNK] ) + if ( entitystats->getEffectActive(EFF_WITHDRAWAL) && !entitystats->getEffectActive(EFF_DRUNK) ) { DEX -= 3; // hungover. int minusDex = DEX; @@ -6080,10 +9013,43 @@ Sint32 statGetDEX(Stat* entitystats, Entity* my) DEX -= (minusDex / 4); // -1 DEX for every 4 DEX we have. } } - if ( entitystats->EFFECTS[EFF_SHRINE_GREEN_BUFF] ) + if ( entitystats->getEffectActive(EFF_SHRINE_GREEN_BUFF) ) { DEX += 8; } + + if ( entitystats->getEffectActive(EFF_MAXIMISE) ) + { + Uint8 effectStrength = entitystats->getEffectActive(EFF_MAXIMISE) & 0xF; + if ( my && my->behavior == &actPlayer ) + { + DEX -= std::max(5.0, DEX * 0.1 * effectStrength); + } + else + { + DEX -= std::max(5.0, DEX * 0.1 * effectStrength); + } + } + if ( entitystats->getEffectActive(EFF_MINIMISE) ) + { + Uint8 effectStrength = entitystats->getEffectActive(EFF_MINIMISE) & 0xF; + if ( my && my->behavior == &actPlayer ) + { + DEX += std::max((real_t)effectStrength, DEX * 0.1 * effectStrength); + } + else + { + DEX += std::max((real_t)effectStrength, DEX * 0.1 * effectStrength); + } + } + + if ( entitystats->type == MIMIC ) + { + if ( entitystats->getEffectActive(EFF_MIMIC_VOID) ) + { + DEX += 3; + } + } return DEX; } @@ -6175,15 +9141,42 @@ Sint32 statGetCON(Stat* entitystats, Entity* my) CON += (cursedItemIsBuff ? abs(entitystats->gloves->beatitude) : entitystats->gloves->beatitude); } } - if ( entitystats->EFFECTS[EFF_SHRINE_RED_BUFF] ) + if ( entitystats->getEffectActive(EFF_SHRINE_RED_BUFF) ) { CON += 8; } - if ( my && entitystats->EFFECTS[EFF_DRUNK] && entitystats->type == GOATMAN ) + if ( entitystats->getEffectActive(EFF_RATION_HEARTY) ) + { + CON += 4; + } + if ( entitystats->getEffectActive(EFF_RATION_BITTER) ) + { + CON += 4; + } + + if ( entitystats->getEffectActive(EFF_COWARDICE) ) + { + CON -= entitystats->getEffectActive(EFF_COWARDICE); + } + if ( entitystats->getEffectActive(EFF_COURAGE) ) + { + CON += entitystats->getEffectActive(EFF_COURAGE); + } + if ( entitystats->getEffectActive(EFF_DEMESNE_DOOR) ) + { + CON += entitystats->getEffectActive(EFF_DEMESNE_DOOR); + } + if ( my && entitystats->getEffectActive(EFF_DRUNK) && entitystats->type == GOATMAN ) { CON += std::max(4, static_cast(CON * 0.25)); } - if ( entitystats->EFFECTS[EFF_CON_BONUS] ) + CON += (Sint32)entitystats->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_EFF_1); + if ( entitystats->getEffectActive(EFF_STURDINESS) ) + { + CON += entitystats->getThaumProficiencySpellStatBonus(STAT_CON, CON); + } + + if ( entitystats->getEffectActive(EFF_CON_BONUS) ) { CON += 3; int percentHP = static_cast(100.0 * (real_t)entitystats->HP / std::max(1, entitystats->MAXHP)); @@ -6191,6 +9184,57 @@ Sint32 statGetCON(Stat* entitystats, Entity* my) percentHP = 100 - percentHP; CON += percentHP / 10; } + + if ( entitystats->getEffectActive(EFF_MAXIMISE) ) + { + Uint8 effectStrength = entitystats->getEffectActive(EFF_MAXIMISE) & 0xF; + if ( my && my->behavior == &actPlayer ) + { + CON += std::max((real_t)effectStrength, CON * 0.1 * effectStrength); + } + else + { + CON += std::max((real_t)effectStrength, CON * 0.1 * effectStrength); + } + } + if ( entitystats->getEffectActive(EFF_MINIMISE) ) + { + Uint8 effectStrength = entitystats->getEffectActive(EFF_MINIMISE) & 0xF; + if ( my && my->behavior == &actPlayer ) + { + CON -= std::max((real_t)effectStrength, CON * 0.1 * effectStrength); + } + else + { + CON -= std::max((real_t)effectStrength, CON * 0.1 * effectStrength); + } + } + + if ( entitystats->type == SALAMANDER ) + { + if ( Uint8 effectStrength = entitystats->getEffectActive(EFF_SALAMANDER_HEART) ) + { + if ( effectStrength == 1 || effectStrength == 2 ) + { + real_t ratio = (statGetCHR(entitystats, my) + 10) / 100.0; + CON -= 3 + (CON * ratio); + } + else if ( effectStrength == 3 || effectStrength == 4 ) + { + real_t ratio = (statGetCHR(entitystats, my) + 10) / 100.0; + CON += 3 + (CON * ratio); + } + } + } + + if ( entitystats->type == MIMIC ) + { + if ( entitystats->getEffectActive(EFF_MIMIC_VOID) ) + { + CON += 10; + } + } + return CON; } @@ -6290,11 +9334,23 @@ Sint32 statGetINT(Stat* entitystats, Entity* my) INT += (cursedItemIsBuff ? abs(entitystats->breastplate->beatitude) : entitystats->breastplate->beatitude); } } - if ( entitystats->EFFECTS[EFF_SHRINE_BLUE_BUFF] ) + if ( entitystats->getEffectActive(EFF_SHRINE_BLUE_BUFF) ) { INT += 8; } - if ( my && entitystats->EFFECTS[EFF_DRUNK] && my->behavior == &actPlayer && entitystats->type == GOATMAN ) + if ( entitystats->getEffectActive(EFF_RATION_SOUR) ) + { + INT += 4; + } + if ( entitystats->getEffectActive(EFF_DEMESNE_DOOR) ) + { + INT += entitystats->getEffectActive(EFF_DEMESNE_DOOR); + } + if ( entitystats->getEffectActive(EFF_COUNSEL) ) + { + INT += entitystats->getThaumProficiencySpellStatBonus(STAT_INT, INT); + } + if ( my && entitystats->getEffectActive(EFF_DRUNK) && my->behavior == &actPlayer && entitystats->type == GOATMAN ) { INT -= std::max(8, static_cast(INT * 0.25)); } @@ -6435,6 +9491,13 @@ Sint32 statGetPER(Stat* entitystats, Entity* my) } } + if ( entitystats->getEffectActive(EFF_RATION_BITTER) ) + { + PER += 4; + } + + PER += (Sint32)entitystats->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_EFF_1); + if ( !(svFlags & SV_FLAG_HUNGER) ) { if ( my && my->behavior == &actPlayer && entitystats->playerRace == RACE_INSECTOID && entitystats->stat_appearance == 0 ) @@ -6456,14 +9519,18 @@ Sint32 statGetPER(Stat* entitystats, Entity* my) } } - if ( entitystats->EFFECTS[EFF_SHRINE_GREEN_BUFF] ) + if ( entitystats->getEffectActive(EFF_SHRINE_GREEN_BUFF) ) { PER += 8; } - if ( entitystats->EFFECTS[EFF_POTION_STR] ) + if ( entitystats->getEffectActive(EFF_POTION_STR) ) { PER -= std::max(5, PER / 2); } + if ( entitystats->getEffectActive(EFF_DUSTED) ) + { + PER = 0; + } return PER; } @@ -6547,11 +9614,17 @@ Sint32 statGetCHR(Stat* entitystats, Entity* my) CHR += (cursedItemIsBuff ? abs(entitystats->ring->beatitude) : entitystats->ring->beatitude); } } + + if ( entitystats->getEffectActive(EFF_RATION_HERBAL) ) + { + CHR += 4; + } + if ( entitystats->monsterDemonHasBeenExorcised >= 3 ) { CHR += 5; } - if ( my && entitystats->EFFECTS[EFF_DRUNK] && my->behavior == &actPlayer && entitystats->type == GOATMAN ) + if ( my && entitystats->getEffectActive(EFF_DRUNK) && my->behavior == &actPlayer && entitystats->type == GOATMAN ) { CHR += std::max(4, static_cast(CHR * .25)); } @@ -6584,19 +9657,19 @@ bool Entity::isBlind() } // being blind - if ( entitystats->EFFECTS[EFF_BLIND] == true ) + if ( entitystats->getEffectActive(EFF_BLIND) ) { return true; } // asleep - if ( entitystats->EFFECTS[EFF_ASLEEP] == true ) + if ( entitystats->getEffectActive(EFF_ASLEEP) ) { return true; } // messy face - if ( entitystats->EFFECTS[EFF_MESSY] == true ) + if ( entitystats->getEffectActive(EFF_MESSY) ) { return true; } @@ -6631,13 +9704,21 @@ bool Entity::isWaterWalking() const { return true; } - auto color = MonsterData_t::getKeyFromSprite(sprite, SLIME); + auto& color = MonsterData_t::getKeyFromSprite(sprite, SLIME); if ( color == "slime blue" || color == "slime tar" ) { return true; } } + else if ( stats->type == EARTH_ELEMENTAL ) + { + return true; + } + else if ( stats->type == DUCK_SMALL ) + { + return true; + } } } return false; @@ -6658,12 +9739,20 @@ bool Entity::isLavaWalking() const { return true; } - auto color = MonsterData_t::getKeyFromSprite(sprite, SLIME); + auto& color = MonsterData_t::getKeyFromSprite(sprite, SLIME); if ( color == "slime red" ) { return true; } } + else if ( stats->type == EARTH_ELEMENTAL ) + { + return true; + } + else if ( stats->type == DUCK_SMALL ) + { + return true; + } } } return false; @@ -6691,8 +9780,13 @@ bool Entity::isInvisible() const return false; } + if ( entitystats->getEffectActive(EFF_DUSTED) ) + { + return false; + } + // being invisible - if ( entitystats->EFFECTS[EFF_INVISIBLE] == true ) + if ( entitystats->getEffectActive(EFF_INVISIBLE) ) { return true; } @@ -6752,7 +9846,7 @@ bool Entity::isMobile() return true; } - if ( behavior == &actPlayer && (entitystats->EFFECTS[EFF_PACIFY] || entitystats->EFFECTS[EFF_FEAR]) ) + if ( behavior == &actPlayer && (entitystats->getEffectActive(EFF_PACIFY) || entitystats->getEffectActive(EFF_FEAR)) ) { return false; } @@ -6760,6 +9854,10 @@ bool Entity::isMobile() { return false; } + else if ( behavior == &actPlayer && (entitystats->getEffectActive(EFF_PROJECT_SPIRIT)) ) + { + return false; + } if ( behavior == &actPlayer && (this->skill[9] == MONSTER_POSE_SPECIAL_WINDUP1 || this->skill[9] == PLAYER_POSE_GOLEM_SMASH) // special strike attack @@ -6774,25 +9872,31 @@ bool Entity::isMobile() } // paralyzed - if ( entitystats->EFFECTS[EFF_PARALYZED] ) + if ( entitystats->getEffectActive(EFF_PARALYZED) ) + { + return false; + } + + // paralyzed + if ( entitystats->getEffectActive(EFF_STASIS) ) { return false; } // asleep - if ( entitystats->EFFECTS[EFF_ASLEEP] ) + if ( entitystats->getEffectActive(EFF_ASLEEP) ) { return false; } // stunned - if ( entitystats->EFFECTS[EFF_STUNNED] ) + if ( entitystats->getEffectActive(EFF_STUNNED) ) { return false; } if ( isInertMimic() || (entitystats->type == MIMIC - && (entitystats->EFFECTS[EFF_MIMIC_LOCKED] || monsterSpecialState == MIMIC_MAGIC)) ) + && (entitystats->getEffectActive(EFF_MIMIC_LOCKED) || monsterSpecialState == MIMIC_MAGIC)) ) { return false; } @@ -6802,6 +9906,11 @@ bool Entity::isMobile() return false; } + if ( entitystats->type == DUCK_SMALL && (monsterSpecialState == DUCK_RETURN || monsterSpecialState == DUCK_DIVE) ) + { + return false; + } + if ( (entitystats->type == LICH_FIRE || entitystats->type == LICH_ICE) && monsterLichBattleState < LICH_BATTLE_READY ) { @@ -6820,6 +9929,11 @@ bool Entity::isMobile() { return false; } + + if ( entitystats->type == HOLOGRAM ) + { + return false; + } if ( entitystats->MISC_FLAGS[STAT_FLAG_NPC] != 0 && !strcmp(entitystats->name, "scriptNPC") ) { @@ -6991,11 +10105,19 @@ void Entity::attack(int pose, int charge, Entity* target) if ( player >= 0 ) { players[player]->entity->skill[10] = 0; // PLAYER_ATTACKTIME - if ( pose == MONSTER_POSE_SPECIAL_WINDUP1 || pose == PLAYER_POSE_GOLEM_SMASH || pose == MONSTER_POSE_SPECIAL_WINDUP2 ) + if ( pose == MONSTER_POSE_SPECIAL_WINDUP1 || pose == PLAYER_POSE_GOLEM_SMASH + || pose == MONSTER_POSE_SPECIAL_WINDUP2 + || (pose == MONSTER_POSE_PARRY) ) { players[player]->entity->skill[9] = pose; // PLAYER_ATTACK - if ( pose == MONSTER_POSE_SPECIAL_WINDUP1 || pose == MONSTER_POSE_SPECIAL_WINDUP2 ) + if ( pose == MONSTER_POSE_SPECIAL_WINDUP1 || pose == MONSTER_POSE_SPECIAL_WINDUP2 + || pose == MONSTER_POSE_PARRY ) { + if ( pose == MONSTER_POSE_PARRY ) + { + myStats->parrying = charge; + playSoundEntity(this, 23 + local_rng.rand() % 5, 128); // whoosh noise + } if ( multiplayer == SERVER ) { if ( player >= 0 && player < MAXPLAYERS ) @@ -7029,7 +10151,8 @@ void Entity::attack(int pose, int charge, Entity* target) } else { - if ( pose >= MONSTER_POSE_MELEE_WINDUP1 && pose <= MONSTER_POSE_SPECIAL_WINDUP3 ) + if ( (pose >= MONSTER_POSE_MELEE_WINDUP1 && pose <= MONSTER_POSE_SPECIAL_WINDUP3) + || pose == MONSTER_POSE_PARRY || pose == MONSTER_POSE_FLAIL_SWING_WINDUP || pose == MONSTER_POSE_FLAIL_SWING_RETURN ) { // calls animation, but doesn't actually attack // this branch executes for most monsters @@ -7037,6 +10160,12 @@ void Entity::attack(int pose, int charge, Entity* target) if (myStats->type != SCARAB) { monsterAttackTime = 0; } + + if ( pose == MONSTER_POSE_PARRY ) + { + myStats->parrying = charge; + } + if ( multiplayer == SERVER ) { // be sure to update the clients with the new wind-up pose. @@ -7049,7 +10178,7 @@ void Entity::attack(int pose, int charge, Entity* target) } else if ( (myStats->type == INCUBUS && (pose == MONSTER_POSE_INCUBUS_TELEPORT || pose == MONSTER_POSE_INCUBUS_TAUNT)) || (myStats->type == VAMPIRE && (pose == MONSTER_POSE_VAMPIRE_DRAIN || pose == MONSTER_POSE_VAMPIRE_AURA_CHARGE)) - || (myStats->type == MIMIC + || ( (myStats->type == MIMIC || myStats->type == MINIMIMIC) && (pose == MONSTER_POSE_MIMIC_DISTURBED || pose == MONSTER_POSE_MIMIC_DISTURBED2 || pose == MONSTER_POSE_MIMIC_LOCKED || pose == MONSTER_POSE_MIMIC_LOCKED2 || pose == MONSTER_POSE_MIMIC_MAGIC1 || pose == MONSTER_POSE_MIMIC_MAGIC2)) @@ -7096,6 +10225,7 @@ void Entity::attack(int pose, int charge, Entity* target) { if ( myStats->HP <= 0 && oldHP > 0 ) { + killer->killedByMonsterObituary(this); killer->awardXP(this, true, true); steamAchievementEntity(killer, "BARONY_ACH_LOCKJAW"); } @@ -7115,6 +10245,22 @@ void Entity::attack(int pose, int charge, Entity* target) { monsterAttack = 0; } + else if ( (myStats->type == REVENANT_SKULL + || myStats->type == MONSTER_ADORCISED_WEAPON + || myStats->type == FLAME_ELEMENTAL + || myStats->type == MOTH_SMALL + ) && pose == MONSTER_POSE_MAGIC_CAST1 ) + { + monsterAttack = pose; + } + else if ( myStats->type == MOTH_SMALL ) + { + monsterAttack = pose; + } + else if ( myStats->type == EARTH_ELEMENTAL ) + { + monsterAttack = pose; + } else if ( myStats->weapon != nullptr || myStats->type == CRYSTALGOLEM || myStats->type == COCKATRICE || myStats->type == MIMIC ) @@ -7125,7 +10271,15 @@ void Entity::attack(int pose, int charge, Entity* target) { monsterAttack = 1; // punching } - monsterAttackTime = 0; + + if ( monsterAttack == MONSTER_POSE_FLAIL_SWING ) + { + // don't increment attack time + } + else + { + monsterAttackTime = 0; + } } // special AoE attack. @@ -7184,15 +10338,39 @@ void Entity::attack(int pose, int charge, Entity* target) if ( multiplayer == SERVER ) { - if ( player >= 0 && player < MAXPLAYERS ) + if ( pose == MONSTER_POSE_SWEEP_ATTACK_NO_UPDATE ) + { + // don't update aoe hits + } + else if ( player >= 0 && player < MAXPLAYERS ) { serverUpdateEntitySkill(players[player]->entity, 9); serverUpdateEntitySkill(players[player]->entity, 10); } else { - serverUpdateEntitySkill(this, 8); - if (myStats->type != SLIME && myStats->type != RAT && myStats->type != SCARAB && myStats->type != BAT_SMALL) { + if ( myStats->type == EARTH_ELEMENTAL ) + { + if ( pose != 1 ) + { + serverUpdateEntitySkill(this, 8); // don't update basic hits + } + } + else if ( myStats->type == MONSTER_ADORCISED_WEAPON && target != nullptr ) + { + // don't update special auto attack hits + } + else + { + serverUpdateEntitySkill(this, 8); + } + if (myStats->type != SLIME && myStats->type != RAT /*&& myStats->type != SCARAB*/ + && myStats->type != BAT_SMALL + && myStats->type != REVENANT_SKULL + && myStats->type != MONSTER_ADORCISED_WEAPON + && myStats->type != MOTH_SMALL + && myStats->type != FLAME_ELEMENTAL + && myStats->type != EARTH_ELEMENTAL) { serverUpdateEntitySkill(this, 9); } } @@ -7200,7 +10378,7 @@ void Entity::attack(int pose, int charge, Entity* target) if ( myStats->type == SHADOW ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { //Shadows lose invisibility when they attack. //TODO: How does this play with the passive invisibility? @@ -7245,6 +10423,40 @@ void Entity::attack(int pose, int charge, Entity* target) { isIllusion = true; } + else if ( myStats->type == MONSTER_ADORCISED_WEAPON ) + { + isIllusion = true; + } + + if ( myStats->type == REVENANT_SKULL && pose == MONSTER_POSE_MAGIC_CAST1 ) + { + castSpell(uid, &spell_ghost_bolt, true, false); + return; + } + if ( myStats->type == FLAME_ELEMENTAL && pose == MONSTER_POSE_MAGIC_CAST1 ) + { + castSpell(uid, &spell_fireball, true, false); + return; + } + if ( myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "" + && target == nullptr ) + { + return; // don't hit basic hits, only slide through targeted hits + } + + Entity* sweepAttackTimer = nullptr; + if ( player >= 0 + && pose == 2 && !shapeshifted && myStats->weapon && myStats->weapon->type == CLAYMORE_SWORD + && charge >= Stat::getMaxAttackCharge(myStats) / 2 + && target == nullptr ) + { + sweepAttackTimer = createParticleTimer(this, 3, -1); + sweepAttackTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SWEEP_ATTACK; + + playSoundEntity(this, 770 + local_rng.rand() % 4, 108); // whoosh noise + } + + bool magicstaffStrike = false; if ( myStats->weapon != nullptr && !(myStats->type == BUGBEAR && pose == MONSTER_POSE_BUGBEAR_SHIELD) && (!shapeshifted || (shapeshifted && myStats->type == CREATURE_IMP && itemCategory(myStats->weapon) == MAGICSTAFF)) ) @@ -7262,11 +10474,14 @@ void Entity::attack(int pose, int charge, Entity* target) { if ( itemCategory(myStats->weapon) == MAGICSTAFF ) { + Entity* castSpellResult = nullptr; switch ( myStats->weapon->type ) { case MAGICSTAFF_LIGHT: - castSpell(uid, &spell_light, true, false); + { + castSpellResult = castSpell(uid, &spell_light, true, false); break; + } case MAGICSTAFF_DIGGING: castSpell(uid, &spell_dig, true, false); break; @@ -7309,12 +10524,28 @@ void Entity::attack(int pose, int charge, Entity* target) case MAGICSTAFF_POISON: castSpell(uid, &spell_poison, true, false); break; + case MAGICSTAFF_SCEPTER: + if ( charge == 100 ) + { + castSpell(uid, getSpellFromID(SPELL_SCEPTER_BLAST), true, false); + } + else + { + magicstaffStrike = true; + if ( charge == 99 ) + { + charge = Stat::getMaxAttackCharge(myStats); + messagePlayer(isEntityPlayer(), MESSAGE_EQUIPMENT, Language::get(6839), items[myStats->weapon->type].getIdentifiedName()); + playSoundEntity(this, 163, 128); + } + } + break; default: messagePlayer(player, MESSAGE_DEBUG | MESSAGE_MISC, "This is my wish stick! Wishy wishy wish!"); break; } - if ( behavior == &actPlayer ) + if ( behavior == &actPlayer && !magicstaffStrike ) { Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_MAGICSTAFF_CASTS, myStats->weapon->type, 1); } @@ -7325,6 +10556,14 @@ void Entity::attack(int pose, int charge, Entity* target) { degradeWeapon = false; //certain monster's weapons don't degrade. } + if ( myStats->weapon->type == MAGICSTAFF_SCEPTER ) + { + degradeWeapon = false; + } + if ( myStats->weapon->type == MAGICSTAFF_LIGHT && !castSpellResult ) + { + degradeWeapon = false; + } bool forceDegrade = false; if ( degradeWeapon ) { @@ -7337,8 +10576,9 @@ void Entity::attack(int pose, int charge, Entity* target) } } - if ( (local_rng.rand() % 3 == 0 && degradeWeapon && !(svFlags & SV_FLAG_HARDCORE)) || forceDegrade - || ((svFlags & SV_FLAG_HARDCORE) && local_rng.rand() % 6 == 0 && degradeWeapon) ) + if ( ((local_rng.rand() % 3 == 0 && degradeWeapon && !(svFlags & SV_FLAG_HARDCORE)) || forceDegrade + || ((svFlags & SV_FLAG_HARDCORE) && local_rng.rand() % 6 == 0 && degradeWeapon)) + && !spellEffectPreserveItem(myStats->weapon) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -7398,9 +10638,10 @@ void Entity::attack(int pose, int charge, Entity* target) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = myStats->weapon->status; + SDLNet_Write16((int)myStats->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -7458,7 +10699,7 @@ void Entity::attack(int pose, int charge, Entity* target) { if ( Stat* stats = target->getStats() ) { - if ( target->behavior == &actPlayer && stats->EFFECTS[EFF_ASLEEP] ) + if ( target->behavior == &actPlayer && stats->getEffectActive(EFF_ASLEEP) ) { spell = &spell_magicmissile; } @@ -7552,7 +10793,11 @@ void Entity::attack(int pose, int charge, Entity* target) messagePlayer(player,"You lack the energy to cast magic!"); }*/ } - return; + + if ( !magicstaffStrike ) + { + return; + } } // ranged weapons (bows) @@ -7582,10 +10827,28 @@ void Entity::attack(int pose, int charge, Entity* target) { bowDegradeChance = 100; // conjured skeleton weapon doesn't break. } - if ( bowDegradeChance < 100 && local_rng.rand() % bowDegradeChance == 0 && myStats->weapon->type != ARTIFACT_BOW ) + if ( myStats->type == SENTRYBOT ) + { + bowDegradeChance = 100; + } + if ( player >= 0 ) + { + if ( !players[player]->mechanics.itemDegradeRoll(myStats->weapon, PRO_RANGED) ) + { + bowDegradeChance = 100; + } + } + + if ( bowDegradeChance < 100 && local_rng.rand() % bowDegradeChance == 0 && myStats->weapon->type != ARTIFACT_BOW + && !spellEffectPreserveItem(myStats->weapon) ) { if ( myStats->weapon != NULL ) { + if ( player >= 0 ) + { + players[player]->mechanics.onItemDegrade(myStats->weapon); + } + if ( player >= 0 && players[player]->isLocalPlayer() ) { if ( myStats->weapon->count > 1 ) @@ -7609,9 +10872,10 @@ void Entity::attack(int pose, int charge, Entity* target) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = myStats->weapon->status; + SDLNet_Write16((int)myStats->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -7622,7 +10886,9 @@ void Entity::attack(int pose, int charge, Entity* target) entity = newEntity(78, 1, map.entities, nullptr); // rock playSoundEntity(this, 239 + local_rng.rand() % 3, 96); } - else if ( myStats->weapon->type == CROSSBOW || myStats->weapon->type == HEAVY_CROSSBOW ) + else if ( myStats->weapon->type == CROSSBOW + || myStats->weapon->type == HEAVY_CROSSBOW + || myStats->weapon->type == BLACKIRON_CROSSBOW ) { entity = newEntity(167, 1, map.entities, nullptr); // bolt if ( myStats->weapon->type == HEAVY_CROSSBOW ) @@ -7702,6 +10968,16 @@ void Entity::attack(int pose, int charge, Entity* target) } } } + + if ( behavior == &actMonster && entity ) + { + int accuracy = myStats->monsterRangedAccuracy.getAccuracy(monsterTarget); + if ( accuracy > 0 ) + { + myStats->monsterRangedAccuracy.modifyProjectile(*this, *entity); + } + myStats->monsterRangedAccuracy.incrementAccuracy(); + } return; } @@ -7710,13 +10986,14 @@ void Entity::attack(int pose, int charge, Entity* target) || itemCategory(myStats->weapon) == GEM || itemCategory(myStats->weapon) == THROWN || myStats->weapon->type == FOOD_CREAMPIE - || itemIsThrowableTinkerTool(myStats->weapon) ) + || itemIsThrowableTinkerTool(myStats->weapon) + || myStats->weapon->type == TOOL_DUCK ) { bool drankPotion = false; if ( behavior == &actMonster && myStats->type == GOATMAN && itemCategory(myStats->weapon) == POTION ) { //Goatmen chug potions & then toss them at you. - if ( myStats->weapon->type == POTION_BOOZE && !myStats->EFFECTS[EFF_DRUNK] ) + if ( myStats->weapon->type == POTION_BOOZE && !myStats->getEffectActive(EFF_DRUNK) ) { item_PotionBooze(myStats->weapon, this, this, false); drankPotion = true; @@ -7737,7 +11014,11 @@ void Entity::attack(int pose, int charge, Entity* target) { playSoundEntity(this, 75, 64); //playSoundEntity(this, 427 + local_rng.rand() % 4, 128); - + } + else if ( myStats->weapon->type == BOLAS ) + { + playSoundEntity(this, 23 + local_rng.rand() % 5, 128); + playSoundEntity(this, 767 + local_rng.rand() % 3, 128); } else { @@ -7789,7 +11070,7 @@ void Entity::attack(int pose, int charge, Entity* target) if ( itemCategory(myStats->weapon) == THROWN ) { real_t speed = 5.f; - real_t normalisedCharge = (charge * 1.5 / MAXCHARGE); // 0-1.5 + real_t normalisedCharge = (charge * 1.5 / Stat::getMaxAttackCharge(myStats)); // 0-1.5 if ( myStats->weapon->type == BOOMERANG ) { speed = 3.75 + normalisedCharge; //3.75 @@ -7799,8 +11080,27 @@ void Entity::attack(int pose, int charge, Entity* target) speed = 5.f + normalisedCharge; } + + if ( myStats->weapon->type == GREASE_BALL + || myStats->weapon->type == DUST_BALL + || myStats->weapon->type == SLOP_BALL ) + { + speed = 2.f + normalisedCharge; + if ( this->behavior == &actPlayer ) + { + entity->vel_x = speed * cos(players[player]->entity->yaw); + entity->vel_y = speed * sin(players[player]->entity->yaw); + entity->vel_z = -.5; + } + else if ( this->behavior == &actMonster ) + { + entity->vel_x = 4.0 * cos(this->yaw); + entity->vel_y = 4.0 * sin(this->yaw); + entity->vel_z = -.3; + } + } // thrown items have slightly faster velocities - if ( (myStats->weapon->type == STEEL_CHAKRAM || myStats->weapon->type == CRYSTAL_SHURIKEN) ) + else if ( (myStats->weapon->type == STEEL_CHAKRAM || myStats->weapon->type == CRYSTAL_SHURIKEN) ) { if ( this->behavior == &actPlayer ) { @@ -7859,7 +11159,7 @@ void Entity::attack(int pose, int charge, Entity* target) else if ( itemIsThrowableTinkerTool(myStats->weapon) ) { real_t normalisedCharge = (charge * 0.5); - normalisedCharge /= MAXCHARGE; + normalisedCharge /= Stat::getMaxAttackCharge(myStats); entity->sizex = 4; entity->sizey = 4; if ( myStats->weapon->type >= TOOL_BOMB && myStats->weapon->type <= TOOL_TELEPORT_BOMB ) @@ -7884,12 +11184,30 @@ void Entity::attack(int pose, int charge, Entity* target) } } } + else if ( myStats->weapon->type == GEM_JEWEL ) + { + real_t normalisedCharge = (charge * 1.0); + normalisedCharge /= Stat::getMaxAttackCharge(myStats); + entity->sizex = 2; + entity->sizey = 2; + if ( behavior == &actPlayer ) + { + entity->vel_x = (1.f + normalisedCharge) * cos(players[player]->entity->yaw); + entity->vel_y = (1.f + normalisedCharge) * sin(players[player]->entity->yaw); + } + entity->vel_z = -.3; + } else { + if ( entity->skill[10] == TOOL_DUCK ) + { + entity->sizex = 2; + entity->sizey = 2; + } real_t speed = 5.f; if ( itemCategory(myStats->weapon) == GEM ) { - real_t normalisedCharge = (charge * 1.5 / MAXCHARGE); // 0-1.5 + real_t normalisedCharge = (charge * 1.5 / Stat::getMaxAttackCharge(myStats)); // 0-1.5 speed = 3.f + normalisedCharge; if ( behavior == &actPlayer ) { @@ -7911,6 +11229,16 @@ void Entity::attack(int pose, int charge, Entity* target) } } + if ( behavior == &actMonster && entity ) + { + int accuracy = myStats->monsterRangedAccuracy.getAccuracy(monsterTarget); + if ( accuracy > 0 ) + { + myStats->monsterRangedAccuracy.modifyProjectile(*this, *entity); + } + myStats->monsterRangedAccuracy.incrementAccuracy(); + } + if ( behavior == &actPlayer ) { Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_THROWN, @@ -7934,20 +11262,52 @@ void Entity::attack(int pose, int charge, Entity* target) return; } } - bool whip = myStats->weapon && myStats->weapon->type == TOOL_WHIP; + bool whip = myStats->weapon && myStats->weapon->type == TOOL_WHIP && !shapeshifted; + bool flail = myStats->weapon && myStats->weapon->type == STEEL_FLAIL && !shapeshifted; bool miss = false; + bool guard = false; + int strikeRange = STRIKERANGE; // normal attacks if ( target == nullptr ) { - if ( whip ) + if ( flail ) + { + strikeRange = STRIKERANGE * 1.5; + dist = lineTrace(this, x, y, yaw, strikeRange, LINETRACE_ATK_CHECK_FRIENDLYFIRE, false); + if ( behavior == &actMonster || charge >= Stat::getMaxAttackCharge(myStats) / 2 ) + { + playSoundEntity(this, 23 + local_rng.rand() % 5, 128); // whoosh noise + } + else + { + playSoundEntity(this, 770 + local_rng.rand() % 4, 108); // whoosh noise + } + } + else if ( whip ) { - dist = lineTrace(this, x, y, yaw, STRIKERANGE * 1.5, LINETRACE_ATK_CHECK_FRIENDLYFIRE, false); + strikeRange = STRIKERANGE * 1.5; + dist = lineTrace(this, x, y, yaw, strikeRange, LINETRACE_ATK_CHECK_FRIENDLYFIRE, false); playSoundEntity(this, 23 + local_rng.rand() % 5, 128); // whoosh noise } else { - playSoundEntity(this, 23 + local_rng.rand() % 5, 128); // whoosh noise - dist = lineTrace(this, x, y, yaw, STRIKERANGE, LINETRACE_ATK_CHECK_FRIENDLYFIRE, false); + dist = lineTrace(this, x, y, yaw, strikeRange, LINETRACE_ATK_CHECK_FRIENDLYFIRE, false); + if ( sweepAttackTimer ) + { + // already played the sound + if ( hit.entity ) + { + if ( auto hitProps = getParticleEmitterHitProps(sweepAttackTimer->getUID(), hit.entity) ) + { + hitProps->hits++; + hitProps->tick = ::ticks; + } + } + } + else + { + playSoundEntity(this, 23 + local_rng.rand() % 5, 128); // whoosh noise + } } if ( hit.entity && (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) ) @@ -7958,11 +11318,53 @@ void Entity::attack(int pose, int charge, Entity* target) { miss = true; } + else if ( hitstats && hitstats->getEffectActive(EFF_MAGICIANS_ARMOR) + && !(!(svFlags & SV_FLAG_FRIENDLYFIRE) && checkFriend(hit.entity) && this->friendlyFireProtection(hit.entity)) /*dont apply to friendly fire */ ) + { + guard = true; + Uint8 effectStrength = hitstats->getEffectActive(EFF_MAGICIANS_ARMOR); + if ( effectStrength == 1 ) + { + if ( hitstats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR] > 0 ) + { + hitstats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR] = 1; + } + } + else if ( effectStrength > 1 ) + { + --effectStrength; + hitstats->setEffectValueUnsafe(EFF_MAGICIANS_ARMOR, effectStrength); + hit.entity->setEffect(EFF_MAGICIANS_ARMOR, effectStrength, hitstats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR], true); + } + + magicOnSpellCastEvent(hit.entity, hit.entity, this, SPELL_MAGICIANS_ARMOR, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + if ( hit.entity->behavior == &actPlayer ) + { + steamStatisticUpdateClient(hit.entity->skill[2], STEAM_STAT_DOESNT_COUNT, STEAM_STAT_INT, 1); + } + + Entity* fx = createParticleAestheticOrbit(hit.entity, 1817, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE); + fx->x = hit.entity->x; + fx->y = hit.entity->y; + fx->z = hit.entity->z; + real_t tangent = atan2(this->y - hit.entity->y, this->x - hit.entity->x); + fx->x += 4.0 * cos(tangent); + fx->y += 4.0 * sin(tangent); + fx->yaw = tangent; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 0; + serverSpawnMiscParticlesAtLocation(fx->x, fx->y, fx->z, PARTICLE_EFFECT_NULL_PARTICLE, 1817, 0, fx->yaw * 256.0); + } else if ( bat && hit.entity->monsterSpecialState == BAT_REST ) { miss = false; } - else if ( bat || (hitstats && hitstats->EFFECTS[EFF_AGILITY]) ) + else if ( bat || (hitstats && (hitstats->getEffectActive(EFF_AGILITY) + || hit.entity->mistFormDodge(true, this) + || (hitstats->getEffectActive(EFF_MAGIC_GREASE) && hitstats->type == GREMLIN) + || (hitstats && hitstats->type == DRYAD + && hit.entity->behavior == &actPlayer && hitstats->sex == FEMALE) + || hitstats->getEffectActive(EFF_ENSEMBLE_LUTE))) || myStats->getEffectActive(EFF_BLIND) ) { Sint32 previousMonsterState = hit.entity->monsterState; bool backstab = false; @@ -7979,9 +11381,10 @@ void Entity::attack(int pose, int charge, Entity* target) } else { - if ( previousMonsterState == MONSTER_STATE_WAIT + if ( (previousMonsterState == MONSTER_STATE_WAIT || previousMonsterState == MONSTER_STATE_PATH - || (previousMonsterState == MONSTER_STATE_HUNT && uidToEntity(monsterTarget) == nullptr) ) + || (previousMonsterState == MONSTER_STATE_HUNT && uidToEntity(hit.entity->monsterTarget) == nullptr)) + && !hitstats->getEffectActive(EFF_ROOTED) ) { // unaware monster, get backstab damage. backstab = true; @@ -7995,18 +11398,75 @@ void Entity::attack(int pose, int charge, Entity* target) } } - if ( backstab ) + if ( hit.entity->mistFormDodge(false, this) ) + { + miss = true; + } + else if ( backstab ) { miss = false; + + if ( hitstats->type == DRYAD && hitstats->sex == FEMALE && hit.entity->behavior == &actPlayer ) + { + int baseChance = 5; + miss = players[hit.entity->skill[2]]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_EVASION, baseChance); + } + + if ( myStats->weapon ) + { + if ( myStats->weapon->type == ARTIFACT_SPEAR && !shapeshifted ) + { + miss = false; + } + } } else { - int baseChance = bat ? 6 : 3; - if ( flanking ) + int baseChance = 0; + if ( bat ) + { + baseChance = std::max(baseChance, 60); + } + if ( hitstats && hitstats->getEffectActive(EFF_AGILITY) ) + { + baseChance = std::max(baseChance, 30); + } + if ( hitstats && hitstats->getEffectActive(EFF_MAGIC_GREASE) && hitstats->type == GREMLIN ) + { + baseChance = std::max(baseChance, 20); + } + if ( hitstats && hitstats->type == DRYAD + && hit.entity->behavior == &actPlayer && hitstats->sex == FEMALE ) + { + baseChance = std::max(baseChance, 5); + } + if ( hitstats && hitstats->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_TIER) > 0.001 ) + { + baseChance = std::max(baseChance, static_cast(hitstats->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_TIER))); + } + if ( myStats->getEffectActive(EFF_BLIND) ) + { + baseChance = std::max(baseChance, 75); + } + if ( baseChance <= 0 ) + { + miss = false; + } + else { - baseChance = std::max(1, baseChance - 2); + if ( flanking ) + { + baseChance = std::max(1, baseChance - 20); + } + if ( hit.entity->behavior == &actPlayer ) + { + miss = players[hit.entity->skill[2]]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_EVASION, baseChance); + } + else + { + miss = local_rng.rand() % 100 < baseChance; + } } - miss = local_rng.rand() % 10 < baseChance; } if ( myStats->weapon ) @@ -8018,23 +11478,51 @@ void Entity::attack(int pose, int charge, Entity* target) } } - if ( miss ) + if ( miss || guard ) { if ( !hit.entity->isUntargetableBat() ) { if ( player >= 0 || (behavior == &actMonster && monsterAllyGetPlayerLeader()) - || hit.entity->behavior == &actPlayer || hit.entity->monsterAllyGetPlayerLeader() ) + || hit.entity->behavior == &actPlayer || hit.entity->monsterAllyGetPlayerLeader() + || (hitstats && achievementObserver.checkUidIsFromPlayer(hitstats->leader_uid) >= 0) ) { - spawnDamageGib(hit.entity, 0, DamageGib::DMG_MISS, DamageGibDisplayType::DMG_GIB_MISS, true); + if ( guard ) + { + spawnDamageGib(hit.entity, 0, DamageGib::DMG_GUARD, DamageGibDisplayType::DMG_GIB_GUARD, true); + } + else if ( miss ) + { + spawnDamageGib(hit.entity, 0, DamageGib::DMG_MISS, DamageGibDisplayType::DMG_GIB_MISS, true); + } } - if ( player >= 0 && bat ) + if ( player >= 0 && bat && miss ) { steamStatisticUpdateClient(player, STEAM_STAT_PITCH_PERFECT, STEAM_STAT_INT, 1); } + if ( guard ) + { + //playSoundEntity(hit.entity, 166, 128); + if ( this->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(this->skill[2], makeColorRGB(255, 255, 255), + *hitstats, Language::get(6468), Language::get(6469), MSG_COMBAT); // %s guards the attack + } + } if ( hit.entity->behavior == &actPlayer ) { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6286)); + if ( miss ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6286)); + if ( hitstats->type == DRYAD ) + { + hit.entity->playerShakeGrowthHelmet(); + } + } + else if ( guard ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6465)); + } } else if ( hit.entity->behavior == &actMonster ) { @@ -8042,7 +11530,7 @@ void Entity::attack(int pose, int charge, Entity* target) if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( checkFriend(hit.entity) ) + if ( checkFriend(hit.entity) && this->friendlyFireProtection(hit.entity) ) { doHitAlert = false; } @@ -8053,12 +11541,7 @@ void Entity::attack(int pose, int charge, Entity* target) Stat* hitstats = hit.entity->getStats(); if ( hitstats ) { - bool alertTarget = true; - if ( behavior == &actMonster && monsterAllyIndex != -1 && hit.entity->monsterAllyIndex != -1 ) - { - // if we're both allies of players, don't alert the hit target. - alertTarget = false; - } + bool alertTarget = hit.entity->monsterAlertBeforeHit(this); // alert the monster! if ( hit.entity->monsterState != MONSTER_STATE_ATTACK ) @@ -8096,7 +11579,7 @@ void Entity::attack(int pose, int charge, Entity* target) if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( checkFriend(hit.entity) ) + if ( checkFriend(hit.entity) && this->friendlyFireProtection(hit.entity) ) { return; } @@ -8228,7 +11711,8 @@ void Entity::attack(int pose, int charge, Entity* target) Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_PICKAXE_BOULDERS_DUG, myStats->weapon->type, 1); } - if ( myStats->weapon && local_rng.rand() % 2 && pose != PLAYER_POSE_GOLEM_SMASH ) + if ( myStats->weapon && local_rng.rand() % 2 && pose != PLAYER_POSE_GOLEM_SMASH + && !spellEffectPreserveItem(myStats->weapon) ) { myStats->weapon->status = static_cast(myStats->weapon->status - 1); if ( myStats->weapon->status < BROKEN ) @@ -8261,9 +11745,10 @@ void Entity::attack(int pose, int charge, Entity* target) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = myStats->weapon->status; + SDLNet_Write16((int)myStats->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -8310,12 +11795,7 @@ void Entity::attack(int pose, int charge, Entity* target) hitstats = hit.entity->getStats(); if ( hitstats ) { - bool alertTarget = true; - if ( behavior == &actMonster && monsterAllyIndex != -1 && hit.entity->monsterAllyIndex != -1 ) - { - // if we're both allies of players, don't alert the hit target. - alertTarget = false; - } + bool alertTarget = hit.entity->monsterAlertBeforeHit(this); // alert the monster! if ( hit.entity->monsterState != MONSTER_STATE_ATTACK && (hitstats->type < LICH || hitstats->type >= SHOPKEEPER) ) @@ -8398,7 +11878,8 @@ void Entity::attack(int pose, int charge, Entity* target) { magicDig(this, nullptr, 1, 0); - if ( myStats->weapon && local_rng.rand() % 2 && pose != PLAYER_POSE_GOLEM_SMASH ) + if ( myStats->weapon && local_rng.rand() % 2 && pose != PLAYER_POSE_GOLEM_SMASH + && !spellEffectPreserveItem(myStats->weapon) ) { myStats->weapon->status = static_cast(myStats->weapon->status - 1); if ( myStats->weapon->status < BROKEN ) @@ -8431,14 +11912,17 @@ void Entity::attack(int pose, int charge, Entity* target) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = myStats->weapon->status; + SDLNet_Write16((int)myStats->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } } - else if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &::actFurniture || hit.entity->behavior == &::actChest + else if ( hit.entity->behavior == &actDoor + || hit.entity->behavior == &::actIronDoor + || hit.entity->behavior == &::actFurniture || hit.entity->behavior == &::actChest || mimic || (hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMelee()) ) { @@ -8450,6 +11934,7 @@ void Entity::attack(int pose, int charge, Entity* target) int axe = 0; int damage = 1; int weaponskill = -1; + if ( myStats->weapon && !shapeshifted ) { weaponskill = getWeaponSkill(myStats->weapon); @@ -8508,6 +11993,23 @@ void Entity::attack(int pose, int charge, Entity* target) axe = std::min(axe, 9); } } + + if ( myStats->type == GREMLIN ) + { + axe += 1; + if ( this->behavior == &actPlayer ) + { + axe += 1 * players[this->skill[2]]->mechanics.getBreakableCounterTier(); + } + } + if ( myStats->type == GNOME ) + { + if ( !shapeshifted && myStats->weapon && myStats->weapon->type == TOOL_PICKAXE ) + { + axe += 2 + local_rng.rand() % 3; + } + } + if ( pose == PLAYER_POSE_GOLEM_SMASH ) { if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &::actFurniture ) @@ -8523,15 +12025,20 @@ void Entity::attack(int pose, int charge, Entity* target) else { damage += axe; - if ( charge >= MAXCHARGE / 2 ) + if ( charge >= Stat::getMaxAttackCharge(myStats) / 2 ) { damage *= 2; } } + if ( hit.entity->behavior == &::actIronDoor ) + { + damage = 0; + } + int& entityHP = hit.entity->behavior == &actColliderDecoration ? hit.entity->colliderCurrentHP : (hit.entity->behavior == &::actChest ? hit.entity->chestHealth : - (hit.entity->behavior == &actDoor ? hit.entity->doorHealth : + ((hit.entity->behavior == &actDoor || hit.entity->behavior == &::actIronDoor) ? hit.entity->doorHealth : ((mimic && hitstats) ? hitstats->HP : hit.entity->furnitureHealth))); int oldHP = entityHP; @@ -8556,6 +12063,10 @@ void Entity::attack(int pose, int charge, Entity* target) { messagePlayer(player, MESSAGE_COMBAT_BASIC, Language::get(666)); } + else if ( hit.entity->behavior == &::actIronDoor ) + { + messagePlayer(player, MESSAGE_COMBAT_BASIC, Language::get(6412)); + } else if ( hit.entity->behavior == &::actChest ) { messagePlayer(player, MESSAGE_COMBAT_BASIC, Language::get(667)); @@ -8564,12 +12075,7 @@ void Entity::attack(int pose, int charge, Entity* target) { messagePlayer(player, MESSAGE_COMBAT_BASIC, Language::get(667)); previousMonsterState = hit.entity->monsterState; - bool alertTarget = true; - if ( behavior == &actMonster && monsterAllyIndex != -1 && hit.entity->monsterAllyIndex != -1 ) - { - // if we're both allies of players, don't alert the hit target. - alertTarget = false; - } + bool alertTarget = hit.entity->monsterAlertBeforeHit(this); // alert the monster! if ( hit.entity->monsterState != MONSTER_STATE_ATTACK ) @@ -8614,6 +12120,52 @@ void Entity::attack(int pose, int charge, Entity* target) else { entityHP = 0; + + if ( oldHP > 0 ) + { + if ( behavior == &actPlayer ) + { + if ( weaponskill >= 0 && (weaponskill != PRO_RANGED || whip) ) + { + if ( myStats->getProficiency(weaponskill) < SKILL_LEVEL_BASIC + && local_rng.rand() % 20 == 0 ) + { + bool skillIncreased = this->increaseSkill(weaponskill); + if ( skillIncreased && myStats->type == GOBLIN && weaponskill != PRO_RANGED ) + { + // goblins level up all combat skills at once. + int numIncreases = 0; + if ( weaponskill != PRO_SWORD ) + { + numIncreases += this->increaseSkill(PRO_SWORD, false) ? 1 : 0; + } + if ( weaponskill != PRO_MACE ) + { + numIncreases += this->increaseSkill(PRO_MACE, false) ? 1 : 0; + } + if ( weaponskill != PRO_AXE ) + { + numIncreases += this->increaseSkill(PRO_AXE, false) ? 1 : 0; + } + if ( weaponskill != PRO_POLEARM ) + { + numIncreases += this->increaseSkill(PRO_POLEARM, false) ? 1 : 0; + } + if ( weaponskill != PRO_UNARMED ) + { + numIncreases += this->increaseSkill(PRO_UNARMED, false) ? 1 : 0; + } + if ( player >= 0 && numIncreases > 0 ) + { + Uint32 color = makeColorRGB(255, 255, 0); + messagePlayerColor(player, MESSAGE_PROGRESSION, color, Language::get(3446)); + } + } + } + } + } + } + if ( hit.entity->behavior == &actDoor ) { messagePlayer(player, MESSAGE_COMBAT, Language::get(670)); @@ -8627,6 +12179,19 @@ void Entity::attack(int pose, int charge, Entity* target) } Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_DOOR_BROKEN, "door", 1); } + else if ( hit.entity->behavior == &::actIronDoor ) + { + messagePlayer(player, MESSAGE_COMBAT, Language::get(6413)); + if ( !hit.entity->skill[0] ) + { + hit.entity->skill[6] = (x > hit.entity->x); + } + else + { + hit.entity->skill[6] = (y < hit.entity->y); + } + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_DOOR_BROKEN, "iron door", 1); + } else if ( hit.entity->behavior == &::actChest ) { messagePlayer(player, MESSAGE_COMBAT, Language::get(671)); @@ -8639,8 +12204,11 @@ void Entity::attack(int pose, int charge, Entity* target) else if ( hit.entity->isDamageableCollider() ) { hit.entity->colliderKillerUid = getUID(); - messagePlayer(player, MESSAGE_COMBAT, Language::get(hit.entity->getColliderOnBreakLangEntry()), - Language::get(hit.entity->getColliderLangName())); + if ( hit.entity->getColliderOnBreakLangEntry() != 0 ) + { + messagePlayer(player, MESSAGE_COMBAT, Language::get(hit.entity->getColliderOnBreakLangEntry()), + Language::get(hit.entity->getColliderLangName())); + } if ( hit.entity->isColliderWall() ) { Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_BARRIER_DESTROYED, "breakable barriers", 1); @@ -8680,10 +12248,16 @@ void Entity::attack(int pose, int charge, Entity* target) break; } } + + if ( behavior == &actPlayer ) + { + players[this->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_COMMON, hit.entity); + } } - if ( hit.entity->behavior == &actDoor ) + if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &::actIronDoor ) { - updateEnemyBar(this, hit.entity, Language::get(674), entityHP, hit.entity->skill[9], false, + updateEnemyBar(this, hit.entity, hit.entity->behavior == &::actIronDoor ? Language::get(6414) : Language::get(674), + entityHP, hit.entity->doorMaxHealth, false, DamageGib::DMG_DEFAULT); } else if ( hit.entity->behavior == &::actChest ) @@ -8695,6 +12269,91 @@ void Entity::attack(int pose, int charge, Entity* target) { updateEnemyBar(this, hit.entity, Language::get(hit.entity->getColliderLangName()), entityHP, hit.entity->colliderMaxHP, false, DamageGib::DMG_DEFAULT); + + int thornsEffect = 0; + Entity* colliderParent = hit.entity->colliderCreatedParent != 0 ? uidToEntity(hit.entity->colliderCreatedParent) : nullptr; + if ( hit.entity->colliderSpellEvent % 1000 == 8 ) + { + thornsEffect += getSpellDamageFromID(SPELL_THORNS, colliderParent, nullptr, hit.entity); + if ( colliderParent && colliderParent->behavior == &actPlayer ) + { + magicOnSpellCastEvent(colliderParent, hit.entity, this, SPELL_SHRUB, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, thornsEffect); + } + } + else if ( hit.entity->colliderSpellEvent % 1000 == 9 ) + { + thornsEffect += getSpellDamageFromID(SPELL_BLADEVINES, colliderParent, nullptr, hit.entity); + if ( colliderParent && colliderParent->behavior == &actPlayer ) + { + magicOnSpellCastEvent(colliderParent, hit.entity, this, SPELL_SHRUB, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, thornsEffect); + } + } + + if ( thornsEffect != 0 ) + { + if ( hit.entity->colliderSpellEvent % 1000 == 9 ) + { + if ( !myStats->getEffectActive(EFF_BLEEDING) ) + { + if ( setEffect(EFF_BLEEDING, true, 3 * TICKS_PER_SECOND, false) ) + { + if ( colliderParent != this ) + { + myStats->bleedInflictedBy = colliderParent ? colliderParent->getUID() : 0; + } + for ( int gibs = 0; gibs < 3; ++gibs ) + { + Entity* gib = spawnGib(this); + serverSpawnGibForClient(gib); + } + } + } + } + + this->modHP(-abs(thornsEffect)); + if ( myStats->HP <= 0 && myStats->OLDHP > myStats->HP ) + { + if ( colliderParent && (colliderParent->behavior == &actMonster || colliderParent->behavior == &actPlayer) ) + { + colliderParent->killedByMonsterObituary(this); + colliderParent->awardXP(this, true, true); + } + } + if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 10; // turns into .1 + net_packet->data[5] = 10; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + else if ( player >= 0 && players[player]->isLocalPlayer() ) + { + cameravars[player].shakex += 0.1; + cameravars[player].shakey += 10; + } + + if ( player >= 0 ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(6263), + Language::get(hit.entity->getColliderLangName())); // you were hurt attacking the %s + } + + // update enemy bar for attacker + if ( !strcmp(myStats->name, "") ) + { + updateEnemyBar(colliderParent, this, getMonsterLocalizedName(myStats->type).c_str(), myStats->HP, myStats->MAXHP, false, + DamageGib::DMG_DEFAULT); + } + else + { + updateEnemyBar(colliderParent, this, myStats->name, myStats->HP, myStats->MAXHP, false, + DamageGib::DMG_DEFAULT); + } + } } else if ( hit.entity->behavior == &::actFurniture ) { @@ -8733,6 +12392,7 @@ void Entity::attack(int pose, int charge, Entity* target) if ( hitstats->HP == 0 && oldHP > 0 ) { messagePlayerMonsterEvent(player, makeColorRGB(0, 255, 0), *hitstats, Language::get(692), Language::get(692), MSG_COMBAT); + this->killedByMonsterObituary(hit.entity); awardXP(hit.entity, true, true); } } @@ -8854,17 +12514,20 @@ void Entity::attack(int pose, int charge, Entity* target) } real_t weaponMultipliers = 0.0; + DamageTableType dmgType = DAMAGE_TABLE_UNARMED; if ( weaponskill == PRO_UNARMED ) { + dmgType = DAMAGE_TABLE_UNARMED; weaponMultipliers = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_UNARMED); } else if ( weaponskill == PRO_RANGED ) { + dmgType = DAMAGE_TABLE_RANGED; weaponMultipliers = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_RANGED); } else if ( weaponskill >= 0 ) { - DamageTableType dmgType = static_cast(weaponskill - PRO_SWORD); + dmgType = static_cast(weaponskill - PRO_SWORD); weaponMultipliers = Entity::getDamageTableMultiplier(hit.entity, *hitstats, dmgType); } @@ -8880,40 +12543,125 @@ void Entity::attack(int pose, int charge, Entity* target) thornsEffect = -2 * (1 + abs(hitstats->mask->beatitude)); } } + if ( hitstats->getEffectActive(EFF_ENSEMBLE_HORN) ) + { + thornsEffect += static_cast(hitstats->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_TIER)); + } bool dyrnwynSmite = false; + bool zealSmite = false; bool gugnirProc = false; + bool armorPierceProc = false; + bool whipPierce = false; + if ( whip ) + { + int statChance = std::min(std::max(this->getPER(), 0), 50); // 0 to 50 value. + if ( behavior == &actMonster ) + { + statChance = std::min(std::max(this->getPER() / 2, 0), 50); + } + int chance = local_rng.rand() % 100; + if ( chance < statChance ) + { + whipPierce = true; + armorPierceProc = true; + if ( this->behavior == &actMonster ) + { + weaponMultipliers += std::min(25, std::max(0, statGetPER(myStats, this))) / 100.0; + } + else + { + weaponMultipliers += std::max(0, statGetPER(myStats, this)) / 100.0; + } + } + } - if ( weaponskill == PRO_SWORD && myStats->weapon && myStats->weapon->type == ARTIFACT_SWORD && !shapeshifted ) + Entity::modifyDamageMultipliersFromEffects(hit.entity, this, weaponMultipliers, dmgType); + + if ( (hitstats->getEffectActive(EFF_DIVINE_FIRE) & 0xF) || + ( + (!shapeshifted && weaponskill == PRO_SWORD && myStats->weapon && myStats->weapon->type == ARTIFACT_SWORD) + || (/*myStats->weapon &&*/ myStats->getEffectActive(EFF_DIVINE_ZEAL)) + || (/*myStats->weapon &&*/ myStats->getEffectActive(EFF_FOCI_LIGHT_JUSTICE))) + ) { - switch ( hitstats->type ) + bool particle = false; + + bool effect = false; + if ( (hitstats->getEffectActive(EFF_DIVINE_FIRE) & 0xF) ) { - case SKELETON: - case CREATURE_IMP: - case GHOUL: - case DEMON: - case SUCCUBUS: - case INCUBUS: - case VAMPIRE: - case LICH: - case LICH_ICE: - case LICH_FIRE: - case DEVIL: + particle = true; + effect = true; + } + + if ( hit.entity->isSmiteWeakMonster() ) + { + // smite these creatures + if ( weaponskill == PRO_SWORD && myStats->weapon && myStats->weapon->type == ARTIFACT_SWORD + && !shapeshifted ) { - // smite these creatures real_t amount = 0.0; real_t percent = getArtifactWeaponEffectChance(myStats->weapon->type, *myStats, &amount); if ( local_rng.rand() % 100 < static_cast(percent) ) { weaponMultipliers += amount; dyrnwynSmite = true; - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 981); + particle = true; + effect = true; //playSoundEntity(hit.entity, 249, 64); } - break; } - default: - break; + if ( !dyrnwynSmite ) + { + if ( myStats->getEffectActive(EFF_DIVINE_ZEAL) ) + { + zealSmite = true; + particle = true; + effect = true; + real_t bonus = getSpellDamageFromID(SPELL_DIVINE_ZEAL, this, myStats, this) / 100.0; + bonus = std::min(bonus, getSpellDamageSecondaryFromID(SPELL_DIVINE_ZEAL, nullptr, nullptr, nullptr) / 100.0); + weaponMultipliers += bonus; + + if ( behavior == &actPlayer ) + { + players[skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_DIVINE_ZEAL, bonus * 100.0, 1.0, hit.entity); + } + } + if ( myStats->getEffectActive(EFF_FOCI_LIGHT_JUSTICE) ) + { + effect = true; + zealSmite = true; + particle = true; + weaponMultipliers += getSpellEffectDurationSecondaryFromID(SPELL_FOCI_LIGHT_JUSTICE, nullptr, nullptr, nullptr) / 100.0; + int tier = std::max(0, (myStats->getEffectActive(EFF_FOCI_LIGHT_JUSTICE) - 1)); + weaponMultipliers += tier * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_JUSTICE, nullptr, nullptr, nullptr) / 100.0; + } + } + } + + if ( effect ) + { + if ( myStats->type == SALAMANDER + && myStats->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + if ( myStats->EFFECTS_TIMERS[EFF_SALAMANDER_HEART] > 0 ) + { + int prevDuration = myStats->EFFECTS_TIMERS[EFF_SALAMANDER_HEART]; + myStats->EFFECTS_TIMERS[EFF_SALAMANDER_HEART] += TICKS_PER_SECOND / 2; + if ( prevDuration < 5 * TICKS_PER_SECOND && myStats->EFFECTS_TIMERS[EFF_SALAMANDER_HEART] >= 5 * TICKS_PER_SECOND ) + { + if ( this->behavior == &actPlayer ) + { + serverUpdateEffects(skill[2]); // update timer + } + } + } + } + } + + if ( particle ) + { + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 981); } } /*if( weaponskill>=0 ) @@ -8930,6 +12678,60 @@ void Entity::attack(int pose, int charge, Entity* target) } if ( hitsuccess ) */ { + /*if ( myStats->weapon && myStats->weapon->type == RAPIER && !shapeshifted ) + { + if ( *cvar_rapier_toggle == 3 ) + { + myStats->parrying = 35; + } + else if ( charge < Stat::getMaxAttackCharge(myStats) ) + { + if ( *cvar_rapier_toggle == 2 ) + { + myStats->parrying = 15; + } + } + }*/ + + Sint32 parriedDamage = 0; + int parriedSkill = -1; + if ( hitstats->parrying > 0 ) + { + if ( !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) + { + if ( !hitstats->weapon || (hitstats->weapon && itemCategory(hitstats->weapon) != WEAPON) ) + { + hitstats->parrying = 0; + } + else + { + parriedDamage = hitstats->weapon && hitstats->weapon->type == RAPIER ? + Entity::getAttack(hit.entity, hitstats, hit.entity->behavior == &actPlayer, 0) : 0; + + parriedSkill = getWeaponSkill(hitstats->weapon); + if ( hitstats->weapon && hitstats->weapon->type == RAPIER ) + { + real_t variance = 20; + real_t baseSkillModifier = 50.0; // 40-60 base + Entity::setMeleeDamageSkillModifiers(hit.entity, hitstats, parriedSkill, baseSkillModifier, variance, nullptr); + real_t skillModifier = baseSkillModifier - (variance / 2) + (hitstats->getModifiedProficiency(parriedSkill) / 2.0); + skillModifier += (local_rng.rand() % (1 + static_cast(variance))); + skillModifier /= 100.0; + skillModifier = std::min(skillModifier, 1.0); + parriedDamage = parriedDamage - static_cast((1.0 - skillModifier) * parriedDamage); + } + + parriedDamage = std::max(1, parriedDamage); + + if ( hit.entity->behavior == &actPlayer && hitstats->weapon ) + { + Compendium_t::Events_t::eventUpdate(hit.entity->skill[2], + Compendium_t::CPDM_PARRIES, hitstats->weapon->type, 1); + } + } + } + } + // calculate and perform damage to opponent int damage = 0; int damagePreMultiplier = 1; @@ -8944,7 +12746,7 @@ void Entity::attack(int pose, int charge, Entity* target) damagePreMultiplier = 2; } - int myAttack = std::max(0, (Entity::getAttack(this, myStats, behavior == &actPlayer) * damagePreMultiplier) + getBonusAttackOnTarget(*hitstats)); + int myAttack = std::max(0, (Entity::getAttack(this, myStats, behavior == &actPlayer, charge) * damagePreMultiplier) + getBonusAttackOnTarget(*hitstats)); if ( myStats->type == BUGBEAR && pose == MONSTER_POSE_BUGBEAR_SHIELD ) { myAttack += 2 + local_rng.rand() % 3; @@ -8960,6 +12762,24 @@ void Entity::attack(int pose, int charge, Entity* target) gugnirProc = true; } } + else if ( whipPierce ) + { + enemyAC *= 0.5; + armorPierceProc = true; + } + else if ( myStats->getEffectActive(EFF_ENSEMBLE_DRUM) ) + { + real_t amount = (100.0 - myStats->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_TIER)) / 100.0; + if ( (charge >= Stat::getMaxAttackCharge(myStats) - 3) ) + { + if ( /*(local_rng.rand() % 100 < 25)*/true ) + { + enemyAC *= amount; + armorPierceProc = true; + } + } + } + int numBlessings = 0; real_t targetACEffectiveness = Entity::getACEffectiveness(hit.entity, hitstats, hit.entity->behavior == &actPlayer, this, myStats, numBlessings); int attackAfterReductions = static_cast(std::max(0.0, ((myAttack * targetACEffectiveness - enemyAC))) + (1.0 - targetACEffectiveness) * myAttack); @@ -8989,6 +12809,15 @@ void Entity::attack(int pose, int charge, Entity* target) damage = std::max(0, damage); } + if ( myStats->type == GREMLIN ) + { + damage++; + if ( this->behavior == &actPlayer ) + { + damage += 1 * players[this->skill[2]]->mechanics.getBreakableCounterTier(); + } + } + if ( weaponskill == PRO_AXE ) { damage++; @@ -8997,21 +12826,26 @@ void Entity::attack(int pose, int charge, Entity* target) damage += 1; } } - if ( myStats->type == LICH_FIRE && !hitstats->defending ) + if ( zealSmite ) + { + damage += 3; + } + if ( myStats->type == LICH_FIRE && (!hitstats->defending && parriedDamage == 0) ) { if ( damage <= 8 ) { damage += (8 - damage) + local_rng.rand() % 9; // 8 - 16 minimum damage. } } - if ( behavior == &actMonster && myStats->EFFECTS[EFF_VAMPIRICAURA] ) + if ( behavior == &actMonster && myStats->getEffectActive(EFF_VAMPIRICAURA) ) { damage += 5; // 5 bonus damage after reductions. } bool backstab = false; bool flanking = false; - if ( player >= 0 && !monsterIsImmobileTurret(hit.entity, hitstats) && !(hitstats->type == MIMIC) ) + if ( player >= 0 && !monsterIsImmobileTurret(hit.entity, hitstats) && !hitstats->getEffectActive(EFF_STASIS) + && !(hitstats->type == MIMIC || hitstats->type == MINIMIMIC || hitstats->type == MONSTER_ADORCISED_WEAPON) ) { real_t hitAngle = hit.entity->yawDifferenceFromEntity(this); if ( (hitAngle >= 0 && hitAngle <= 2 * PI / 3) ) // 120 degree arc @@ -9022,9 +12856,20 @@ void Entity::attack(int pose, int charge, Entity* target) stealthCapstoneBonus = 2; } - if ( previousMonsterState == MONSTER_STATE_WAIT + bool skillChanceIncrease = true; + if ( hit.entity->behavior == &actMonster && hit.entity->monsterAllyGetPlayerLeader() && behavior == &actPlayer ) + { + skillChanceIncrease = false; // no level up on allies + } + if ( behavior == &actPlayer && !players[player]->mechanics.allowedRaiseStealthAgainstEntity(*hit.entity) ) + { + skillChanceIncrease = false; + } + + if ( (previousMonsterState == MONSTER_STATE_WAIT || previousMonsterState == MONSTER_STATE_PATH - || (previousMonsterState == MONSTER_STATE_HUNT && uidToEntity(monsterTarget) == nullptr) ) + || (previousMonsterState == MONSTER_STATE_HUNT && uidToEntity(hit.entity->monsterTarget) == nullptr)) + && !hitstats->getEffectActive(EFF_ROOTED) ) { // unaware monster, get backstab damage. backstab = true; @@ -9041,13 +12886,17 @@ void Entity::attack(int pose, int charge, Entity* target) } } damage += bonus; - if ( hit.entity->behavior != &actPlayer ) + if ( hit.entity->behavior != &actPlayer && skillChanceIncrease ) { if ( hitstats->type == BAT_SMALL && previousMonsterSpecialState == BAT_REST ) { if ( local_rng.rand() % 10 == 0 ) { this->increaseSkill(PRO_STEALTH); + if ( player >= 0 ) + { + players[player]->mechanics.enemyRaisedStealthAgainst[hit.entity->getUID()]++; + } } } else @@ -9055,6 +12904,10 @@ void Entity::attack(int pose, int charge, Entity* target) if ( local_rng.rand() % 4 > 0 ) { this->increaseSkill(PRO_STEALTH); + if ( player >= 0 ) + { + players[player]->mechanics.enemyRaisedStealthAgainst[hit.entity->getUID()]++; + } } } } @@ -9065,9 +12918,13 @@ void Entity::attack(int pose, int charge, Entity* target) // 1 in 2 chance to flank defenses. flanking = true; damage += (stats[player]->getModifiedProficiency(PRO_STEALTH) / 20 + 1) * (stealthCapstoneBonus); - if ( local_rng.rand() % 20 == 0 && hit.entity->behavior != &actPlayer ) + if ( local_rng.rand() % 20 == 0 && hit.entity->behavior != &actPlayer && skillChanceIncrease ) { this->increaseSkill(PRO_STEALTH); + if ( player >= 0 ) + { + players[player]->mechanics.enemyRaisedStealthAgainst[hit.entity->getUID()]++; + } } } } @@ -9115,9 +12972,17 @@ void Entity::attack(int pose, int charge, Entity* target) } } - int olddamage = damage; - const int chargeMult = std::max(charge, MAXCHARGE / 2) / ((double)(MAXCHARGE / 2)); + real_t chargeMult = (real_t)std::min(2, + static_cast(std::max(charge, static_cast(Stat::getMaxAttackCharge(myStats) / 2)) + / ((double)(Stat::getMaxAttackCharge(myStats) / 2)))); + if ( myStats->weapon && myStats->weapon->type == RAPIER && !shapeshifted ) + { + if ( charge >= Stat::getMaxAttackCharge(myStats) ) + { + chargeMult += 0.5; + } + } damage *= chargeMult; bool parashuProc = false; if ( myStats->weapon && !shapeshifted ) @@ -9138,7 +13003,7 @@ void Entity::attack(int pose, int charge, Entity* target) } } - if ( hitstats->type == DUMMYBOT ) + if ( hitstats->type == DUMMYBOT && hitstats->getAttribute("dummy_target") == "" ) { // higher level dummy bots have damage cap limits on hit. if ( myStats->type != MINOTAUR && myStats->type != LICH_FIRE ) @@ -9174,6 +13039,36 @@ void Entity::attack(int pose, int charge, Entity* target) } } + bool thornsAllowZeroDmg = false; + if ( hitstats->getEffectActive(EFF_THORNS) ) + { + int thorn = getSpellDamageFromID(SPELL_THORNS, hit.entity, nullptr, hit.entity); + int extraThorn = std::min(damage, getSpellDamageSecondaryFromID(SPELL_THORNS, hit.entity, nullptr, hit.entity)); + thorn += extraThorn; + thorn = std::min(thorn, getSpellEffectDurationSecondaryFromID(SPELL_THORNS, hit.entity, nullptr, hit.entity)); + thornsAllowZeroDmg = true; + if ( playerhit >= 0 ) + { + hit.entity->safeConsumeMP(1); + players[playerhit]->mechanics.updateSustainedSpellEvent(SPELL_THORNS, 50.0 + thorn * 5.0, 1.0, this); + } + thornsEffect += thorn; + } + if ( hitstats->getEffectActive(EFF_BLADEVINES) ) + { + int thorn = getSpellDamageFromID(SPELL_BLADEVINES, hit.entity, nullptr, hit.entity); + int extraThorn = std::min(damage, getSpellDamageSecondaryFromID(SPELL_BLADEVINES, hit.entity, nullptr, hit.entity)); + thorn += extraThorn; + thorn = std::min(thorn, getSpellEffectDurationSecondaryFromID(SPELL_BLADEVINES, hit.entity, nullptr, hit.entity)); + thornsAllowZeroDmg = true; + if ( playerhit >= 0 ) + { + hit.entity->safeConsumeMP(1); + players[playerhit]->mechanics.updateSustainedSpellEvent(SPELL_BLADEVINES, 50.0 + thorn * 5.0, 1.0, this); + } + thornsEffect += thorn; + } + Item** weaponToBreak = nullptr; ItemType weaponType = static_cast(WOODEN_SHIELD); bool hasMeleeGloves = false; @@ -9203,9 +13098,24 @@ void Entity::attack(int pose, int charge, Entity* target) weaponType = (*weaponToBreak)->type; } + if ( hitstats && hitstats->getEffectActive(EFF_SALAMANDER_HEART) == 4 && hitstats->type == SALAMANDER ) + { + damage = 0; + spawnBang(hit.entity->x, hit.entity->y, hit.entity->z); + } + Sint32 oldHP = hitstats->HP; hit.entity->modHP(-damage); // do the damage + if ( hitstats ) + { + Sint32 damageTaken = oldHP - hitstats->HP; + if ( damageTaken > 0 && hitstats->getEffectActive(EFF_DEFY_FLESH) ) + { + hit.entity->defyFleshProc(this); + } + } + bool skillIncreased = false; // skill increase // can raise skills up to skill level 20 on dummybots... @@ -9221,10 +13131,29 @@ void Entity::attack(int pose, int charge, Entity* target) doSkillIncrease = false; // no skill for killing/hurting other turrets. } } + else if ( hit.entity->behavior == &actMonster + && (hit.entity->monsterAllyGetPlayerLeader() || (hitstats && achievementObserver.checkUidIsFromPlayer(hitstats->leader_uid) >= 0)) + && behavior == &actPlayer ) + { + doSkillIncrease = false; // no level up on allies + } if ( hit.entity->behavior == &actPlayer && behavior == &actPlayer ) { doSkillIncrease = false; // no skill for killing/hurting players } + if ( hitstats->getEffectActive(EFF_STASIS) ) + { + doSkillIncrease = false; + } + + if ( doSkillIncrease && player >= 0 ) + { + if ( isInvisible() ) + { + players[player]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, 10.0, 1.0, hit.entity); + } + } + if ( doSkillIncrease && ((weaponskill >= PRO_SWORD && weaponskill <= PRO_POLEARM) || weaponskill == PRO_UNARMED || (whip && weaponskill == PRO_RANGED)) ) { @@ -9244,7 +13173,8 @@ void Entity::attack(int pose, int charge, Entity* target) if ( local_rng.rand() % chance == 0 ) { - if ( hitstats->type != DUMMYBOT || (hitstats->type == DUMMYBOT && myStats->getProficiency(weaponskill) < SKILL_LEVEL_BASIC) ) + bool lowSkill = hitstats->type == DUMMYBOT; + if ( !lowSkill || (lowSkill && myStats->getProficiency(weaponskill) < SKILL_LEVEL_BASIC) ) { this->increaseSkill(weaponskill, notify); skillIncreased = true; @@ -9255,7 +13185,7 @@ void Entity::attack(int pose, int charge, Entity* target) { if ( player >= 0 && weaponskill == PRO_UNARMED && stats[player]->type == GOATMAN - && stats[player]->EFFECTS[EFF_DRUNK] ) + && stats[player]->getEffectActive(EFF_DRUNK) ) { steamStatisticUpdateClient(player, STEAM_STAT_BARFIGHT_CHAMP, STEAM_STAT_INT, 1); } @@ -9263,13 +13193,23 @@ void Entity::attack(int pose, int charge, Entity* target) bool notify = true; if ( myStats->type == GOBLIN ) { - chance = 12; - notify = true; + chance = 12; + notify = true; + } + + if ( flail && pose == MONSTER_POSE_FLAIL_SWING ) + { + chance *= 4; } + if ( local_rng.rand() % chance == 0 ) { - this->increaseSkill(weaponskill, notify); - skillIncreased = true; + bool lowSkill = hitstats->type == DUMMYBOT; + if ( !lowSkill || (lowSkill && myStats->getProficiency(weaponskill) < SKILL_LEVEL_BASIC) ) + { + this->increaseSkill(weaponskill, notify); + skillIncreased = true; + } } } else @@ -9281,6 +13221,12 @@ void Entity::attack(int pose, int charge, Entity* target) chance = 14; notify = true; } + + if ( flail && pose == MONSTER_POSE_FLAIL_SWING ) + { + chance *= 4; + } + if ( local_rng.rand() % chance == 0 ) { if ( hitstats->type != DUMMYBOT || (hitstats->type == DUMMYBOT && myStats->getProficiency(weaponskill) < SKILL_LEVEL_BASIC) ) @@ -9323,6 +13269,15 @@ void Entity::attack(int pose, int charge, Entity* target) } } + if ( hitstats && hitstats->getEffectActive(EFF_GUARD_BODY) ) + { + thaumSpellArmorProc(hit.entity, *hitstats, false, this, EFF_GUARD_BODY); + } + if ( hitstats && hitstats->getEffectActive(EFF_DIVINE_GUARD) ) + { + thaumSpellArmorProc(hit.entity, *hitstats, false, this, EFF_DIVINE_GUARD); + } + // write the obituary if ( hit.entity != this ) { @@ -9335,6 +13290,19 @@ void Entity::attack(int pose, int charge, Entity* target) bool artifactWeapon = false; bool degradeWeapon = false; + if ( pose == PLAYER_POSE_GOLEM_SMASH ) + { + if ( behavior == &actPlayer ) + { + if ( damage > 0 && oldHP > hitstats->HP ) + { + Sint32 damageTaken = oldHP - hitstats->HP; + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_SPELL_DMG, SPELL_ITEM, damageTaken, false, SPELL_STRIKE); + magicOnSpellCastEvent(this, this, hit.entity, SPELL_STRIKE, spell_t::SPELL_LEVEL_EVENT_DMG, damageTaken); + } + } + } + if ( weaponToBreak != nullptr && weaponType != WOODEN_SHIELD ) { if ( behavior == &actPlayer ) @@ -9343,7 +13311,7 @@ void Entity::attack(int pose, int charge, Entity* target) weaponType, 1); if ( pose == PLAYER_POSE_GOLEM_SMASH ) { - Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_SPELL_DMG, SPELL_ITEM, damage, false, SPELL_STRIKE); + } else { @@ -9403,6 +13371,13 @@ void Entity::attack(int pose, int charge, Entity* target) degradeOnNormalDMG *= 2; } + if ( flail && pose == MONSTER_POSE_FLAIL_SWING ) + { + // double durability. + degradeOnZeroDMG *= 2; + degradeOnNormalDMG *= 2; + } + if ( (local_rng.rand() % degradeOnZeroDMG == 0 && damage == 0) || (local_rng.rand() % degradeOnNormalDMG == 0 && damage > 0) ) @@ -9410,6 +13385,13 @@ void Entity::attack(int pose, int charge, Entity* target) degradeWeapon = true; } + if ( player >= 0 ) + { + if ( !players[player]->mechanics.itemDegradeRoll(*weaponToBreak, weaponskill) ) + { + degradeWeapon = false; + } + } if ( behavior == &actPlayer && skillCapstoneUnlocked(skill[2], weaponskill) ) { // don't degrade on capstone skill. @@ -9431,9 +13413,30 @@ void Entity::attack(int pose, int charge, Entity* target) { degradeWeapon = false; } + else if ( myStats->weapon && myStats->weapon->type == RAPIER ) + { + degradeWeapon = false; + } + else if ( myStats->weapon && myStats->weapon->type == SHILLELAGH_MACE ) + { + degradeWeapon = false; + } + else if ( myStats->weapon && myStats->weapon->type == TOOL_PICKAXE && myStats->type == GNOME ) + { + degradeWeapon = false; + } + else if ( flail && pose == MONSTER_POSE_FLAIL_SWING && local_rng.rand() % 4 > 0 ) + { + degradeWeapon = false; + } - if ( degradeWeapon ) + if ( degradeWeapon && !spellEffectPreserveItem(*weaponToBreak) ) { + if ( player >= 0 ) + { + players[player]->mechanics.onItemDegrade(*weaponToBreak); + } + if ( (player >= 0 && players[player]->isLocalPlayer()) || player < 0 ) { if ( (*weaponToBreak)->count > 1 ) @@ -9473,9 +13476,10 @@ void Entity::attack(int pose, int charge, Entity* target) net_packet->data[4] = 2; } net_packet->data[5] = (*weaponToBreak)->status; + SDLNet_Write16((int)(*weaponToBreak)->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } if ( (*weaponToBreak)->status == BROKEN && behavior == &actMonster && playerhit >= 0 ) @@ -9491,8 +13495,14 @@ void Entity::attack(int pose, int charge, Entity* target) int armornum = 0; bool isWeakArmor = false; bool shieldIncreased = false; - if ( damage > 0 || (damage == 0 && !(hitstats->shield && hitstats->defending)) ) + if ( damage > 0 || (damage == 0 && !(hitstats->shield && hitstats->defending) && parriedDamage == 0) ) { + if ( behavior == &actMonster ) + { + int accuracy = myStats->monsterRangedAccuracy.getAccuracy(monsterTarget); + myStats->monsterRangedAccuracy.incrementAccuracy(); + } + // choose random piece of equipment to target armornum = hitstats->pickRandomEquippedItemToDegradeOnHit(&armor, true, false, false, true); if ( armor != NULL && armor->status > BROKEN ) @@ -9518,14 +13528,20 @@ void Entity::attack(int pose, int charge, Entity* target) armorDegradeChance = 15; if ( weaponskill == PRO_MACE ) { - armorDegradeChance = 5; + if ( !(flail && pose == MONSTER_POSE_FLAIL_SWING) ) + { + armorDegradeChance = 5; + } } } else { if ( weaponskill == PRO_MACE ) { - armorDegradeChance = 10; + if ( !(flail && pose == MONSTER_POSE_FLAIL_SWING) ) + { + armorDegradeChance = 10; + } } } @@ -9594,7 +13610,12 @@ void Entity::attack(int pose, int charge, Entity* target) // if nothing chosen to degrade, check extra shield chances to degrade if ( hitstats->shield != NULL && hitstats->shield->status > BROKEN && armor == NULL && !itemTypeIsQuiver(hitstats->shield->type) && itemCategory(hitstats->shield) != SPELLBOOK - && hitstats->shield->type != TOOL_TINKERING_KIT ) + && !itemTypeIsFoci(hitstats->shield->type) + && !itemTypeIsInstrument(hitstats->shield->type) + && hitstats->shield->type != TOOL_DUCK + && parriedDamage == 0 + && hitstats->shield->type != TOOL_TINKERING_KIT + && hitstats->shield->type != TOOL_FRYING_PAN ) { if ( hitstats->shield->type == TOOL_CRYSTALSHARD && hitstats->defending ) { @@ -9659,7 +13680,7 @@ void Entity::attack(int pose, int charge, Entity* target) { increaseSkill = false; } - else if ( hitstats->EFFECTS[EFF_SHAPESHIFT] ) + else if ( hitstats->getEffectActive(EFF_SHAPESHIFT) ) { increaseSkill = false; } @@ -9670,6 +13691,10 @@ void Entity::attack(int pose, int charge, Entity* target) } if ( increaseSkill ) { + if ( hitstats->getEffectActive(EFF_FORCE_SHIELD) ) + { + magicOnSpellCastEvent(hit.entity, hit.entity, this, SPELL_FORCE_SHIELD, spell_t::SpellOnCastEventTypes::SPELL_LEVEL_EVENT_DEFAULT, 1); + } hit.entity->increaseSkill(PRO_SHIELD); // increase shield skill shieldIncreased = true; if ( hit.entity->behavior == &actPlayer ) @@ -9735,6 +13760,7 @@ void Entity::attack(int pose, int charge, Entity* target) { if ( player >= 0 && hit.entity->behavior == &actMonster ) { + players[player]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_DEGRADE, hit.entity); steamStatisticUpdateClient(player, STEAM_STAT_UNSTOPPABLE_FORCE, STEAM_STAT_INT, 1); if ( armornum == 4 && hitstats->type == BUGBEAR && (hitstats->defending || hit.entity->monsterAttack == MONSTER_POSE_BUGBEAR_SHIELD) ) @@ -9752,7 +13778,8 @@ void Entity::attack(int pose, int charge, Entity* target) bool swordExtraDamageInflicted = false; bool knockbackInflicted = false; bool dyrnwynBurn = false; - + int shillelaghDamage = 0; + int shillelaghDebuffs = 0; /*if ( thornsEffect < 0 ) { hit.entity->modHP(thornsEffect); @@ -9776,7 +13803,7 @@ void Entity::attack(int pose, int charge, Entity* target) bool wasBurning = hit.entity->flags[BURNING]; // Attempt to set the Entity on fire - hit.entity->SetEntityOnFire(); + hit.entity->SetEntityOnFire(this); if ( !wasBurning && hit.entity->flags[BURNING] ) { @@ -9786,7 +13813,7 @@ void Entity::attack(int pose, int charge, Entity* target) } // If a Player was hit, and they are now on fire, tell them what set them on fire - if ( playerhit > 0 && hit.entity->flags[BURNING] ) + if ( playerhit >= 0 && hit.entity->flags[BURNING] ) { messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(683)); // "Dyrnwyn sets you on fire!" } @@ -9803,16 +13830,84 @@ void Entity::attack(int pose, int charge, Entity* target) spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 171); } } + if ( !shapeshifted && weaponskill == PRO_MACE && myStats->weapon && myStats->weapon->type == SHILLELAGH_MACE ) + { + int numDebuffs = hitstats->numShillelaghDebuffsActive(hit.entity); + if ( numDebuffs > 0 ) + { + shillelaghDebuffs = numDebuffs; + real_t scale = 0.25 + numDebuffs * 0.25; + int bonusDamage = attackAfterReductions * scale * Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_MAGIC); + if ( bonusDamage > 0 ) + { + real_t variance = 20; + real_t baseSkillModifier = 50.0; // 40-60 base + real_t skillModifier = baseSkillModifier - (variance / 2) + (myStats->getModifiedProficiency(PRO_MYSTICISM) / 2.0); + skillModifier += (local_rng.rand() % (1 + static_cast(variance))); + skillModifier /= 100.0; + skillModifier = std::min(skillModifier, 1.0); + bonusDamage = bonusDamage - static_cast((1.0 - skillModifier) * bonusDamage); + //messagePlayer(0, MESSAGE_DEBUG, "Shill: %d", bonusDamage); + shillelaghDamage = bonusDamage; + + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_SHILLELAGH_DEBUFFS_MAX, SHILLELAGH_MACE, numDebuffs); + Compendium_t::Events_t::eventUpdate(skill[2], Compendium_t::CPDM_DMG_MAX, SHILLELAGH_MACE, damage + shillelaghDamage); + + Sint32 prevHP = hitstats->HP; + hit.entity->modHP(-shillelaghDamage); + Sint32 newHP = hitstats->HP; + if ( newHP < prevHP ) + { + //spawnDamageGib(hit.entity, shillelaghDamage, DamageGib::DMG_STRONGER, DamageGibDisplayType::DMG_GIB_NUMBER); + spawnDamageGib(hit.entity, 225, DamageGib::DMG_STRONGER, DamageGibDisplayType::DMG_GIB_SPRITE); + spawnDamageGib(hit.entity, 261, DamageGib::DMG_STRONGER, DamageGibDisplayType::DMG_GIB_SPRITE); + } + } + } + } } - if ( (hitstats->EFFECTS[EFF_WEBBED] || pose == PLAYER_POSE_GOLEM_SMASH) - && !hitstats->EFFECTS[EFF_KNOCKBACK] && hit.entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + if ( (hitstats->getEffectActive(EFF_WEBBED) || hitstats->getEffectActive(EFF_MAGIC_GREASE) + || pose == PLAYER_POSE_GOLEM_SMASH + || hit.entity->myconidReboundOnHit(this) + || (myStats->type == GNOME && myStats->weapon && !shapeshifted && myStats->weapon->type == TOOL_PICKAXE)) + && !hitstats->getEffectActive(EFF_KNOCKBACK) && hit.entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) { real_t baseMultiplier = 0.7; + knockbackInflicted = true; if ( pose == PLAYER_POSE_GOLEM_SMASH ) { baseMultiplier = 0.9; } + else if ( (hitstats->getEffectActive(EFF_WEBBED) || hitstats->getEffectActive(EFF_MAGIC_GREASE)) ) + { + baseMultiplier = 0.7; + + if ( hitstats->getEffectActive(EFF_MAGIC_GREASE) ) + { + if ( behavior == &actPlayer ) + { + achievementObserver.playerAchievements[skill[2]].skidRow++; + } + } + } + else if ( (myStats->type == GNOME && myStats->weapon && !shapeshifted && myStats->weapon->type == TOOL_PICKAXE) ) + { + baseMultiplier = 0.7; + if ( behavior == &actPlayer ) + { + if ( charge < Stat::getMaxAttackCharge(myStats) / 2 ) + { + baseMultiplier = 0.5; + } + } + } + else if ( hit.entity->myconidReboundOnHit(this) ) + { + baseMultiplier = 0.2 + (hitstats->getEffectActive(EFF_GROWTH) - 1) * 0.25; + knockbackInflicted = false; // hide message + hit.entity->playerShakeGrowthHelmet(); + } real_t pushbackMultiplier = baseMultiplier; if ( !hit.entity->isMobile() ) { @@ -9859,11 +13954,10 @@ void Entity::attack(int pose, int charge, Entity* target) hit.entity->monsterKnockbackTangentDir = tangent; } } - knockbackInflicted = true; } // player weapon skills - if ( damage > 0 && weaponskill == PRO_UNARMED && behavior == &actPlayer && (charge >= MAXCHARGE - 3) ) + if ( damage > 0 && weaponskill == PRO_UNARMED && behavior == &actPlayer && (charge >= Stat::getMaxAttackCharge(myStats) - 3) ) { int chance = 0; bool inflictParalyze = false; @@ -9885,7 +13979,7 @@ void Entity::attack(int pose, int charge, Entity* target) default: break; } - if ( chance > 0 && backstab && !hitstats->EFFECTS[EFF_PARALYZED] && hitstats->HP > 0 ) + if ( chance > 0 && backstab && !hitstats->getEffectActive(EFF_PARALYZED) && hitstats->HP > 0 ) { int duration = 50; if ( hitstats->HP > 0 && hit.entity->setEffect(EFF_PARALYZED, true, duration, true) ) @@ -9975,7 +14069,7 @@ void Entity::attack(int pose, int charge, Entity* target) knockbackInflicted = true; } } - else if ( damage > 0 && behavior == &actPlayer && weaponskill >= PRO_SWORD && weaponskill <= PRO_POLEARM && (charge >= MAXCHARGE - 3) ) + else if ( damage > 0 && behavior == &actPlayer && weaponskill >= PRO_SWORD && weaponskill <= PRO_POLEARM && (charge >= Stat::getMaxAttackCharge(myStats) - 3) ) { // special weapon effects. int capstoneDamage = 5; @@ -10034,12 +14128,12 @@ void Entity::attack(int pose, int charge, Entity* target) hit.entity->modHP(-capstoneDamage); // do the damage } } - else if ( weaponskill == PRO_MACE && hitstats->HP > 0 ) + else if ( weaponskill == PRO_MACE && hitstats->HP > 0 && !(flail && pose == MONSTER_POSE_FLAIL_SWING) ) { // paralyze. if ( chance > 0 ) // chance based paralyze { - if ( local_rng.rand() % chance == 0 && !hitstats->EFFECTS[EFF_PARALYZED] ) + if ( local_rng.rand() % chance == 0 && !hitstats->getEffectActive(EFF_PARALYZED) ) { int duration = 75; // 1.5 seconds if ( hitstats->HP > 0 && hit.entity->setEffect(EFF_PARALYZED, true, duration, true) ) @@ -10062,10 +14156,14 @@ void Entity::attack(int pose, int charge, Entity* target) if ( chance > 0 ) // always { int duration = 150; // 3 seconds + bool wasSlowed = hitstats->getEffectActive(EFF_SLOW); if ( hitstats->HP > 0 && hit.entity->setEffect(EFF_SLOW, true, duration, true) && !slowStatusInflicted ) { slowStatusInflicted = true; - playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + if ( !wasSlowed ) + { + playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + } spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 171); if ( behavior == &actPlayer ) { @@ -10108,6 +14206,43 @@ void Entity::attack(int pose, int charge, Entity* target) } bool playerPoisonedTarget = false; + bool envenomWeapon = false; + + if ( damage > 0 && myStats->getEffectActive(EFF_ENVENOM_WEAPON) ) + { + if ( local_rng.rand() % 2 == 0 ) + { + int envenomDamage = std::min( + getSpellDamageSecondaryFromID(SPELL_ENVENOM_WEAPON, this, myStats, this), + getSpellDamageFromID(SPELL_ENVENOM_WEAPON, this, myStats, this)); + + hit.entity->modHP(-envenomDamage); // do the damage + for ( int tmp = 0; tmp < 3; ++tmp ) + { + Entity* gib = spawnGib(hit.entity, 211); + serverSpawnGibForClient(gib); + } + if ( !hitstats->getEffectActive(EFF_POISONED) ) + { + envenomWeapon = true; + hitstats->setEffectActive(EFF_POISONED, 1); + + int duration = 310 * envenomDamage; + hitstats->EFFECTS_TIMERS[EFF_POISONED] = std::max(200, duration - hit.entity->getCON() * 20); + hitstats->poisonKiller = getUID(); + if ( playerhit >= 0 ) + { + messagePlayerMonsterEvent(playerhit, makeColorRGB(255, 0, 0), *myStats, Language::get(6531), Language::get(6532), MSG_COMBAT); + serverUpdateEffects(playerhit); + } + + if ( player >= 0 ) + { + players[player]->mechanics.updateSustainedSpellEvent(SPELL_ENVENOM_WEAPON, 50.0, 1.0, hit.entity); + } + } + } + } // special monster effects if ( myStats->type == CRYSTALGOLEM && pose == MONSTER_POSE_GOLEM_SMASH ) @@ -10169,9 +14304,9 @@ void Entity::attack(int pose, int charge, Entity* target) } else if ( myStats->type == BAT_SMALL ) { - if ( !hitstats->EFFECTS[EFF_BLEEDING] ) + if ( !hitstats->getEffectActive(EFF_BLEEDING) ) { - if ( !hitstats->defending ) + if ( !hitstats->defending && parriedDamage == 0 ) { if ( hit.entity->setEffect(EFF_BLEEDING, true, 6 * TICKS_PER_SECOND, false) ) { @@ -10188,7 +14323,9 @@ void Entity::attack(int pose, int charge, Entity* target) if ( behavior == &actPlayer || (hit.entity->behavior == &actMonster && ((hit.entity->monsterAllySummonRank != 0 && hitstats->type == SKELETON) - || hit.entity->monsterIsTinkeringCreation())) ) + || hit.entity->monsterIsTinkeringCreation() + || hitstats->type == MONSTER_ADORCISED_WEAPON + || hitstats->type == REVENANT_SKULL)) ) { armor = nullptr; } @@ -10210,76 +14347,136 @@ void Entity::attack(int pose, int charge, Entity* target) } if ( armor != nullptr ) { - int qty = 1; - int startCount = armor->count; - if ( itemTypeIsQuiver(armor->type) ) + bool mimicSentToVoid = false; + if ( myStats->getEffectActive(EFF_MIMIC_VOID) ) { - qty = armor->count; - armor->count = 0; - } - else - { - armor->count--; - } - if ( hit.entity->behavior == &actPlayer && playerhit >= 0 ) - { - steamStatisticUpdateClient(playerhit, STEAM_STAT_I_NEEDED_THAT, STEAM_STAT_INT, 1); - } - messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(6085), armor->getName()); - Item* stolenArmor = newItem(armor->type, armor->status, armor->beatitude, qty, armor->appearance, armor->identified, &myStats->inventory); - stolenArmor->ownerUid = hit.entity->getUID(); - stolenArmor->isDroppable = armor->isDroppable; - if ( playerhit > 0 && multiplayer == SERVER && !players[playerhit]->isLocalPlayer() ) - { - strcpy((char*)net_packet->data, "STLA"); - net_packet->data[4] = armornum; - SDLNet_Write32(static_cast(armor->type), &net_packet->data[5]); - SDLNet_Write32(static_cast(armor->status), &net_packet->data[9]); - SDLNet_Write32(static_cast(armor->beatitude), &net_packet->data[13]); - SDLNet_Write32(static_cast(startCount), &net_packet->data[17]); - SDLNet_Write32(static_cast(armor->appearance), &net_packet->data[21]); - net_packet->data[25] = armor->identified; - net_packet->address.host = net_clients[playerhit - 1].host; - net_packet->address.port = net_clients[playerhit - 1].port; - net_packet->len = 26; - sendPacketSafe(net_sock, -1, net_packet, playerhit - 1); + int qty = 1; + if ( itemTypeIsQuiver(armor->type) ) + { + qty = armor->count; + } + + Item* stolenArmor = newItem(armor->type, armor->status, armor->beatitude, qty, armor->appearance, armor->identified, nullptr); + if ( Item* chestItem = Entity::addItemToVoidChestServer(-1, stolenArmor, true, nullptr) ) + { + if ( chestItem != stolenArmor ) + { + free(stolenArmor); + } + mimicSentToVoid = true; + } + else + { + free(stolenArmor); + } } - if ( armor->count <= 0 ) + if ( armor ) { - Item** slot = itemSlot(hitstats, armor); - if ( slot ) + int qty = 1; + int startCount = armor->count; + if ( itemTypeIsQuiver(armor->type) ) + { + qty = armor->count; + armor->count = 0; + } + else + { + armor->count--; + } + if ( hit.entity->behavior == &actPlayer && playerhit >= 0 ) { - *slot = NULL; + steamStatisticUpdateClient(playerhit, STEAM_STAT_I_NEEDED_THAT, STEAM_STAT_INT, 1); } - if ( armor->node ) + if ( !mimicSentToVoid ) { - list_RemoveNode(armor->node); + messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(6085), armor->getName()); + Item* stolenArmor = newItem(armor->type, armor->status, armor->beatitude, qty, armor->appearance, armor->identified, &myStats->inventory); + stolenArmor->ownerUid = hit.entity->getUID(); + stolenArmor->isDroppable = armor->isDroppable; + if ( myStats->getEffectActive(EFF_MIMIC_VOID) ) + { + dropItemMonster(stolenArmor, this, myStats, stolenArmor->count); + } } else { - free(armor); + messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(6567), armor->getName()); + } + if ( playerhit > 0 && multiplayer == SERVER && !players[playerhit]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "STLA"); + net_packet->data[4] = armornum; + SDLNet_Write32(static_cast(armor->type), &net_packet->data[5]); + SDLNet_Write32(static_cast(armor->status), &net_packet->data[9]); + SDLNet_Write32(static_cast(armor->beatitude), &net_packet->data[13]); + SDLNet_Write32(static_cast(startCount), &net_packet->data[17]); + SDLNet_Write32(static_cast(armor->appearance), &net_packet->data[21]); + net_packet->data[25] = armor->identified; + net_packet->address.host = net_clients[playerhit - 1].host; + net_packet->address.port = net_clients[playerhit - 1].port; + net_packet->len = 26; + sendPacketSafe(net_sock, -1, net_packet, playerhit - 1); + } + + if ( armor->count <= 0 ) + { + Item** slot = itemSlot(hitstats, armor); + if ( slot ) + { + *slot = NULL; + } + if ( armor->node ) + { + list_RemoveNode(armor->node); + } + else + { + free(armor); + } } } } } - else if ( (damage > 0 || hitstats->EFFECTS[EFF_PACIFY] || hitstats->EFFECTS[EFF_FEAR]) && local_rng.rand() % 4 == 0 ) + else if ( (damage > 0 || hitstats->getEffectActive(EFF_PACIFY) + || hitstats->getEffectActive(EFF_FEAR) + || hitstats->getEffectActive(EFF_COWARDICE) ) && local_rng.rand() % 4 == 0 ) { switch ( myStats->type ) { case SCORPION: - hitstats->EFFECTS[EFF_PARALYZED] = true; + hitstats->setEffectActive(EFF_PARALYZED, 1); hitstats->EFFECTS_TIMERS[EFF_PARALYZED] = std::max(50, 150 - hit.entity->getCON() * 5); messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(684)); messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(685)); serverUpdateEffects(playerhit); break; + case SCARAB: + if ( myStats->getAttribute("SCARAB_GREATER_CURSE") != "" ) + { + if ( hit.entity->setEffect(EFF_WEAKNESS, (Uint8)5, hitstats->EFFECTS_TIMERS[EFF_WEAKNESS] + 30 * TICKS_PER_SECOND, + false, true, false) ) + { + messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(6964)); + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 2367); + } + } + else + { + if ( hit.entity->setEffect(EFF_WEAKNESS, (Uint8)2, hitstats->EFFECTS_TIMERS[EFF_WEAKNESS] + 15 * TICKS_PER_SECOND, + false, true, false) ) + { + messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(6963)); + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 2367); + } + } + break; case SPIDER: { bool applyPoison = true; if ( behavior == &actPlayer ) { - if ( charge >= MAXCHARGE - 3 ) // fully charged strike injects venom. + if ( charge >= Stat::getMaxAttackCharge(myStats) - 3 ) // fully charged strike injects venom. { applyPoison = true; } @@ -10291,7 +14488,7 @@ void Entity::attack(int pose, int charge, Entity* target) if ( applyPoison ) { playerPoisonedTarget = true; - hitstats->EFFECTS[EFF_POISONED] = true; + hitstats->setEffectActive(EFF_POISONED, 1); hitstats->EFFECTS_TIMERS[EFF_POISONED] = std::max(200, 600 - hit.entity->getCON() * 20); hitstats->poisonKiller = getUID(); if ( arachnophobia_filter ) { @@ -10300,7 +14497,7 @@ void Entity::attack(int pose, int charge, Entity* target) else { messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(686)); } - messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(687)); + //messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(687)); serverUpdateEffects(playerhit); for ( int tmp = 0; tmp < 3; ++tmp ) { @@ -10405,8 +14602,29 @@ void Entity::attack(int pose, int charge, Entity* target) default: break; } + + if ( myStats->weapon && myStats->weapon->type == BRANCH_STAFF && !shapeshifted ) + { + if ( !hitstats->getEffectActive(EFF_POISONED) ) + { + envenomWeapon = true; + hitstats->setEffectActive(EFF_POISONED, 1); + hitstats->EFFECTS_TIMERS[EFF_POISONED] = std::max(200, 600 - hit.entity->getCON() * 20); + hitstats->poisonKiller = getUID(); + if ( playerhit >= 0 ) + { + messagePlayerMonsterEvent(playerhit, makeColorRGB(255, 0, 0), *myStats, Language::get(6531), Language::get(6532), MSG_COMBAT); + } + serverUpdateEffects(playerhit); + for ( int tmp = 0; tmp < 3; ++tmp ) + { + Entity* gib = spawnGib(hit.entity, 211); + serverSpawnGibForClient(gib); + } + } + } } - else if ( damage == 0 && !(hitstats->defending) ) + else if ( damage == 0 && !hitstats->defending && parriedDamage == 0 ) { // special chance effects when damage is 0. if ( local_rng.rand() % 20 == 0 ) @@ -10414,24 +14632,44 @@ void Entity::attack(int pose, int charge, Entity* target) switch ( myStats->type ) { case SCORPION: - hitstats->EFFECTS[EFF_PARALYZED] = true; + hitstats->setEffectActive(EFF_PARALYZED, 1); hitstats->EFFECTS_TIMERS[EFF_PARALYZED] = std::max(50, 150 - hit.entity->getCON() * 5); messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(684)); messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(685)); serverUpdateEffects(playerhit); statusInflicted = true; break; + case SCARAB: + if ( myStats->getAttribute("SCARAB_GREATER_CURSE") != "" ) + { + if ( hit.entity->setEffect(EFF_WEAKNESS, (Uint8)5, hitstats->EFFECTS_TIMERS[EFF_WEAKNESS] + 30 * TICKS_PER_SECOND, + false, true, false) ) + { + messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(6964)); + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 2367); + } + } + else + { + if ( hit.entity->setEffect(EFF_WEAKNESS, (Uint8)2, hitstats->EFFECTS_TIMERS[EFF_WEAKNESS] + 15 * TICKS_PER_SECOND, + false, true, false) ) + { + messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(6963)); + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 2367); + } + } + break; case SPIDER: if ( behavior != &actPlayer ) { - hitstats->EFFECTS[EFF_POISONED] = true; + hitstats->setEffectActive(EFF_POISONED, 1); hitstats->EFFECTS_TIMERS[EFF_POISONED] = std::max(200, 300 - hit.entity->getCON() * 20); if (arachnophobia_filter) { messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(4089)); } else { messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(686)); } - messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(687)); + //messagePlayer(playerhit, MESSAGE_COMBAT, Language::get(687)); serverUpdateEffects(playerhit); statusInflicted = true; } @@ -10472,6 +14710,10 @@ void Entity::attack(int pose, int charge, Entity* target) } if ( increaseSkill ) { + if ( myStats->getEffectActive(EFF_FORCE_SHIELD) ) + { + magicOnSpellCastEvent(this, this, hit.entity, SPELL_FORCE_SHIELD, spell_t::SpellOnCastEventTypes::SPELL_LEVEL_EVENT_DEFAULT, 1); + } this->increaseSkill(PRO_SHIELD); } } @@ -10480,7 +14722,7 @@ void Entity::attack(int pose, int charge, Entity* target) } } - bool artifactWeaponProc = parashuProc || dyrnwynSmite || dyrnwynBurn || gugnirProc; + bool specialWeaponProc = parashuProc || dyrnwynSmite || dyrnwynBurn || gugnirProc || armorPierceProc || zealSmite; // send messages if ( !strcmp(hitstats->name, "") || monsterNameIsGeneric(*hitstats) ) @@ -10489,7 +14731,7 @@ void Entity::attack(int pose, int charge, Entity* target) Uint32 colorSpecial = color;// makeColorRGB(255, 0, 255); if ( hitstats->HP > 0 ) { - if ( !artifactWeaponProc ) + if ( !specialWeaponProc ) { if ( damage > olddamage ) { @@ -10519,6 +14761,14 @@ void Entity::attack(int pose, int charge, Entity* target) { messagePlayerMonsterEvent(player, colorSpecial, *hitstats, Language::get(3760), Language::get(3761), MSG_COMBAT); } + else if ( armorPierceProc ) + { + messagePlayerMonsterEvent(player, colorSpecial, *hitstats, Language::get(6432), Language::get(6433), MSG_COMBAT); + } + else if ( zealSmite ) + { + messagePlayerMonsterEvent(player, colorSpecial, *hitstats, Language::get(6510), Language::get(6511), MSG_COMBAT); + } if ( damage == 0 ) { @@ -10536,7 +14786,7 @@ void Entity::attack(int pose, int charge, Entity* target) { // backstab on unaware enemy messagePlayerMonsterEvent(player, color, *hitstats, Language::get(2543), Language::get(2543), MSG_COMBAT); - if ( player >= 0 && hitstats->EFFECTS[EFF_SHADOW_TAGGED] && this->creatureShadowTaggedThisUid == hit.entity->getUID() ) + if ( player >= 0 && hitstats->getEffectActive(EFF_SHADOW_TAGGED) && this->creatureShadowTaggedThisUid == hit.entity->getUID() ) { achievementObserver.awardAchievementIfActive(player, this, AchievementObserver::BARONY_ACH_OHAI_MARK); } @@ -10563,6 +14813,10 @@ void Entity::attack(int pose, int charge, Entity* target) { messagePlayerMonsterEvent(player, color, *hitstats, Language::get(3215), Language::get(3214), MSG_COMBAT); } + else if ( envenomWeapon ) + { + messagePlayerMonsterEvent(player, color, *hitstats, Language::get(6533), Language::get(6534), MSG_COMBAT); + } } else { @@ -10579,7 +14833,7 @@ void Entity::attack(int pose, int charge, Entity* target) { steamStatisticUpdateClient(player, STEAM_STAT_BLOOD_SPORT, STEAM_STAT_INT, 1); } - if ( player >= 0 && hitstats->EFFECTS[EFF_SHADOW_TAGGED] && this->creatureShadowTaggedThisUid == hit.entity->getUID() ) + if ( player >= 0 && hitstats->getEffectActive(EFF_SHADOW_TAGGED) && this->creatureShadowTaggedThisUid == hit.entity->getUID() ) { achievementObserver.awardAchievementIfActive(player, this, AchievementObserver::BARONY_ACH_OHAI_MARK); } @@ -10650,6 +14904,21 @@ void Entity::attack(int pose, int charge, Entity* target) if ( player >= 0 && myStats->weapon && this->checkEnemy(hit.entity) ) { + if ( myStats->weapon && myStats->weapon->type == STEEL_FLAIL && weaponskill == PRO_MACE ) + { + achievementObserver.awardAchievementIfActive(player, hit.entity, AchievementObserver::BARONY_ACH_THATS_A_WRAP); + } + if ( myStats->weapon && myStats->weapon->type == SHILLELAGH_MACE && weaponskill == PRO_MACE ) + { + if ( shillelaghDebuffs >= 3 ) + { + achievementObserver.playerAchievements[player].bonk++; + } + } + if ( zealSmite && myStats->getEffectActive(EFF_DIVINE_ZEAL) ) + { + achievementObserver.playerAchievements[player].righteousFury++; + } if ( myStats->weapon->ownerUid == hit.entity->getUID() ) { achievementObserver.awardAchievementIfActive(player, hit.entity, AchievementObserver::BARONY_ACH_IRONIC_PUNISHMENT); @@ -10660,8 +14929,8 @@ void Entity::attack(int pose, int charge, Entity* target) } if ( weaponskill == PRO_AXE && client_classes[player] == CLASS_PUNISHER ) { - if ( hitstats->EFFECTS[EFF_DISORIENTED] || hitstats->EFFECTS[EFF_PARALYZED] - || hitstats->EFFECTS[EFF_SLOW] || hitstats->EFFECTS[EFF_ASLEEP] ) + if ( hitstats->getEffectActive(EFF_DISORIENTED) || hitstats->getEffectActive(EFF_PARALYZED) + || hitstats->getEffectActive(EFF_SLOW) || hitstats->getEffectActive(EFF_ASLEEP) ) { steamStatisticUpdateClient(player, STEAM_STAT_CHOPPING_BLOCK, STEAM_STAT_INT, 1); } @@ -10675,7 +14944,7 @@ void Entity::attack(int pose, int charge, Entity* target) Uint32 colorSpecial = color;// makeColorRGB(255, 0, 255); if ( hitstats->HP > 0 ) { - if ( !artifactWeaponProc ) + if ( !specialWeaponProc ) { if ( damage > olddamage ) { @@ -10705,6 +14974,14 @@ void Entity::attack(int pose, int charge, Entity* target) { messagePlayerMonsterEvent(player, colorSpecial, *hitstats, Language::get(3760), Language::get(3761), MSG_COMBAT); } + else if ( armorPierceProc ) + { + messagePlayerMonsterEvent(player, colorSpecial, *hitstats, Language::get(6432), Language::get(6433), MSG_COMBAT); + } + else if ( zealSmite ) + { + messagePlayerMonsterEvent(player, colorSpecial, *hitstats, Language::get(6510), Language::get(6511), MSG_COMBAT); + } if ( damage == 0 ) { @@ -10729,7 +15006,7 @@ void Entity::attack(int pose, int charge, Entity* target) { // backstab on unaware enemy messagePlayerMonsterEvent(player, color, *hitstats, Language::get(2543), Language::get(2544), MSG_COMBAT); - if ( player >= 0 && hitstats->EFFECTS[EFF_SHADOW_TAGGED] && this->creatureShadowTaggedThisUid == hit.entity->getUID() ) + if ( player >= 0 && hitstats->getEffectActive(EFF_SHADOW_TAGGED) && this->creatureShadowTaggedThisUid == hit.entity->getUID() ) { achievementObserver.awardAchievementIfActive(player, this, AchievementObserver::BARONY_ACH_OHAI_MARK); } @@ -10756,6 +15033,10 @@ void Entity::attack(int pose, int charge, Entity* target) { messagePlayerMonsterEvent(player, color, *hitstats, Language::get(3215), Language::get(3214), MSG_COMBAT); } + else if ( envenomWeapon ) + { + messagePlayerMonsterEvent(player, color, *hitstats, Language::get(6533), Language::get(6534), MSG_COMBAT); + } } else { @@ -10772,7 +15053,7 @@ void Entity::attack(int pose, int charge, Entity* target) { steamStatisticUpdateClient(player, STEAM_STAT_BLOOD_SPORT, STEAM_STAT_INT, 1); } - if ( player >= 0 && hitstats->EFFECTS[EFF_SHADOW_TAGGED] && this->creatureShadowTaggedThisUid == hit.entity->getUID() ) + if ( player >= 0 && hitstats->getEffectActive(EFF_SHADOW_TAGGED) && this->creatureShadowTaggedThisUid == hit.entity->getUID() ) { achievementObserver.awardAchievementIfActive(player, this, AchievementObserver::BARONY_ACH_OHAI_MARK); } @@ -10848,8 +15129,8 @@ void Entity::attack(int pose, int charge, Entity* target) } if ( weaponskill == PRO_AXE && client_classes[player] == CLASS_PUNISHER ) { - if ( hitstats->EFFECTS[EFF_DISORIENTED] || hitstats->EFFECTS[EFF_PARALYZED] - || hitstats->EFFECTS[EFF_SLOW] || hitstats->EFFECTS[EFF_ASLEEP] ) + if ( hitstats->getEffectActive(EFF_DISORIENTED) || hitstats->getEffectActive(EFF_PARALYZED) + || hitstats->getEffectActive(EFF_SLOW) || hitstats->getEffectActive(EFF_ASLEEP) ) { steamStatisticUpdateClient(player, STEAM_STAT_CHOPPING_BLOCK, STEAM_STAT_INT, 1); } @@ -10861,19 +15142,22 @@ void Entity::attack(int pose, int charge, Entity* target) bool disarmed = false; if ( hitstats->HP > 0 ) { - if ( !whip && hitstats->EFFECTS[EFF_DISORIENTED] ) + if ( !whip && hitstats->getEffectActive(EFF_DISORIENTED) ) { - hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + if ( hitstats->getEffectActive(EFF_DISORIENTED) == 2 ) + { + // reduce counter to 2 to turn around next tick in actMonster + hitstats->EFFECTS_TIMERS[EFF_DISORIENTED] = 2; + } + else + { + hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + } // secondary alert to nerf the disorient time, second hit will aggro if ( myStats->mask && myStats->mask->type == MASK_PHANTOM && hit.entity->behavior == &actMonster ) { - bool alertTarget = true; - if ( behavior == &actMonster && monsterAllyIndex != -1 && hit.entity->monsterAllyIndex != -1 ) - { - // if we're both allies of players, don't alert the hit target. - alertTarget = false; - } + bool alertTarget = hit.entity->monsterAlertBeforeHit(this); // alert the monster! if ( hit.entity->monsterState != MONSTER_STATE_ATTACK && (hitstats->type < LICH || hitstats->type >= SHOPKEEPER) ) @@ -10885,10 +15169,10 @@ void Entity::attack(int pose, int charge, Entity* target) } } } - else if ( whip && (hitstats->EFFECTS[EFF_DISORIENTED] + else if ( whip && (hitstats->getEffectActive(EFF_DISORIENTED) || !hit.entity->isMobile() - || (hitstats->EFFECTS[EFF_DRUNK] && local_rng.rand() % 3 == 0) - || (hitstats->EFFECTS[EFF_CONFUSED] && local_rng.rand() % 3 == 0)) + || (hitstats->getEffectActive(EFF_DRUNK) && local_rng.rand() % 3 == 0) + || (hitstats->getEffectActive(EFF_CONFUSED) && local_rng.rand() % 3 == 0)) ) { if ( hit.entity->behavior == &actMonster && !hit.entity->isBossMonster() ) @@ -10900,9 +15184,17 @@ void Entity::attack(int pose, int charge, Entity* target) Entity* dropped = dropItemMonster(hitstats->weapon, hit.entity, hitstats); if ( dropped ) { - if ( hitstats->EFFECTS[EFF_DISORIENTED] && !hitstats->shield ) + if ( hitstats->getEffectActive(EFF_DISORIENTED) && !hitstats->shield ) { - hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + if ( hitstats->getEffectActive(EFF_DISORIENTED) == 2 ) + { + // reduce counter to 2 to turn around next tick in actMonster + hitstats->EFFECTS_TIMERS[EFF_DISORIENTED] = 2; + } + else + { + hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + } } playSoundEntity(hit.entity, 406, 128); dropped->itemDelayMonsterPickingUp = TICKS_PER_SECOND * 5; @@ -10912,9 +15204,9 @@ void Entity::attack(int pose, int charge, Entity* target) dropped->vel_y = (1.5 + .025 * (local_rng.rand() % 11)) * sin(tangent); dropped->vel_z = (-10 - local_rng.rand() % 20) * .01; dropped->flags[USERFLAG1] = false; + dropped->itemOriginalOwner = hit.entity->getUID(); messagePlayerMonsterEvent(player, color, *hitstats, Language::get(3454), Language::get(3455), MSG_COMBAT); disarmed = true; - dropped->itemOriginalOwner = hit.entity->getUID(); if ( player >= 0 ) { achievementObserver.addEntityAchievementTimer(hit.entity, AchievementObserver::BARONY_ACH_IRONIC_PUNISHMENT, -1, true, 0); @@ -10927,9 +15219,17 @@ void Entity::attack(int pose, int charge, Entity* target) Entity* dropped = dropItemMonster(hitstats->shield, hit.entity, hitstats); if ( dropped ) { - if ( hitstats->EFFECTS[EFF_DISORIENTED] ) + if ( hitstats->getEffectActive(EFF_DISORIENTED) ) { - hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + if ( hitstats->getEffectActive(EFF_DISORIENTED) == 2 ) + { + // reduce counter to 2 to turn around next tick in actMonster + hitstats->EFFECTS_TIMERS[EFF_DISORIENTED] = 2; + } + else + { + hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + } } playSoundEntity(hit.entity, 406, 128); dropped->itemDelayMonsterPickingUp = TICKS_PER_SECOND * 5; @@ -10946,17 +15246,33 @@ void Entity::attack(int pose, int charge, Entity* target) } else { - if ( hitstats->EFFECTS[EFF_DISORIENTED] ) + if ( hitstats->getEffectActive(EFF_DISORIENTED) ) { - hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + if ( hitstats->getEffectActive(EFF_DISORIENTED) == 2 ) + { + // reduce counter to 2 to turn around next tick in actMonster + hitstats->EFFECTS_TIMERS[EFF_DISORIENTED] = 2; + } + else + { + hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + } } } } else { - if ( hitstats->EFFECTS[EFF_DISORIENTED] ) + if ( hitstats->getEffectActive(EFF_DISORIENTED) ) { - hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + if ( hitstats->getEffectActive(EFF_DISORIENTED) == 2 ) + { + // reduce counter to 2 to turn around next tick in actMonster + hitstats->EFFECTS_TIMERS[EFF_DISORIENTED] = 2; + } + else + { + hit.entity->setEffect(EFF_DISORIENTED, false, 0, false); + } } } } @@ -10972,7 +15288,7 @@ void Entity::attack(int pose, int charge, Entity* target) } } - if ( !hitstats->EFFECTS[EFF_DISORIENTED] && doPhantomStrike ) + if ( !hitstats->getEffectActive(EFF_DISORIENTED) && doPhantomStrike ) { if ( hit.entity->setEffect(EFF_DISORIENTED, true, TICKS_PER_SECOND, false) ) { @@ -10988,6 +15304,11 @@ void Entity::attack(int pose, int charge, Entity* target) } } + real_t shakeScale = 1.0; + if ( myStats->type == MOTH_SMALL ) + { + shakeScale = 0.4; + } if ( playerhit > 0 && multiplayer == SERVER && !players[playerhit]->isLocalPlayer() ) { if ( pose == MONSTER_POSE_GOLEM_SMASH ) @@ -10996,8 +15317,8 @@ void Entity::attack(int pose, int charge, Entity* target) { // primary target strcpy((char*)net_packet->data, "SHAK"); - net_packet->data[4] = 20; // turns into .2 - net_packet->data[5] = 20; + net_packet->data[4] = 20 * shakeScale; // turns into .2 + net_packet->data[5] = 20 * shakeScale; net_packet->address.host = net_clients[playerhit - 1].host; net_packet->address.port = net_clients[playerhit - 1].port; net_packet->len = 6; @@ -11007,8 +15328,8 @@ void Entity::attack(int pose, int charge, Entity* target) { // secondary target strcpy((char*)net_packet->data, "SHAK"); - net_packet->data[4] = 10; // turns into .1 - net_packet->data[5] = 10; + net_packet->data[4] = 10 * shakeScale; // turns into .1 + net_packet->data[5] = 10 * shakeScale; net_packet->address.host = net_clients[playerhit - 1].host; net_packet->address.port = net_clients[playerhit - 1].port; net_packet->len = 6; @@ -11028,13 +15349,13 @@ void Entity::attack(int pose, int charge, Entity* target) strcpy((char*)net_packet->data, "SHAK"); if ( damage > 0 ) { - net_packet->data[4] = 10; // turns into .1 - net_packet->data[5] = 10; + net_packet->data[4] = 10 * shakeScale; // turns into .1 + net_packet->data[5] = 10 * shakeScale; } else { - net_packet->data[4] = 5; // turns into .05 - net_packet->data[5] = 5; + net_packet->data[4] = 5 * shakeScale; // turns into .05 + net_packet->data[5] = 5 * shakeScale; } net_packet->address.host = net_clients[playerhit - 1].host; net_packet->address.port = net_clients[playerhit - 1].port; @@ -11049,28 +15370,30 @@ void Entity::attack(int pose, int charge, Entity* target) if ( target == nullptr ) { // primary target - cameravars[playerhit].shakex += .2; - cameravars[playerhit].shakey += 20; + cameravars[playerhit].shakex += .2 * shakeScale; + cameravars[playerhit].shakey += 20 * shakeScale; } else { // secondary target - cameravars[playerhit].shakex += .1; - cameravars[playerhit].shakey += 10; + cameravars[playerhit].shakex += .1 * shakeScale; + cameravars[playerhit].shakey += 10 * shakeScale; } } else if ( damage > 0 ) { - cameravars[playerhit].shakex += .1; - cameravars[playerhit].shakey += 10; + cameravars[playerhit].shakex += .1 * shakeScale; + cameravars[playerhit].shakey += 10 * shakeScale; } else { - cameravars[playerhit].shakex += .05; - cameravars[playerhit].shakey += 5; + cameravars[playerhit].shakex += .05 * shakeScale; + cameravars[playerhit].shakey += 5 * shakeScale; } } + bool blowBouncesOff = false; + if ( damage > 0 ) { Entity* gib = spawnGib(hit.entity); @@ -11107,13 +15430,14 @@ void Entity::attack(int pose, int charge, Entity* target) // display 'blow bounces off' message if ( !statusInflicted ) { + blowBouncesOff = true; messagePlayerMonsterEvent(playerhit, 0xFFFFFFFF, *myStats, Language::get(2457), Language::get(2458), MSG_COMBAT_BASIC); } if ( myStats->type == COCKATRICE && hitstats->defending ) { steamAchievementClient(playerhit, "BARONY_ACH_COCK_BLOCK"); } - else if ( myStats->type == MINOTAUR && !hitstats->defending ) + else if ( myStats->type == MINOTAUR && !hitstats->defending && parriedDamage == 0 ) { steamAchievementClient(playerhit, "BARONY_ACH_ONE_WHO_KNOCKS"); } @@ -11153,6 +15477,10 @@ void Entity::attack(int pose, int charge, Entity* target) } if ( skillIncrease ) { + if ( hitstats->getEffectActive(EFF_FORCE_SHIELD) ) + { + magicOnSpellCastEvent(hit.entity, hit.entity, this, SPELL_FORCE_SHIELD, spell_t::SpellOnCastEventTypes::SPELL_LEVEL_EVENT_DEFAULT, 1); + } hit.entity->increaseSkill(PRO_SHIELD); shieldIncreased = true; } @@ -11209,14 +15537,19 @@ void Entity::attack(int pose, int charge, Entity* target) } } + if ( oldHP - hitstats->HP > 0 ) + { + hit.entity->pinpointDamageProc(this, oldHP - hitstats->HP); + } + if ( !strncmp(hitstats->name, "inner demon", strlen("inner demon")) ) { hit.entity->modHP(damage); // undo melee damage. } DamageGib dmgGib = DMG_DEFAULT; - bool charged = std::max(charge, MAXCHARGE / 2) / ((double)(MAXCHARGE / 2)) > 1; - if ( weaponMultipliers >= 1.15 || (weaponskill == PRO_AXE && hitstats->type == MIMIC) ) + bool charged = static_cast(std::max(charge, Stat::getMaxAttackCharge(myStats) / 2) / ((double)(Stat::getMaxAttackCharge(myStats) / 2))) > 1; + if ( weaponMultipliers >= 1.15 || (weaponskill == PRO_AXE && hitstats->type == MIMIC) || shillelaghDamage > 0 ) { dmgGib = DMG_STRONGER; if ( charged ) @@ -11244,12 +15577,280 @@ void Entity::attack(int pose, int charge, Entity* target) dmgGib); } - if ( thornsEffect != 0 && damage > 0 ) + if ( parriedDamage == 0 && damage > 0 && playerhit >= 0 ) + { + if ( achievementObserver.playerAchievements[playerhit].parryTank > 0 ) + { + achievementObserver.playerAchievements[playerhit].parryTank = -1; + } + } + if ( parriedDamage > 0 ) + { + real_t parriedWeaponMultipliers = 0.0; + if ( parriedSkill == PRO_UNARMED ) + { + parriedWeaponMultipliers = Entity::getDamageTableMultiplier(this, *myStats, DAMAGE_TABLE_UNARMED); + } + else if ( parriedSkill == PRO_RANGED ) + { + parriedWeaponMultipliers = Entity::getDamageTableMultiplier(this, *myStats, DAMAGE_TABLE_RANGED); + } + else if ( parriedSkill >= 0 ) + { + DamageTableType dmgType = static_cast(parriedSkill - PRO_SWORD); + parriedWeaponMultipliers = Entity::getDamageTableMultiplier(this, *myStats, dmgType); + } + + int numBlessings = 0; + int myAC = AC(myStats); + real_t targetACEffectiveness = Entity::getACEffectiveness(this, myStats, this->behavior == &actPlayer, hit.entity, hitstats, numBlessings); + int attackAfterReductions = static_cast(std::max(0.0, ((parriedDamage * targetACEffectiveness - myAC))) + (1.0 - targetACEffectiveness) * parriedDamage); + if ( parriedSkill == PRO_UNARMED ) + { + parriedDamage = attackAfterReductions * parriedWeaponMultipliers; + } + else if ( parriedSkill == PRO_RANGED ) + { + parriedDamage = attackAfterReductions * parriedWeaponMultipliers; + } + else if ( parriedSkill >= 0 ) + { + parriedDamage = attackAfterReductions * parriedWeaponMultipliers; + } + else + { + parriedDamage = attackAfterReductions; + } + + if ( targetACEffectiveness < 0.99 ) + { + parriedDamage = std::max(1, parriedDamage); + } + else + { + parriedDamage = std::max(0, parriedDamage); + } + + spawnBang(this->x + cos(this->yaw) * 2, this->y + sin(this->yaw) * 2, 0); + + if ( hitstats->weapon && hitstats->weapon->type == RAPIER ) + { + Sint32 oldHP = myStats->HP; + this->modHP(-parriedDamage); + if ( oldHP > myStats->HP ) + { + if ( hit.entity->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdate(hit.entity->skill[2], + Compendium_t::CPDM_PARRIES_DMG, RAPIER, oldHP - myStats->HP); + steamStatisticUpdateClient(hit.entity->skill[2], STEAM_STAT_TOUCHE, STEAM_STAT_INT, oldHP - myStats->HP); + } + } + if ( myStats->HP <= 0 && oldHP > myStats->HP ) + { + hit.entity->killedByMonsterObituary(this); + hit.entity->awardXP(this, true, true); + } + } + + if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 5; // turns into .1 + net_packet->data[5] = 5; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + else if ( player >= 0 && players[player]->isLocalPlayer() ) + { + cameravars[player].shakex += 0.05; + cameravars[player].shakey += 5; + } + + if ( player >= 0 ) + { + Uint32 constexpr color = makeColorRGB(255, 0, 0); + const char* msg = Language::get(6435); // named + if ( !strcmp(hitstats->name, "") || monsterNameIsGeneric(*hitstats) ) + { + msg = Language::get(6434); + } + + if ( !strcmp(hitstats->name, "") ) + { + messagePlayerColor(player, MESSAGE_COMBAT, color, msg, getMonsterLocalizedName(hitstats->type).c_str()); + } + else + { + messagePlayerColor(player, MESSAGE_COMBAT, color, msg, hitstats->name); + } + } + if ( playerhit >= 0 ) + { + Uint32 constexpr color = makeColorRGB(0, 255, 0); + const char* msg = Language::get(6437); // named + if ( !strcmp(myStats->name, "") || monsterNameIsGeneric(*myStats) ) + { + msg = Language::get(6436); + } + + if ( !strcmp(myStats->name, "") ) + { + messagePlayerColor(playerhit, MESSAGE_COMBAT, color, msg, getMonsterLocalizedName(myStats->type).c_str()); + } + else + { + messagePlayerColor(playerhit, MESSAGE_COMBAT, color, msg, myStats->name); + } + if ( damage == 0 ) + { + achievementObserver.playerAchievements[playerhit].parryTank += 1; + } + else + { + if ( achievementObserver.playerAchievements[playerhit].parryTank > 0 ) + { + achievementObserver.playerAchievements[playerhit].parryTank = -1; + } + } + } + + for ( int i = 0; i < 2; ++i ) + { + Entity* enemyHit = (i == 0 ? hit.entity : this); + Entity* enemyAttacker = (i == 0 ? this : hit.entity); + if ( enemyHit->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + real_t pushbackMultiplier = 0.3; + knockbackInflicted = true; + + real_t tangent = atan2(enemyHit->y - enemyAttacker->y, enemyHit->x - enemyAttacker->x); + if ( enemyHit->behavior == &actMonster ) + { + enemyHit->vel_x = cos(tangent) * pushbackMultiplier; + enemyHit->vel_y = sin(tangent) * pushbackMultiplier; + enemyHit->monsterKnockbackVelocity = 0.01; + enemyHit->monsterKnockbackUID = enemyAttacker->getUID(); + enemyHit->monsterKnockbackTangentDir = tangent; + //enemyHit->lookAtEntity(*parent); + } + else if ( enemyHit->behavior == &actPlayer ) + { + if ( !players[enemyHit->skill[2]]->isLocalPlayer() ) + { + enemyHit->monsterKnockbackVelocity = pushbackMultiplier; + enemyHit->monsterKnockbackTangentDir = tangent; + serverUpdateEntityFSkill(enemyHit, 11); + serverUpdateEntityFSkill(enemyHit, 9); + } + else + { + enemyHit->monsterKnockbackVelocity = pushbackMultiplier; + enemyHit->monsterKnockbackTangentDir = tangent; + } + } + } + } + + if ( parriedDamage > 0 ) + { + // update enemy bar for attacker + if ( !strcmp(myStats->name, "") ) + { + updateEnemyBar(hit.entity, this, getMonsterLocalizedName(myStats->type).c_str(), myStats->HP, myStats->MAXHP, false, + DamageGib::DMG_DEFAULT); + } + else + { + updateEnemyBar(hit.entity, this, myStats->name, myStats->HP, myStats->MAXHP, false, + DamageGib::DMG_DEFAULT); + } + } + } + + if ( flail && !hitstats->getEffectActive(EFF_KNOCKBACK) && pose == MONSTER_POSE_FLAIL_SWING ) + { + real_t dist = entityDist(hit.entity, this); + real_t ratio = 0.0; + if ( behavior == &actMonster ) + { + if ( dist <= 24.0 ) + { + ratio = 0.5; + } + } + else + { + ratio = std::max(0.0, std::min(1.0, (40.0 - dist) / (40.0))); + } + if ( ratio >= 0.5 && hit.entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + real_t pushbackMultiplier = ratio; + if ( hit.entity->behavior == &actPlayer ) + { + pushbackMultiplier = 0.5; + } + + knockbackInflicted = true; + + real_t tangent = atan2(hit.entity->y - this->y, hit.entity->x - this->x); + if ( hit.entity->behavior == &actMonster ) + { + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackUID = this->getUID(); + hit.entity->monsterKnockbackTangentDir = tangent; + //enemyHit->lookAtEntity(*parent); + } + else if ( hit.entity->behavior == &actPlayer ) + { + if ( !players[hit.entity->skill[2]]->isLocalPlayer() ) + { + hit.entity->monsterKnockbackVelocity = pushbackMultiplier; + hit.entity->monsterKnockbackTangentDir = tangent; + serverUpdateEntityFSkill(hit.entity, 11); + serverUpdateEntityFSkill(hit.entity, 9); + } + else + { + hit.entity->monsterKnockbackVelocity = pushbackMultiplier; + hit.entity->monsterKnockbackTangentDir = tangent; + } + } + } + } + + if ( thornsEffect != 0 && (damage > 0 || thornsAllowZeroDmg) ) { + if ( hitstats->getEffectActive(EFF_BLADEVINES) ) + { + if ( !myStats->getEffectActive(EFF_BLEEDING) ) + { + if ( setEffect(EFF_BLEEDING, true, 3 * TICKS_PER_SECOND, false) ) + { + myStats->bleedInflictedBy = hit.entity->getUID(); + for ( int gibs = 0; gibs < 3; ++gibs ) + { + Entity* gib = spawnGib(this); + serverSpawnGibForClient(gib); + } + } + } + } + Sint32 oldHP = myStats->HP; this->modHP(-abs(thornsEffect)); - if ( myStats->HP <= 0 && myStats->OLDHP > myStats->HP ) + if ( myStats->HP <= 0 && oldHP > myStats->HP ) { + hit.entity->killedByMonsterObituary(this); hit.entity->awardXP(this, true, true); + + if ( hit.entity->behavior == &actPlayer && hitstats->playerRace == RACE_DRYAD && hitstats->stat_appearance == 0 ) + { + steamStatisticUpdateClient(hit.entity->skill[2], STEAM_STAT_PRICKLY_PERSONALITY, STEAM_STAT_INT, 1); + } } if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) { @@ -11298,6 +15899,41 @@ void Entity::attack(int pose, int charge, Entity* target) DamageGib::DMG_DEFAULT); } } + + if ( hitstats->getEffectActive(EFF_FLAME_CLOAK) ) + { + if ( this->flags[BURNABLE] ) + { + bool wasBurning = this->flags[BURNING]; + int chance = local_rng.rand() % 100; + if ( chance < hitstats->getEffectActive(EFF_FLAME_CLOAK) ) + { + if ( this->SetEntityOnFire(hit.entity) ) + { + if ( !wasBurning && this->flags[BURNING] ) + { + this->char_fire = std::min(this->char_fire, getSpellEffectDurationSecondaryFromID(SPELL_FLAME_CLOAK, hit.entity, nullptr, hit.entity)); + myStats->burningInflictedBy = static_cast(hit.entity->getUID()); + // If a Player was hit, and they are now on fire, tell them what set them on fire + if ( player >= 0 && this->flags[BURNING] ) + { + messagePlayer(player, MESSAGE_COMBAT, Language::get(6734)); + } + if ( playerhit >= 0 && this->flags[BURNING] ) + { + messagePlayerMonsterEvent(playerhit, makeColorRGB(0, 255, 0), *myStats, + Language::get(6732), Language::get(6733), MSG_COMBAT); + } + if ( playerhit >= 0 ) + { + players[playerhit]->mechanics.updateSustainedSpellEvent(SPELL_FLAME_CLOAK, 100.0, 1.0, this); + } + } + } + } + } + } + if ( hitstats->type == INCUBUS && !strncmp(hitstats->name, "inner demon", strlen("inner demon")) ) { @@ -11309,6 +15945,7 @@ void Entity::attack(int pose, int charge, Entity* target) { if ( myStats->HP <= 0 ) { + illusionParent->killedByMonsterObituary(this); illusionParent->awardXP(this, true, true); if ( illusionParent->behavior == &actPlayer ) { @@ -11334,6 +15971,8 @@ void Entity::attack(int pose, int charge, Entity* target) spawnMagicEffectParticles(this->x, this->y, this->z, 983); if ( illusionParent->behavior == &actPlayer && illusionParent != this ) { + players[illusionParent->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_DEMON_ILLUSION, 30.0, 1.0, this); + // update enemy bar for attacker if ( !strcmp(myStats->name, "") ) { @@ -11349,6 +15988,11 @@ void Entity::attack(int pose, int charge, Entity* target) } } + if ( hitstats->getEffectActive(EFF_SPORES) ) + { + floorMagicCreateSpores(hit.entity, hit.entity->x, hit.entity->y, hit.entity, 0, SPELL_SPORES); + } + if ( !disarmed ) { if ( whip ) @@ -11357,12 +16001,20 @@ void Entity::attack(int pose, int charge, Entity* target) } else { - playSoundEntity(hit.entity, 28, 64); + if ( blowBouncesOff && hitstats && hitstats->shield && hitstats->shield->type == TOOL_FRYING_PAN + && hitstats->defending && !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) + { + playSoundEntity(hit.entity, 776, 64); + } + else + { + playSoundEntity(hit.entity, 28, 64); + } } } // chance of bleeding - bool wasBleeding = hitstats->EFFECTS[EFF_BLEEDING]; // check if currently bleeding when this roll occurred. + bool wasBleeding = hitstats->getEffectActive(EFF_BLEEDING) > 0; // check if currently bleeding when this roll occurred. if ( gibtype[(int)hitstats->type] > 0 && gibtype[(int)hitstats->type] != 5 ) { if ( bleedStatusInflicted || (hitstats->HP > 5 && damage > 0) ) @@ -11374,7 +16026,7 @@ void Entity::attack(int pose, int charge, Entity* target) || (local_rng.rand() % 4 == 0 && pose == PLAYER_POSE_GOLEM_SMASH) || (thornsEffect < 0 && behavior == &actPlayer) || (local_rng.rand() % 10 == 0 && myStats->type == VAMPIRE && myStats->weapon == nullptr) - || (local_rng.rand() % 8 == 0 && myStats->EFFECTS[EFF_VAMPIRICAURA] && (myStats->weapon == nullptr || myStats->type == LICH_FIRE)) + || (local_rng.rand() % 8 == 0 && myStats->getEffectActive(EFF_VAMPIRICAURA) && (myStats->weapon == nullptr || myStats->type == LICH_FIRE)) ) { bool heavyBleedEffect = false; // heavy bleed will have a greater starting duration, and add to existing duration. @@ -11386,7 +16038,7 @@ void Entity::attack(int pose, int charge, Entity* target) { heavyBleedEffect = false; } - else if ( (myStats->type == VAMPIRE && this->behavior == &actMonster) || myStats->EFFECTS[EFF_VAMPIRICAURA] ) + else if ( (myStats->type == VAMPIRE && this->behavior == &actMonster) || myStats->getEffectActive(EFF_VAMPIRICAURA) ) { if ( local_rng.rand() % 2 == 0 ) // 50% for heavy bleed effect. { @@ -11420,7 +16072,7 @@ void Entity::attack(int pose, int charge, Entity* target) { hitstats->EFFECTS_TIMERS[EFF_BLEEDING] = std::max(480 + (int)local_rng.rand() % 360 - hit.entity->getCON() * 100, 120); // 2.4-16.8 seconds } - hitstats->EFFECTS[EFF_BLEEDING] = true; + hitstats->setEffectActive(EFF_BLEEDING, 1); strcpy(playerHitMessage, Language::get(701)); if ( !strcmp(hitstats->name, "") || monsterNameIsGeneric(*hitstats) ) { @@ -11436,7 +16088,7 @@ void Entity::attack(int pose, int charge, Entity* target) if ( !wasBleeding ) { hitstats->EFFECTS_TIMERS[EFF_BLEEDING] = std::max(500 + (int)local_rng.rand() % 500 - hit.entity->getCON() * 10, 250); // 5-20 seconds - hitstats->EFFECTS[EFF_BLEEDING] = true; + hitstats->setEffectActive(EFF_BLEEDING, 1); strcpy(playerHitMessage, Language::get(2451)); if ( !strcmp(hitstats->name, "") || monsterNameIsGeneric(*hitstats) ) { @@ -11450,7 +16102,7 @@ void Entity::attack(int pose, int charge, Entity* target) else { hitstats->EFFECTS_TIMERS[EFF_BLEEDING] += std::max((int)local_rng.rand() % 350 - hit.entity->getCON() * 5, 100); // 2-7 seconds in addition - hitstats->EFFECTS[EFF_BLEEDING] = true; + hitstats->setEffectActive(EFF_BLEEDING, 1); strcpy(playerHitMessage, Language::get(2454)); if ( !strcmp(hitstats->name, "") || monsterNameIsGeneric(*hitstats) ) { @@ -11464,12 +16116,12 @@ void Entity::attack(int pose, int charge, Entity* target) } // message player of effect, skip if hit entity was already bleeding. - if ( hitstats->EFFECTS[EFF_BLEEDING] && (!wasBleeding || heavyBleedEffect) ) + if ( hitstats->getEffectActive(EFF_BLEEDING) && (!wasBleeding || heavyBleedEffect) ) { hitstats->bleedInflictedBy = static_cast(this->getUID()); if ( heavyBleedEffect ) { - hitstats->EFFECTS[EFF_SLOW] = true; + hitstats->setEffectActive(EFF_SLOW, 1); hitstats->EFFECTS_TIMERS[EFF_SLOW] = 60; } if ( hit.entity->behavior == &actPlayer && multiplayer == SERVER ) @@ -11498,9 +16150,10 @@ void Entity::attack(int pose, int charge, Entity* target) // energize if wearing punisher hood! if ( myStats->helmet && myStats->helmet->type == PUNISHER_HOOD ) { - this->modMP(1 + local_rng.rand() % 2); + int mpAmount = this->modMP(1 + local_rng.rand() % 2); + this->playerInsectoidIncrementHungerToMP(mpAmount); Uint32 color = makeColorRGB(0, 255, 0); - this->setEffect(EFF_MP_REGEN, true, 250, true); + this->setEffect(EFF_MP_REGEN, true, std::max(myStats->EFFECTS_TIMERS[EFF_MP_REGEN], 10 * TICKS_PER_SECOND), false); if ( behavior == &actPlayer ) { messagePlayerColor(player, MESSAGE_HINT, color, Language::get(3753)); @@ -11629,7 +16282,7 @@ void Entity::attack(int pose, int charge, Entity* target) } } } - else if ( myStats->EFFECTS[EFF_VAMPIRICAURA] && myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] > 0 ) + else if ( myStats->getEffectActive(EFF_VAMPIRICAURA) && myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] > 0 ) { tryLifesteal = true; if ( backstab || flanking ) @@ -11645,7 +16298,7 @@ void Entity::attack(int pose, int charge, Entity* target) lifeStealAmount = std::max(3, lifeStealAmount); } } - else if ( (myStats->EFFECTS[EFF_VAMPIRICAURA] && (myStats->weapon == nullptr || myStats->type == LICH_FIRE)) ) + else if ( (myStats->getEffectActive(EFF_VAMPIRICAURA) && (myStats->weapon == nullptr || myStats->type == LICH_FIRE)) ) { tryLifesteal = true; } @@ -11677,7 +16330,7 @@ void Entity::attack(int pose, int charge, Entity* target) playSoundEntity(this, 168, 128); lifestealSuccess = true; } - else if ( !wasBleeding && hitstats->EFFECTS[EFF_BLEEDING] ) + else if ( !wasBleeding && hitstats->getEffectActive(EFF_BLEEDING) ) { // attack caused the target to bleed, trigger lifesteal tick this->modHP(lifeStealAmount); @@ -11685,7 +16338,7 @@ void Entity::attack(int pose, int charge, Entity* target) playSoundEntity(this, 168, 128); lifestealSuccess = true; } - else if ( (local_rng.rand() % 4 == 0) && (myStats->type == VAMPIRE && behavior == &actMonster && myStats->EFFECTS[EFF_VAMPIRICAURA]) ) + else if ( (local_rng.rand() % 4 == 0) && (myStats->type == VAMPIRE && behavior == &actMonster && myStats->getEffectActive(EFF_VAMPIRICAURA)) ) { // vampires under aura have higher chance. this->modHP(lifeStealAmount); @@ -11706,6 +16359,14 @@ void Entity::attack(int pose, int charge, Entity* target) { if ( player >= 0 ) { + if ( myStats->getEffectActive(EFF_VAMPIRICAURA) && myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] > 0 ) + { + if ( behavior == &actPlayer ) + { + players[player]->mechanics.updateSustainedSpellEvent(SPELL_VAMPIRIC_AURA, 50.0, 1.0, hit.entity); + } + } + myStats->HUNGER = std::min(1499, myStats->HUNGER + 100); serverUpdateHunger(player); if ( stats[player]->type == VAMPIRE ) @@ -11779,7 +16440,7 @@ void Entity::attack(int pose, int charge, Entity* target) { spawnBloodVial = true; } - else if ( hitstats->EFFECTS[EFF_BLEEDING] || myStats->EFFECTS[EFF_VAMPIRICAURA] ) + else if ( hitstats->getEffectActive(EFF_BLEEDING) || myStats->getEffectActive(EFF_VAMPIRICAURA) ) { if ( hitstats->EFFECTS_TIMERS[EFF_BLEEDING] >= 250 ) { @@ -11804,12 +16465,59 @@ void Entity::attack(int pose, int charge, Entity* target) spawnBloodVial = (local_rng.rand() % 10 == 0); } - if ( spawnBloodVial ) + if ( spawnBloodVial ) + { + Item* blood = newItem(FOOD_BLOOD, EXCELLENT, 0, 1, gibtype[hitstats->type] - 1, true, &hitstats->inventory); + if ( spawnSecondVial ) + { + blood = newItem(FOOD_BLOOD, EXCELLENT, 0, 1, gibtype[hitstats->type] - 1, true, &hitstats->inventory); + } + } + } + + if ( hitstats->HP > 0 && hitstats->OLDHP > hitstats->HP ) + { + // assist damage from summons + if ( this->behavior == &actMonster ) + { + int summonSpellID = getSpellFromSummonedEntityForSpellEvent(this); + if ( summonSpellID != SPELL_NONE ) + { + if ( Entity* leader = uidToEntity(myStats->leader_uid) ) + { + if ( local_rng.rand() % 8 == 0 ) + { + magicOnSpellCastEvent(leader, nullptr, hit.entity, summonSpellID, spell_t::SPELL_LEVEL_EVENT_ASSIST | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + + Sint32 damageTaken = oldHP - hitstats->HP; + if ( damageTaken > 0 && leader->behavior == &actPlayer && summonSpellID == SPELL_SPIRIT_WEAPON ) + { + if ( myStats->getAttribute("SUMMON_BY_SPELLBOOK") != "" + && std::stoi(myStats->getAttribute("SUMMON_BY_SPELLBOOK")) == summonSpellID ) + { + Compendium_t::Events_t::eventUpdate(leader->skill[2], Compendium_t::CPDM_SPELL_DMG, + SPELLBOOK_SPIRIT_WEAPON, damageTaken); + } + else + { + Compendium_t::Events_t::eventUpdate(leader->skill[2], + Compendium_t::CPDM_SPELL_DMG, SPELL_ITEM, damageTaken, false, summonSpellID); + } + } + } + } + } + + } + + if ( damage > 0 && hitstats->OLDHP > hitstats->HP ) + { + if ( myStats->getEffectActive(EFF_VAMPIRICAURA) && myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] > 0 ) { - Item* blood = newItem(FOOD_BLOOD, EXCELLENT, 0, 1, gibtype[hitstats->type] - 1, true, &hitstats->inventory); - if ( spawnSecondVial ) + if ( behavior == &actPlayer ) { - blood = newItem(FOOD_BLOOD, EXCELLENT, 0, 1, gibtype[hitstats->type] - 1, true, &hitstats->inventory); + players[player]->mechanics.updateSustainedSpellEvent(SPELL_VAMPIRIC_AURA, 10.0, 1.0, hit.entity); } } } @@ -11842,7 +16550,7 @@ void Entity::attack(int pose, int charge, Entity* target) } else { - if ( !miss && ((dist != STRIKERANGE && !whip) || (dist != STRIKERANGE * 1.5 && whip)) ) + if ( !guard && !miss && (dist != strikeRange) ) { // hit a wall if ( pose == PLAYER_POSE_GOLEM_SMASH ) @@ -11908,6 +16616,7 @@ void Entity::attack(int pose, int charge, Entity* target) playSoundPos(hit.x, hit.y, 66, 128); // strike wall // bang spawnBang(hit.x - cos(yaw) * 2, hit.y - sin(yaw) * 2, 0); + degradePickaxe = false; } else if ( swimmingtiles[map.tiles[OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height]] || lavatiles[map.tiles[OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height]] ) @@ -11917,6 +16626,7 @@ void Entity::attack(int pose, int charge, Entity* target) } else if ( !mapTileDiggable(hit.mapx, hit.mapy) ) { + degradePickaxe = false; spawnBang(hit.x - cos(yaw) * 2, hit.y - sin(yaw) * 2, 0); messagePlayer(player, MESSAGE_HINT, Language::get(706)); } @@ -11987,7 +16697,8 @@ void Entity::attack(int pose, int charge, Entity* target) generatePathMaps(); } int chance = 2 + (myStats->type == GOBLIN ? 2 : 0); - if ( local_rng.rand() % chance && degradePickaxe && myStats->weapon ) + if ( local_rng.rand() % chance == 0 && degradePickaxe && myStats->weapon + && !spellEffectPreserveItem(myStats->weapon) ) { if ( myStats->weapon->status > BROKEN ) { @@ -12019,9 +16730,10 @@ void Entity::attack(int pose, int charge, Entity* target) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = myStats->weapon->status; + SDLNet_Write16((int)myStats->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -12151,9 +16863,11 @@ void Entity::attack(int pose, int charge, Entity* target) net_packet->data[4] = player; net_packet->data[5] = pose; net_packet->data[6] = charge; + SDLNet_Write32((Sint32)(myStats->weapon ? myStats->weapon->type : 0), &net_packet->data[7]); + SDLNet_Write32((Uint32)(myStats->weapon ? myStats->weapon->appearance : 0), &net_packet->data[11]); net_packet->address.host = net_server.host; net_packet->address.port = net_server.port; - net_packet->len = 7; + net_packet->len = 15; sendPacketSafe(net_sock, -1, net_packet, 0); } } @@ -12174,10 +16888,12 @@ int AC(Stat* stat) } Entity* playerEntity = nullptr; + int player = -1; for ( int i = 0; i < MAXPLAYERS; ++i ) { if ( stat && stats[i] == stat ) { + player = i; if ( players[i] && players[i]->entity ) { playerEntity = players[i]->entity; @@ -12186,6 +16902,24 @@ int AC(Stat* stat) } } int armor = statGetCON(stat, playerEntity); + if ( stat->getEffectActive(EFF_FOCI_LIGHT_SANCTUARY) ) + { + armor += getSpellDamageFromID(SPELL_FOCI_LIGHT_SANCTUARY, nullptr, nullptr, nullptr); + int tier = std::max(0, (stat->getEffectActive(EFF_FOCI_LIGHT_SANCTUARY) - 1)); + armor += tier * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_SANCTUARY, nullptr, nullptr, nullptr); + } + if ( stat->getEffectActive(EFF_GUARD_BODY) ) + { + armor += thaumSpellArmorProc(playerEntity, *stat, true, nullptr, EFF_GUARD_BODY); + } + if ( stat->getEffectActive(EFF_DIVINE_GUARD) ) + { + armor += thaumSpellArmorProc(playerEntity, *stat, true, nullptr, EFF_DIVINE_GUARD); + } + if ( stat->getEffectActive(EFF_DISRUPTED) ) + { + armor -= std::min((Uint8)3, stat->getEffectActive(EFF_DISRUPTED)) * 5; + } if ( stat->helmet ) { @@ -12203,9 +16937,12 @@ int AC(Stat* stat) { armor += stat->shoes->armorGetAC(stat); } - if ( stat->shield ) + if ( !(stat->parrying && stat->weapon) ) { - armor += stat->shield->armorGetAC(stat); + if ( stat->shield ) + { + armor += stat->shield->armorGetAC(stat); + } } if ( stat->cloak ) { @@ -12231,17 +16968,42 @@ int AC(Stat* stat) } } - if ( stat->shield ) + if ( stat->type == MYCONID && player >= 0 ) + { + if ( !stat->helmet && stat->getEffectActive(EFF_GROWTH) > 1 ) + { + int bonus = std::min(3, stat->getEffectActive(EFF_GROWTH) - 1); + armor += bonus; + } + } + + if ( stat->parrying && stat->weapon ) + { + armor += Stat::getParryingACBonus(stat, stat->weapon, true, false, getWeaponSkill(stat->weapon)); + } + else if ( stat->shield ) { int shieldskill = stat->getPassiveShieldBonus(true, false); armor += shieldskill; + if ( stat->getEffectActive(EFF_FORCE_SHIELD) > 0 ) + { + armor += stat->getEffectActive(EFF_FORCE_SHIELD); + } if ( stat->defending ) { //messagePlayer(0, "shield up! +%d", 5 + stat->PROFICIENCIES[PRO_SHIELD] / 5); armor += stat->getActiveShieldBonus(true, false); + /*if ( stat->getEffectActive(EFF_FORCE_SHIELD) > 0 ) + { + armor += stat->getEffectActive(EFF_FORCE_SHIELD); + }*/ } } - if ( stat->type == MIMIC && stat->EFFECTS[EFF_MIMIC_LOCKED] ) + if ( stat->type == EARTH_ELEMENTAL && stat->defending ) + { + armor *= 2; + } + if ( stat->type == MIMIC && stat->getEffectActive(EFF_MIMIC_LOCKED) ) { armor *= 2; } @@ -12286,19 +17048,6 @@ bool Entity::teleport(int tele_x, int tele_y) return false; } - // play sound effect - int sfx = 77; - if ( behavior == &actDeathGhost ) - { - sfx = 608 + local_rng.rand() % 3; - playSoundEntity(this, sfx, 128); - } - else - { - playSoundEntity(this, sfx, 64); - } - spawnPoof(x, y, 0, 1.0, true); - // relocate entity double oldx = x; double oldy = y; @@ -12306,14 +17055,46 @@ bool Entity::teleport(int tele_x, int tele_y) y = (tele_y << 4) + 8; if ( entityInsideSomething(this) && getRace() != LICH_FIRE && getRace() != LICH_ICE ) { - x = oldx; - y = oldy; - if ( multiplayer == SERVER && player > 0 ) + if ( behavior == &actPlayer && barony_clear(x, y, this) ) + { + // it's fine + if ( !strncmp(map.name, "Mages Guild", 11) ) // mages guild out of bounds + { + x = oldx; + y = oldy; + messagePlayer(player, MESSAGE_HINT, Language::get(707)); + return false; + } + } + else { + x = oldx; + y = oldy; messagePlayer(player, MESSAGE_HINT, Language::get(707)); + return false; } - return false; } + + // play sound effect + int sfx = 77; + if ( behavior == &actDeathGhost ) + { + if ( skill[2] >= 0 && skill[2] < MAXPLAYERS && players[skill[2]]->ghost.isSpiritGhost() ) + { + sfx = 794 + local_rng.rand() % 2; + } + else + { + sfx = 608 + local_rng.rand() % 3; + } + playSoundPos(oldx, oldy, sfx, 128); + } + else + { + playSoundPos(oldx, oldy, sfx, 64); + } + spawnPoof(oldx, oldy, 0, 1.0, true); + updateAchievementBaitAndSwitch(player, true); if ( multiplayer != CLIENT ) { @@ -12390,7 +17171,7 @@ Teleports the given entity to a random location on the map. -------------------------------------------------------------------------------*/ -bool Entity::teleportRandom() +bool Entity::teleportRandom(int x1, int x2, int y1, int y2) { int numlocations = 0; int pickedlocation; @@ -12408,9 +17189,26 @@ bool Entity::teleportRandom() } } - for ( int iy = 1; iy < map.height; ++iy ) + if ( x1 == 0 ) + { + x1 = 1; + } + if ( x2 == 0 ) + { + x2 = map.width; + } + if ( y1 == 0 ) + { + y1 = 1; + } + if ( y2 == 0 ) + { + y2 = map.height; + } + + for ( int iy = y1; iy < y2; ++iy ) { - for ( int ix = 1; ix < map.width; ++ix ) + for ( int ix = x1; ix < x2; ++ix ) { if ( !checkObstacle((ix << 4) + 8, (iy << 4) + 8, this, NULL) ) { @@ -12425,9 +17223,9 @@ bool Entity::teleportRandom() } pickedlocation = local_rng.rand() % numlocations; numlocations = 0; - for ( int iy = 1; iy < map.height; iy++ ) + for ( int iy = y1; iy < y2; ++iy ) { - for ( int ix = 1; ix < map.width; ix++ ) + for ( int ix = x1; ix < x2; ++ix ) { if ( !checkObstacle((ix << 4) + 8, (iy << 4) + 8, this, NULL) ) { @@ -12590,7 +17388,7 @@ bool Entity::teleportAroundEntity(Entity* target, int dist, int effectType) } else { - if ( target->behavior == &actBomb && target->skill[22] == 1 && ix == tx && iy == ty ) // teleport receiver. + if ( target->behavior == &actBomb && target->skill[22] == 1 && ix == tx && iy == ty ) // teleport receiver { // directly on top, let's go there. real_t tmpx = x; @@ -12621,7 +17419,7 @@ bool Entity::teleportAroundEntity(Entity* target, int dist, int effectType) x = tmpx; y = tmpy; } - else if ( target->behavior == &::actTeleportShrine && ix == tx && iy == ty ) + else if ( (target->behavior == &::actTeleportShrine || effectType == SPELL_JUMP) && ix == tx && iy == ty ) { // directly on top, let's go there. real_t tmpx = x; @@ -12668,15 +17466,69 @@ bool Entity::teleportAroundEntity(Entity* target, int dist, int effectType) y = (iy << 4) + 8; if ( !entityInsideSomething(this) ) { - if ( behavior == &actDeathGhost ) + if ( effectType == SPELL_JUMP && (this == target) ) { - goodspots.push_back(Coord_t(ix, iy, false)); + Entity* ohit = hit.entity; + if ( this->bodyparts.size() ) + { + // check LOS + TileEntityList.updateEntity(*this); // important - lineTrace needs the TileEntityListUpdated. + + Entity* tmpTarget = nullptr; + for ( auto limb : bodyparts ) + { + if ( limb->behavior == &actPlayerLimb ) + { + tmpTarget = limb; + break; + } + } + + if ( tmpTarget ) + { + //real_t tmpx2 = tmpTarget->x; + //real_t tmpy2 = tmpTarget->y; + //tmpTarget->x = tmpx; + //tmpTarget->y = tmpy; + bool oldPassable = tmpTarget->flags[PASSABLE]; + tmpTarget->flags[PASSABLE] = false; + TileEntityList.updateEntity(*tmpTarget); + + // pretend player has teleported, get the angle needed. + real_t tangent = atan2(tmpTarget->y - this->y, tmpTarget->x - this->x); + lineTraceTarget(this, this->x, this->y, tangent, 64 * dist, LINETRACE_TELEKINESIS, false, tmpTarget); + if ( hit.entity == tmpTarget ) + { + goodspots.push_back(Coord_t(ix, iy, teleportCoordHasTrap(ix, iy))); + numlocations++; + } + + tmpTarget->flags[PASSABLE] = oldPassable; + //tmpTarget->x = tmpx2; + //tmpTarget->y = tmpy2; + TileEntityList.updateEntity(*tmpTarget); + } + + } + // restore coordinates. + x = tmpx; + y = tmpy; + TileEntityList.updateEntity(*this); // important - lineTrace needs the TileEntityListUpdated. + hit.entity = ohit; + continue; } else { - goodspots.push_back(Coord_t(ix, iy, teleportCoordHasTrap(ix, iy))); + if ( behavior == &actDeathGhost ) + { + goodspots.push_back(Coord_t(ix, iy, false)); + } + else + { + goodspots.push_back(Coord_t(ix, iy, teleportCoordHasTrap(ix, iy))); + } + numlocations++; } - numlocations++; } // restore coordinates. x = tmpx; @@ -12773,7 +17625,10 @@ bool Entity::teleportAroundEntity(Entity* target, int dist, int effectType) // restore coordinates. x = tmpx; y = tmpy; - this->yaw = tangent; + if ( target != this ) + { + this->yaw = tangent; + } if ( target->behavior == &actMonster && target->monsterTarget == getUID() ) { target->monsterReleaseAttackTarget(); @@ -12848,7 +17703,7 @@ bool Entity::teleporterMove(int tele_x, int tele_y, int type) { playSoundEntityLocal(this, 96, 64); } - else if ( type == 2 ) + else if ( type == 2 || type == 3 ) { playSoundEntityLocal(this, 154, 64); } @@ -12865,7 +17720,7 @@ Awards XP to the dest (ie killer) entity from the src (ie killed) entity void Entity::awardXP(Entity* src, bool share, bool root) { - if ( !src ) + if ( !src ) { return; } @@ -12883,13 +17738,26 @@ void Entity::awardXP(Entity* src, bool share, bool root) Compendium_t::Events_t::eventUpdateMonster(src->skill[2], Compendium_t::CPDM_KILLED_BY, this, 1); } + if ( this->behavior == &actPlayer && root ) + { + players[this->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_KILL, src); + } + if ( src->behavior == &actMonster && (src->monsterAllySummonRank != 0 + || srcStats->type == REVENANT_SKULL + || srcStats->type == MONSTER_ADORCISED_WEAPON + || (srcStats->type == MOTH_SMALL && srcStats->getAttribute("fire_sprite") != "") + || (srcStats->type == SKELETON && srcStats->getAttribute("revenant_skeleton") != "") + || srcStats->type == FLAME_ELEMENTAL + || srcStats->type == DUCK_SMALL + || (srcStats->type == EARTH_ELEMENTAL && src->monsterAllyGetPlayerLeader()) + || srcStats->type == HOLOGRAM || src->monsterIsTinkeringCreation()) ) { if ( root ) { - if ( src->monsterIsTinkeringCreation() ) + if ( !(src->monsterAllySummonRank != 0) ) { int compendiumPlayer = behavior == &actPlayer ? skill[2] : -1; if ( behavior == &actMonster ) @@ -12929,6 +17797,10 @@ void Entity::awardXP(Entity* src, bool share, bool root) { return; } + if ( src == this ) + { + return; + } int player = -1; if ( behavior == &actPlayer ) @@ -12940,27 +17812,75 @@ void Entity::awardXP(Entity* src, bool share, bool root) } } + if ( src->monsterAllyGetPlayerLeader() && (this->behavior == &actPlayer || this->monsterAllyGetPlayerLeader()) ) + { + return; + } + // calculate XP gain int baseXp = 10; if ( srcStats->type == BAT_SMALL ) { baseXp = 1 + local_rng.rand() % 2; } - else if ( srcStats->type == SLIME ) + /*else if ( srcStats->type == SLIME ) { baseXp = 5; - } + }*/ else if ( srcStats->type == GNOME ) { baseXp = 5; } - int xpGain = baseXp + local_rng.rand() % std::max(1, baseXp) + std::max(0, srcStats->LVL - destStats->LVL) * baseXp; + int baseXpTop = baseXp; + + static ConsoleVariable cvar_level_curve("/levelcurve", true); + if ( svFlags & SV_FLAG_CHEATS && !*cvar_level_curve ) + { + // disabled curve + } + else if ( srcStats->LVL - destStats->LVL < 0 ) + { + if ( srcStats->LVL - destStats->LVL <= -5 ) + { + baseXpTop += (srcStats->LVL - destStats->LVL) + 5; + } + if ( srcStats->LVL - destStats->LVL <= -20 ) + { + if ( baseXp > 5 ) + { + baseXp += ((srcStats->LVL - destStats->LVL) + 20) / 4; + if ( destStats->LVL >= 60 ) + { + baseXp = std::max(3, baseXp); + } + else if ( destStats->LVL >= 50 ) + { + baseXp = std::max(4, baseXp); + } + else + { + baseXp = std::max(5, baseXp); + } + } + } + } + int xpGain = baseXp + local_rng.rand() % std::max(2, baseXpTop) + std::max(0, srcStats->LVL - destStats->LVL) * baseXp; if ( srcStats->MISC_FLAGS[STAT_FLAG_XP_PERCENT_AWARD] > 0 ) { int value = srcStats->MISC_FLAGS[STAT_FLAG_XP_PERCENT_AWARD] - 1; // offset by 1 since 0 is nothing double percent = value / 100.f; xpGain = percent * xpGain; } + if ( srcStats->getEffectActive(EFF_MINIMISE) ) + { + int effectStrength = srcStats->getEffectActive(EFF_MINIMISE) & 0xF; + xpGain = std::max(1, std::min((int)(xpGain * (1.0 - 0.1 * effectStrength)), xpGain - 2 * effectStrength)); + } + else if ( srcStats->getEffectActive(EFF_MAXIMISE) ) + { + int effectStrength = srcStats->getEffectActive(EFF_MAXIMISE) & 0xF; + xpGain = std::max(1, std::min((int)(xpGain * (1.0 + 0.1 * effectStrength)), xpGain + 2 * effectStrength)); + } if ( gameplayCustomManager.inUse() ) { xpGain = (gameplayCustomManager.globalXPPercent / 100.f) * xpGain; @@ -12987,46 +17907,76 @@ void Entity::awardXP(Entity* src, bool share, bool root) { int numshares = 0; Entity* shares[MAXPLAYERS]; - int c; - for ( c = 0; c < MAXPLAYERS; ++c ) + for ( int c = 0; c < MAXPLAYERS; ++c ) { shares[c] = nullptr; } // find other players to divide shares with - node_t* node; - for ( node = map.creatures->first; node != nullptr; node = node->next ) //Since only looking at players, this should just iterate over players[] + //node_t* node; + //for ( node = map.creatures->first; node != nullptr; node = node->next ) //Since only looking at players, this should just iterate over players[] + //{ + // Entity* entity = (Entity*)node->element; + // if ( entity == this ) + // { + // continue; + // } + // if ( entity && entity->behavior == &actPlayer ) + // { + // if ( entityDist(this, entity) < shareRange ) + // { + // ++numshares; + // shares[numshares] = entity; + // if ( numshares == MAXPLAYERS - 1 ) + // { + // break; + // } + // } + // } + //} + for ( int i = 0; i < MAXPLAYERS; ++i ) { - Entity* entity = (Entity*)node->element; - if ( entity == this ) - { - continue; - } - if ( entity && entity->behavior == &actPlayer ) + if ( !client_disconnected[i] && i != player ) { - if ( entityDist(this, entity) < shareRange ) + ++numshares; + if ( players[i]->entity ) { - ++numshares; - shares[numshares] = entity; - if ( numshares == MAXPLAYERS - 1 ) - { - break; - } + shares[numshares] = players[i]->entity; } } } // divide value of each share + real_t numSharesMult = 1.0; + if ( numshares == 1 ) + { + numSharesMult = 0.9; + } + else if ( numshares == 2 ) + { + numSharesMult = 0.8; + } + else if ( numshares >= 3 ) + { + numSharesMult = 0.7; + } if ( numshares ) { - xpGain /= numshares; + if ( (xpGain * numSharesMult) < 3 ) + { + xpGain = 3; + } + else + { + xpGain *= numSharesMult; + } } // award XP to everyone else in the group if ( share ) { - for ( c = 0; c < MAXPLAYERS; c++ ) + for ( int c = 0; c < MAXPLAYERS; c++ ) { if ( shares[c] ) { @@ -13039,8 +17989,7 @@ void Entity::awardXP(Entity* src, bool share, bool root) if ( stats[this->skill[2]] ) { // award XP to player's followers. - int numFollowers = list_Size(&stats[this->skill[2]]->FOLLOWERS); - for ( node = stats[this->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + for ( node_t* node = stats[this->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) { Entity* follower = nullptr; if ( (Uint32*)node->element ) @@ -13049,10 +17998,16 @@ void Entity::awardXP(Entity* src, bool share, bool root) } if ( follower && entityDist(this, follower) < shareRange && follower != src ) { - if ( follower->monsterIsTinkeringCreation() ) + if ( follower->monsterIsTinkeringCreation() + || (follower->getStats() && follower->getStats()->type == REVENANT_SKULL) + || (follower->getStats() && follower->getStats()->type == MONSTER_ADORCISED_WEAPON) + || (follower->getStats() && follower->getStats()->type == SKELETON && follower->getStats()->getAttribute("revenant_skeleton") != "") + || (follower->getStats() && follower->getStats()->type == MOTH_SMALL && follower->getStats()->getAttribute("fire_sprite") != "") + || (follower->getStats() && follower->getStats()->type == EARTH_ELEMENTAL && follower->monsterAllyGetPlayerLeader()) + || (follower->getStats() && follower->getStats()->type == FLAME_ELEMENTAL) + || (follower->getStats() && follower->getStats()->type == DUCK_SMALL) ) { - --numFollowers; // tinkering creation don't penalise XP. - continue; + continue; // no award xp } Stat* followerStats = follower->getStats(); if ( followerStats ) @@ -13063,12 +18018,12 @@ void Entity::awardXP(Entity* src, bool share, bool root) //int xpDivide = std::min(std::max(1, numFollowers), 4); // 1 - 4 depending on followers. if ( follower->monsterAllySummonRank != 0 && numshares > 0 ) { - int gain = (xpGain * numshares); // summoned monsters aren't penalised XP. + int gain = (xpGain / numSharesMult); // summoned monsters aren't penalised XP. if ( inspiration ) { int oldGain = gain; gain *= inspirationMult; - if ( ((followerStats->EXP + gain) >= 100) && ((followerStats->EXP + (xpGain * numshares)) < 100) ) + if ( ((followerStats->EXP + gain) >= 100) && ((followerStats->EXP + (oldGain)) < 100) ) { // inspiration caused us to level steamAchievementEntity(this, "BARONY_ACH_BY_EXAMPLE"); @@ -13088,7 +18043,7 @@ void Entity::awardXP(Entity* src, bool share, bool root) { int oldGain = gain; gain *= inspirationMult; - if ( ((followerStats->EXP + gain) >= 100) && ((followerStats->EXP + xpGain) < 100) ) + if ( ((followerStats->EXP + gain) >= 100) && ((followerStats->EXP + oldGain) < 100) ) { // inspiration caused us to level steamAchievementEntity(this, "BARONY_ACH_BY_EXAMPLE"); @@ -13112,7 +18067,17 @@ void Entity::awardXP(Entity* src, bool share, bool root) } // award XP to main victor - if ( !this->monsterIsTinkeringCreation() ) + if ( !(this->monsterIsTinkeringCreation() + || destStats->type == REVENANT_SKULL + || destStats->type == MONSTER_ADORCISED_WEAPON + || (destStats->type == MOTH_SMALL && destStats->getAttribute("fire_sprite") != "" ) + || (destStats->type == SKELETON && destStats->getAttribute("revenant_skeleton") != "") + || destStats->type == FLAME_ELEMENTAL + || destStats->type == DUCK_SMALL + || (destStats->type == EARTH_ELEMENTAL && monsterAllyGetPlayerLeader()) + || destStats->type == HOLOGRAM + ) + ) { int inspiration = getEntityInspirationFromAllies(); real_t inspirationMult = (1.0 + (inspiration / 100.0)); @@ -13152,6 +18117,40 @@ void Entity::awardXP(Entity* src, bool share, bool root) Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_XP_KILLS, "xp", gain); Compendium_t::Events_t::eventUpdateMonster(skill[2], Compendium_t::CPDM_KILL_XP, src, gain); + + if ( gain > 0 && ((destStats->playerRace == RACE_SALAMANDER && destStats->stat_appearance == 0) || destStats->type == SALAMANDER) ) + { + Sint32 oldMP = destStats->MP; + int minRoll = std::max(1, destStats->MAXMP / 50 + statGetCHR(destStats, this) / 10); + bool bonus = false; + if ( srcStats->getEffectActive(EFF_DIVINE_FIRE) ) + { + int effectInflictedBy = (srcStats->getEffectActive(EFF_DIVINE_FIRE) & 0xF0) >> 4; + if ( behavior == &actPlayer && !checkFriend(src) ) + { + if ( effectInflictedBy & (1 + skill[2]) ) + { + minRoll += srcStats->getEffectActive(EFF_DIVINE_FIRE) & 0xF; + bonus = true; + } + } + } + + int mpAmount = this->modMP(minRoll); + this->playerInsectoidIncrementHungerToMP(mpAmount); + if ( oldMP < destStats->MP ) + { + if ( behavior == &actPlayer ) + { + if ( bonus ) + { + messagePlayerColor(skill[2], MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6916)); + } + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_RUN, "rgn", destStats->MP - oldMP); + Compendium_t::Events_t::eventUpdateCodex(skill[2], Compendium_t::CPDM_RGN_MP_SUM, "rgn", destStats->MP - oldMP); + } + } + } } destStats->EXP += gain; } @@ -13175,6 +18174,36 @@ void Entity::awardXP(Entity* src, bool share, bool root) if ( root ) // global stats { + if ( srcStats ) + { + if ( srcStats->getEffectActive(EFF_SHADOW_TAGGED) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && players[i]->entity ) + { + if ( players[i]->entity->creatureShadowTaggedThisUid == src->getUID() ) + { + magicOnSpellCastEvent(players[i]->entity, players[i]->entity, src, + SPELL_SHADOW_TAG, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + } + if ( srcStats->getEffectActive(EFF_DETECT_ENEMY) ) + { + int caster = srcStats->getEffectActive(EFF_DETECT_ENEMY) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster] && players[caster]->entity ) + { + magicOnSpellCastEvent(players[caster]->entity, players[caster]->entity, src, + SPELL_DETECT_ENEMY, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + } + if ( src->behavior == &actPlayer && this->behavior == &actMonster ) { achievementObserver.updateGlobalStat(getIndexForDeathType(destStats->type), src->skill[2]); @@ -13182,6 +18211,13 @@ void Entity::awardXP(Entity* src, bool share, bool root) { steamAchievementClient(src->skill[2], "BARONY_ACH_ETERNAL_REWARD"); } + if ( destStats->type == SHADOW ) + { + if ( destStats->getAttribute("deface_spawn") == std::to_string(src->skill[2]) ) + { + steamAchievementClient(src->skill[2], "BARONY_ACH_HAUNTED"); + } + } } else if ( src->behavior == &actMonster && this->behavior == &actPlayer ) { @@ -13257,18 +18293,27 @@ void Entity::awardXP(Entity* src, bool share, bool root) { steamAchievementClient(player, "BARONY_ACH_BUT_A_SCRATCH"); } - if ( srcStats->EFFECTS[EFF_PARALYZED] ) + if ( srcStats->getEffectActive(EFF_PARALYZED) ) { serverUpdatePlayerGameplayStats(player, STATISTICS_SITTING_DUCK, 1); } if ( root ) { + if ( stats[player]->getEffectActive(EFF_FOCI_LIGHT_JUSTICE) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_PEACE) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_PROVIDENCE) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_PURITY) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_SANCTUARY) ) + { + steamStatisticUpdateClient(player, STEAM_STAT_BLESSED_ADDITION, STEAM_STAT_INT, 1); + } + achievementObserver.awardAchievementIfActive(player, src, AchievementObserver::BARONY_ACH_TELEFRAG); if ( stats[player]->playerRace == RACE_INCUBUS && stats[player]->stat_appearance == 0 ) { achievementObserver.playerAchievements[player].checkTraditionKill(this, src); } - if ( stats[player]->type == SPIDER && srcStats->EFFECTS[EFF_WEBBED] ) + if ( stats[player]->type == SPIDER && srcStats->getEffectActive(EFF_WEBBED) ) { steamStatisticUpdateClient(player, STEAM_STAT_MANY_PEDI_PALP, STEAM_STAT_INT, 1); } @@ -13430,42 +18475,141 @@ void Entity::awardXP(Entity* src, bool share, bool root) sendPacketSafe(net_sock, -1, net_packet, i - 1); } } - else + else + { + messagePlayerColor(i, MESSAGE_COMBAT | MESSAGE_HINT, makeColorRGB(255, 0, 0), Language::get(6102)); + } + } + } + } + } + } + else + { + Entity* leader = nullptr; + + // NPCs with leaders award equal XP to their master (so NPCs don't steal XP gainz) + leader = uidToEntity(destStats->leader_uid); + + bool spellEffectLeader = false; + int spellEffectLeaderID = SPELL_NONE; + if ( !leader || (leader && !leader->monsterAllyGetPlayerLeader()) ) + { + // target is taboo'd, then award xp to the inflicter + if ( srcStats->getEffectActive(EFF_TABOO) >= 1 && srcStats->getEffectActive(EFF_TABOO) < MAXPLAYERS + 1 ) + { + if ( players[srcStats->getEffectActive(EFF_TABOO) - 1]->entity ) + { + spellEffectLeader = true; + leader = players[srcStats->getEffectActive(EFF_TABOO) - 1]->entity; + } + } + } + if ( destStats->getEffectActive(EFF_CONFUSED) >= 1 && destStats->getEffectActive(EFF_CONFUSED) < MAXPLAYERS + 1 ) + { + if ( players[destStats->getEffectActive(EFF_CONFUSED) - 1]->entity ) + { + if ( destStats->type == AUTOMATON + && !strncmp(destStats->name, "corrupted automaton", strlen("corrupted automaton")) ) + { + // ignore + } + else + { + spellEffectLeader = true; + leader = players[destStats->getEffectActive(EFF_CONFUSED) - 1]->entity; + } + } + } + else if ( destStats->getEffectActive(EFF_COMMAND) >= 1 && destStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + if ( players[destStats->getEffectActive(EFF_COMMAND) - 1]->entity ) + { + spellEffectLeader = true; + leader = players[destStats->getEffectActive(EFF_COMMAND) - 1]->entity; + spellEffectLeaderID = SPELL_COMMAND; + } + } + + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i]->entity ) + { + { + auto find1 = players[i]->mechanics.targetsCompelled.find(src->getUID()); // has the player compelled the deceased + if ( find1 != players[i]->mechanics.targetsCompelled.end() ) + { + auto find2 = find1->second.find(this->getUID()); // has the player compelled the deceased to attack me + if ( find2 != find1->second.end() ) + { + if ( (::ticks - find2->second) < 30 * TICKS_PER_SECOND ) // less than x seconds ago + { + if ( src->behavior != &actPlayer && this->behavior != &actPlayer + && !src->monsterAllyGetPlayerLeader() && !this->monsterAllyGetPlayerLeader() ) + { + players[i]->mechanics.updateSustainedSpellEvent(SPELL_COMMAND, 150.0, 1.0, nullptr); + if ( !spellEffectLeader ) + { + spellEffectLeader = true; + leader = players[i]->entity; + spellEffectLeaderID = SPELL_COMMAND; + } + } + } + } + } + } + { + auto find1 = players[i]->mechanics.targetsCompelled.find(this->getUID()); // has the player compelled me + if ( find1 != players[i]->mechanics.targetsCompelled.end() ) + { + auto find2 = find1->second.find(src->getUID()); // has the player compelled me to attack the deceased + if ( find2 != find1->second.end() ) { - messagePlayerColor(i, MESSAGE_COMBAT | MESSAGE_HINT, makeColorRGB(255, 0, 0), Language::get(6102)); + if ( (::ticks - find2->second) < 30 * TICKS_PER_SECOND ) // less than x seconds ago + { + if ( src->behavior != &actPlayer && this->behavior != &actPlayer + && !src->monsterAllyGetPlayerLeader() && !this->monsterAllyGetPlayerLeader() ) + { + players[i]->mechanics.updateSustainedSpellEvent(SPELL_COMMAND, 150.0, 1.0, nullptr); + if ( !spellEffectLeader ) + { + spellEffectLeader = true; + leader = players[i]->entity; + spellEffectLeaderID = SPELL_COMMAND; + } + } + } } } } } } - } - else - { - Entity* leader = nullptr; - - // NPCs with leaders award equal XP to their master (so NPCs don't steal XP gainz) - if ( (leader = uidToEntity(destStats->leader_uid)) != NULL ) + if ( leader ) { if ( this->monsterIsTinkeringCreation() ) { - if ( local_rng.rand() % 10 == 0 ) - { - leader->increaseSkill(PRO_LOCKPICKING); - } - if ( root && leader->behavior == &actPlayer && srcStats->type == MINOTAUR ) + if ( !spellEffectLeader ) { - steamAchievementClient(leader->skill[2], "BARONY_ACH_TIME_TO_PLAN"); - } + if ( local_rng.rand() % 10 == 0 ) + { + leader->increaseSkill(PRO_LOCKPICKING); + } + if ( root && leader->behavior == &actPlayer && srcStats->type == MINOTAUR ) + { + steamAchievementClient(leader->skill[2], "BARONY_ACH_TIME_TO_PLAN"); + } - if ( root ) - { - if ( destStats && (destStats->type == SENTRYBOT || destStats->type == SPELLBOT) ) + if ( root ) { - if ( leader->behavior == &actPlayer ) + if ( destStats && (destStats->type == SENTRYBOT || destStats->type == SPELLBOT) ) { - Compendium_t::Events_t::eventUpdate(leader->skill[2], - Compendium_t::CPDM_SENTRY_DEPLOY_KILLS, destStats->type == SENTRYBOT ? TOOL_SENTRYBOT : TOOL_SPELLBOT, 1); + if ( leader->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdate(leader->skill[2], + Compendium_t::CPDM_SENTRY_DEPLOY_KILLS, destStats->type == SENTRYBOT ? TOOL_SENTRYBOT : TOOL_SPELLBOT, 1); + } } } } @@ -13474,7 +18618,22 @@ void Entity::awardXP(Entity* src, bool share, bool root) { if ( srcStats->type != BAT_SMALL ) { - leader->increaseSkill(PRO_LEADERSHIP); + if ( !spellEffectLeader ) + { + int spellID = getSpellFromSummonedEntityForSpellEvent(this); + if ( spellID != SPELL_NONE ) + { + magicOnSpellCastEvent(leader, this, src, spellID, spell_t::SPELL_LEVEL_EVENT_SUMMON, 1); + } + else + { + if ( destStats->monsterIsCharmed == 1 ) + { + magicOnSpellCastEvent(leader, this, src, SPELL_CHARM_MONSTER, spell_t::SPELL_LEVEL_EVENT_SUMMON, 1); + } + leader->increaseSkill(PRO_LEADERSHIP); + } + } } } leader->awardXP(src, true, false); @@ -13518,7 +18677,10 @@ void Entity::awardXP(Entity* src, bool share, bool root) } } - Compendium_t::Events_t::eventUpdateWorld(leader->skill[2], Compendium_t::CPDM_FOLLOWER_KILLS, "masons guild", 1); + if ( !spellEffectLeader ) + { + Compendium_t::Events_t::eventUpdateWorld(leader->skill[2], Compendium_t::CPDM_FOLLOWER_KILLS, "masons guild", 1); + } } } @@ -13619,7 +18781,39 @@ bool Entity::checkEnemy(Entity* your) return false; } - if ( behavior == &actPlayer && your->behavior == &actMonster && yourStats->monsterForceAllegiance != Stat::MONSTER_FORCE_ALLEGIANCE_NONE ) + if ( yourStats->getEffectActive(EFF_PENANCE) >= 1 && yourStats->getEffectActive(EFF_PENANCE) < 1 + MAXPLAYERS + && behavior == &actPlayer && your->behavior == &actMonster ) + { + return false; + } + else if ( myStats->getEffectActive(EFF_PENANCE) >= 1 && myStats->getEffectActive(EFF_PENANCE) < 1 + MAXPLAYERS + && your->behavior == &actPlayer && behavior == &actMonster ) + { + return false; + } + else if ( yourStats->getEffectActive(EFF_COMMAND) >= 1 && yourStats->getEffectActive(EFF_COMMAND) < 1 + MAXPLAYERS + && behavior == &actPlayer && your->behavior == &actMonster ) + { + return false; + } + else if ( myStats->getEffectActive(EFF_COMMAND) >= 1 && myStats->getEffectActive(EFF_COMMAND) < 1 + MAXPLAYERS + && your->behavior == &actPlayer && behavior == &actMonster ) + { + return false; + } + else if ( (yourStats->getEffectActive(EFF_TABOO) >= 1 && yourStats->getEffectActive(EFF_TABOO) < 1 + MAXPLAYERS + && behavior != &actPlayer && !monsterAllyGetPlayerLeader()) + || yourStats->getEffectActive(EFF_TABOO) >= 1 + MAXPLAYERS ) + { + return true; + } + else if ( (myStats->getEffectActive(EFF_TABOO) >= 1 && myStats->getEffectActive(EFF_TABOO) < 1 + MAXPLAYERS + && your->behavior != &actPlayer && !your->monsterAllyGetPlayerLeader()) + || myStats->getEffectActive(EFF_TABOO) >= 1 + MAXPLAYERS ) + { + return true; + } + else if ( behavior == &actPlayer && your->behavior == &actMonster && yourStats->monsterForceAllegiance != Stat::MONSTER_FORCE_ALLEGIANCE_NONE ) { if ( yourStats->monsterForceAllegiance == Stat::MONSTER_FORCE_PLAYER_ALLY || yourStats->monsterForceAllegiance == Stat::MONSTER_FORCE_PLAYER_RECRUITABLE ) { @@ -13653,6 +18847,12 @@ bool Entity::checkEnemy(Entity* your) return false; } + if ( (your->behavior == &actPlayer && monsterCanTradeWith(your->isEntityPlayer())) + || (behavior == &actPlayer && your->monsterCanTradeWith(isEntityPlayer())) ) + { + return false; + } + if ( myStats->type == HUMAN && (yourStats->type == AUTOMATON && !strncmp(yourStats->name, "corrupted automaton", 19)) ) { return true; @@ -13679,11 +18879,19 @@ bool Entity::checkEnemy(Entity* your) } return true; } - else if ( your->behavior == &actPlayer && myStats->type == VAMPIRE && MonsterData_t::nameMatchesSpecialNPCName(*myStats, "bram kindly") ) + else if ( your->behavior == &actPlayer && myStats->type == VAMPIRE && (myStats->getAttribute("special_npc") == "bram kindly") ) + { + return true; + } + else if ( behavior == &actPlayer && yourStats->type == VAMPIRE && (yourStats->getAttribute("special_npc") == "bram kindly") ) + { + return true; + } + else if ( your->behavior == &actPlayer && myStats->type == INCUBUS && (myStats->getAttribute("special_npc") == "johann") ) { return true; } - else if ( behavior == &actPlayer && yourStats->type == VAMPIRE && MonsterData_t::nameMatchesSpecialNPCName(*yourStats, "bram kindly") ) + else if ( behavior == &actPlayer && yourStats->type == INCUBUS && (yourStats->getAttribute("special_npc") == "johann") ) { return true; } @@ -13823,7 +19031,9 @@ bool Entity::checkEnemy(Entity* your) else if ( behavior == &actPlayer && myStats->type != HUMAN ) { result = swornenemies[HUMAN][yourStats->type]; - if ( (yourStats->type == HUMAN || yourStats->type == SHOPKEEPER) && myStats->type != AUTOMATON ) + if ( (yourStats->type == HUMAN || yourStats->type == SHOPKEEPER) + && !(myStats->type == AUTOMATON || myStats->type == DRYAD || myStats->type == MYCONID || myStats->type == SALAMANDER + || myStats->type == GNOME) ) { // enemies. result = true; @@ -13856,6 +19066,10 @@ bool Entity::checkEnemy(Entity* your) { result = false; } + if ( yourStats->type == GNOME ) + { + result = false; + } break; case CREATURE_IMP: if ( yourStats->type == CREATURE_IMP ) @@ -13868,6 +19082,10 @@ bool Entity::checkEnemy(Entity* your) { result = false; } + if ( yourStats->type == GREMLIN ) + { + result = false; + } break; case GOATMAN: if ( yourStats->type == GOATMAN ) @@ -13900,6 +19118,52 @@ bool Entity::checkEnemy(Entity* your) result = false; } break; + case MYCONID: + if ( yourStats->type == MYCONID ) + { + result = false; + } + if ( yourStats->type == AUTOMATON ) + { + result = true; + } + break; + case DRYAD: + if ( yourStats->type == DRYAD ) + { + result = false; + } + if ( yourStats->type == AUTOMATON ) + { + result = true; + } + break; + case SALAMANDER: + if ( yourStats->type == SALAMANDER ) + { + result = false; + } + break; + case GREMLIN: + if ( yourStats->type == GREMLIN ) + { + result = false; + } + if ( yourStats->type == GOBLIN ) + { + result = false; + } + if ( yourStats->type == AUTOMATON ) + { + result = true; + } + break; + case GNOME: + if ( yourStats->type == TROLL ) + { + result = false; + } + break; default: break; } @@ -13908,7 +19172,9 @@ bool Entity::checkEnemy(Entity* your) else if ( behavior == &actMonster && your->behavior == &actPlayer && yourStats->type != HUMAN ) { result = swornenemies[myStats->type][HUMAN]; - if ( (myStats->type == HUMAN || myStats->type == SHOPKEEPER) && yourStats->type != AUTOMATON ) + if ( (myStats->type == HUMAN || myStats->type == SHOPKEEPER) && + !(yourStats->type == AUTOMATON || yourStats->type == DRYAD || yourStats->type == MYCONID || yourStats->type == SALAMANDER + || yourStats->type == GNOME) ) { // enemies. result = true; @@ -13941,6 +19207,10 @@ bool Entity::checkEnemy(Entity* your) { result = false; } + if ( myStats->type == GNOME ) + { + result = false; + } break; case CREATURE_IMP: if ( myStats->type == CREATURE_IMP ) @@ -13953,6 +19223,10 @@ bool Entity::checkEnemy(Entity* your) { result = false; } + if ( myStats->type == GREMLIN ) + { + result = false; + } break; case GOATMAN: if ( myStats->type == GOATMAN ) @@ -13986,6 +19260,52 @@ bool Entity::checkEnemy(Entity* your) result = false; } break; + case MYCONID: + if ( myStats->type == MYCONID ) + { + result = false; + } + if ( myStats->type == AUTOMATON ) + { + result = true; + } + break; + case DRYAD: + if ( myStats->type == DRYAD ) + { + result = false; + } + if ( myStats->type == AUTOMATON ) + { + result = true; + } + break; + case SALAMANDER: + if ( myStats->type == SALAMANDER ) + { + result = false; + } + break; + case GREMLIN: + if ( myStats->type == GREMLIN ) + { + result = false; + } + if ( myStats->type == GOBLIN ) + { + result = false; + } + if ( myStats->type == AUTOMATON ) + { + result = true; + } + break; + case GNOME: + if ( myStats->type == TROLL ) + { + result = false; + } + break; default: break; } @@ -14023,7 +19343,7 @@ bool Entity::checkEnemy(Entity* your) } // confused monsters mistake their allegiances - if ( myStats->EFFECTS[EFF_CONFUSED] ) + if ( myStats->getEffectActive(EFF_CONFUSED) ) { if ( myStats->type == AUTOMATON && yourStats->type == AUTOMATON && !strncmp(myStats->name, "corrupted automaton", strlen("corrupted automaton")) ) @@ -14039,6 +19359,21 @@ bool Entity::checkEnemy(Entity* your) return result; } +bool Entity::friendlyFireProtection(Entity* your) +{ + if ( !your ) { return false; } + if ( behavior == &actPlayer && (your->behavior == &actPlayer + || your->monsterAllyGetPlayerLeader() + || (your->behavior == &actMonster + && your->getStats() + && achievementObserver.checkUidIsFromPlayer(your->getStats()->leader_uid) >= 0)) ) + { + return true; + } + + return false; +} + /*------------------------------------------------------------------------------- Entity::checkFriend @@ -14096,7 +19431,28 @@ bool Entity::checkFriend(Entity* your) return false; } } - + else if ( yourStats->getEffectActive(EFF_PENANCE) >= 1 && yourStats->getEffectActive(EFF_PENANCE) < 1 + MAXPLAYERS + && behavior == &actPlayer && your->behavior == &actMonster ) + { + return true; + } + else if ( myStats->getEffectActive(EFF_PENANCE) >= 1 && myStats->getEffectActive(EFF_PENANCE) < 1 + MAXPLAYERS + && your->behavior == &actPlayer && behavior == &actMonster ) + { + return true; + } + else if ( (yourStats->getEffectActive(EFF_TABOO) >= 1 && yourStats->getEffectActive(EFF_TABOO) < 1 + MAXPLAYERS + && behavior != &actPlayer && !monsterAllyGetPlayerLeader()) + || yourStats->getEffectActive(EFF_TABOO) >= 1 + MAXPLAYERS ) + { + return false; + } + else if ( (myStats->getEffectActive(EFF_TABOO) >= 1 && myStats->getEffectActive(EFF_TABOO) < 1 + MAXPLAYERS + && your->behavior != &actPlayer && !your->monsterAllyGetPlayerLeader()) + || myStats->getEffectActive(EFF_TABOO) >= 1 + MAXPLAYERS ) + { + return false; + } if ( (myStats->type == SHOPKEEPER && myStats->MISC_FLAGS[STAT_FLAG_MYSTERIOUS_SHOPKEEP] > 0) || (yourStats->type == SHOPKEEPER && yourStats->MISC_FLAGS[STAT_FLAG_MYSTERIOUS_SHOPKEEP] > 0) ) @@ -14104,6 +19460,12 @@ bool Entity::checkFriend(Entity* your) return false; } + if ( (your->behavior == &actPlayer && monsterCanTradeWith(your->isEntityPlayer())) + || (behavior == &actPlayer && your->monsterCanTradeWith(isEntityPlayer())) ) + { + return true; + } + if ( myStats->type == GYROBOT ) { return true; @@ -14135,11 +19497,19 @@ bool Entity::checkFriend(Entity* your) } return false; } - else if ( your->behavior == &actPlayer && myStats->type == VAMPIRE && MonsterData_t::nameMatchesSpecialNPCName(*myStats, "bram kindly") ) + else if ( your->behavior == &actPlayer && myStats->type == VAMPIRE && (myStats->getAttribute("special_npc") == "bram kindly") ) + { + return false; + } + else if ( behavior == &actPlayer && yourStats->type == VAMPIRE && (yourStats->getAttribute("special_npc") == "bram kindly") ) + { + return false; + } + else if ( your->behavior == &actPlayer && myStats->type == INCUBUS && (myStats->getAttribute("special_npc") == "johann") ) { return false; } - else if ( behavior == &actPlayer && yourStats->type == VAMPIRE && MonsterData_t::nameMatchesSpecialNPCName(*yourStats, "bram kindly") ) + else if ( behavior == &actPlayer && yourStats->type == INCUBUS && (yourStats->getAttribute("special_npc") == "johann") ) { return false; } @@ -14274,7 +19644,9 @@ bool Entity::checkFriend(Entity* your) else if ( behavior == &actPlayer && myStats->type != HUMAN ) { result = monsterally[HUMAN][yourStats->type]; - if ( (yourStats->type == HUMAN || yourStats->type == SHOPKEEPER) && myStats->type != AUTOMATON ) + if ( (yourStats->type == HUMAN || yourStats->type == SHOPKEEPER) + && !(myStats->type == AUTOMATON || myStats->type == DRYAD || myStats->type == MYCONID || myStats->type == SALAMANDER + || myStats->type == GNOME) ) { result = false; } @@ -14355,6 +19727,42 @@ bool Entity::checkFriend(Entity* your) result = true; } break; + case MYCONID: + if ( yourStats->type == MYCONID ) + { + result = true; + } + if ( yourStats->type == AUTOMATON ) + { + result = false; + } + break; + case DRYAD: + if ( yourStats->type == DRYAD ) + { + result = true; + } + if ( yourStats->type == AUTOMATON ) + { + result = false; + } + break; + case SALAMANDER: + if ( yourStats->type == SALAMANDER ) + { + result = true; + } + break; + case GREMLIN: + if ( yourStats->type == GREMLIN ) + { + result = true; + } + if ( yourStats->type == AUTOMATON ) + { + result = false; + } + break; default: break; } @@ -14363,7 +19771,9 @@ bool Entity::checkFriend(Entity* your) else if ( behavior == &actMonster && your->behavior == &actPlayer && yourStats->type != HUMAN ) { result = monsterally[myStats->type][HUMAN]; - if ( (myStats->type == HUMAN || myStats->type == SHOPKEEPER) && yourStats->type != AUTOMATON ) + if ( (myStats->type == HUMAN || myStats->type == SHOPKEEPER) + && !(yourStats->type == AUTOMATON || yourStats->type == DRYAD || yourStats->type == MYCONID || yourStats->type == SALAMANDER + || yourStats->type == GNOME) ) { result = false; } @@ -14444,6 +19854,42 @@ bool Entity::checkFriend(Entity* your) result = true; } break; + case MYCONID: + if ( myStats->type == MYCONID ) + { + result = true; + } + if ( myStats->type == AUTOMATON ) + { + result = false; + } + break; + case DRYAD: + if ( myStats->type == DRYAD ) + { + result = true; + } + if ( myStats->type == AUTOMATON ) + { + result = false; + } + break; + case SALAMANDER: + if ( myStats->type == SALAMANDER ) + { + result = true; + } + break; + case GREMLIN: + if ( myStats->type == GREMLIN ) + { + result = true; + } + if ( myStats->type == AUTOMATON ) + { + result = false; + } + break; default: break; } @@ -14773,6 +20219,12 @@ int checkEquipType(const Item *item) case CRYSTAL_BOOTS: case ARTIFACT_BOOTS: case SUEDE_BOOTS: + case BONE_BOOTS: + case BLACKIRON_BOOTS: + case SILVER_BOOTS: + case QUILTED_BOOTS: + case LOAFERS: + case CHAIN_BOOTS: return TYPE_BOOTS; break; @@ -14784,6 +20236,12 @@ int checkEquipType(const Item *item) case HAT_BEAR_HOOD: case HAT_STAG_HOOD: case HAT_WOLF_HOOD: + case BONE_HELM: + case BLACKIRON_HELM: + case SILVER_HELM: + case QUILTED_CAP: + case CHAIN_COIF: + case HAT_CROWNED_HELM: return TYPE_HELM; break; @@ -14795,6 +20253,19 @@ int checkEquipType(const Item *item) case HEALER_DOUBLET: case VAMPIRE_DOUBLET: case ARTIFACT_BREASTPIECE: + case BANDIT_BREASTPIECE: + case TUNIC_BLOUSE: + case BONE_BREASTPIECE: + case BLACKIRON_BREASTPIECE: + case SILVER_BREASTPIECE: + case IRON_PAULDRONS: + case QUILTED_GAMBESON: + case ROBE_CULTIST: + case ROBE_HEALER: + case ROBE_MONK: + case ROBE_WIZARD: + case SHAWL: + case CHAIN_HAUBERK: return TYPE_BREASTPIECE; break; @@ -14805,6 +20276,10 @@ int checkEquipType(const Item *item) case STEEL_SHIELD: case STEEL_SHIELD_RESISTANCE: case MIRROR_SHIELD: + case SCUTUM: + case BONE_SHIELD: + case BLACKIRON_SHIELD: + case SILVER_SHIELD: return TYPE_SHIELD; break; @@ -14822,6 +20297,7 @@ int checkEquipType(const Item *item) case CLOAK_BLACK: case CLOAK_BACKPACK: case CLOAK_SILVER: + case CLOAK_DENDRITE: return TYPE_CLOAK; break; @@ -14837,6 +20313,11 @@ int checkEquipType(const Item *item) case IRON_KNUCKLES: case BRASS_KNUCKLES: case SUEDE_GLOVES: + case BONE_BRACERS: + case BLACKIRON_GAUNTLETS: + case SILVER_GAUNTLETS: + case QUILTED_GLOVES: + case CHAIN_GLOVES: return TYPE_GLOVES; break; @@ -14855,6 +20336,8 @@ int checkEquipType(const Item *item) case HAT_TOPHAT: case HAT_BANDANA: case HAT_CIRCLET: + case HAT_CIRCLET_SORCERY: + case HAT_CIRCLET_THAUMATURGY: case HAT_CROWN: case HAT_LAURELS: case HAT_TURBAN: @@ -14869,6 +20352,8 @@ int checkEquipType(const Item *item) case HAT_HOOD_APPRENTICE: case HAT_HOOD_ASSASSIN: case HAT_HOOD_WHISPERS: + case HAT_FELT: + case HOOD_TEAL: return TYPE_HAT; break; @@ -14923,6 +20408,26 @@ int setGloveSprite(Stat* myStats, Entity* ent, int spriteOffset) { ent->sprite = 804 + (spriteOffset > 0 ? 1 : 0); } + else if ( myStats->gloves->type == BONE_BRACERS ) + { + ent->sprite = 2087 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->gloves->type == BLACKIRON_GAUNTLETS ) + { + ent->sprite = 2091 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->gloves->type == SILVER_GAUNTLETS ) + { + ent->sprite = 2095 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->gloves->type == QUILTED_GLOVES ) + { + ent->sprite = 2099 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->gloves->type == CHAIN_GLOVES ) + { + ent->sprite = 2103 + (spriteOffset > 0 ? 1 : 0); + } else { return 0; @@ -14930,7 +20435,46 @@ int setGloveSprite(Stat* myStats, Entity* ent, int spriteOffset) return 1; } -bool Entity::setBootSprite(Entity* leg, int spriteOffset) +bool Entity::isBootSpriteShortArmor(Entity* leg) +{ + if ( !leg ) { return false; } + + switch ( leg->sprite ) + { + case 1463: // LEATHER_BOOTS + case 1464: + case 1467: // IRON_BOOTS + case 1468: + case 1471: // STEEL_BOOTS + case 1472: + case 1465: // CRYSTAL_BOOTS + case 1466: + case 1461: // ARTIFACT_BOOTS + case 1462: + case 1473: // SUEDE_BOOTS + case 1474: + case 2079: // BONE_BOOTS + case 2080: + case 2077: // BLACKIRON_BOOTS + case 2078: + case 2081: // SILVER_BOOTS + case 2082: + case 2085: // QUILTED_BOOTS + case 2086: + case 1469: // LOAFERS + case 1470: + case 2083: // CHAIN_BOOTS + case 2084: + case 2075: // CLEAT_BOOTS + case 2076: + return true; + default: + break; + } + return false; +} + +bool Entity::setBootSprite(Entity* leg, int spriteOffset, bool forceShort) { if ( multiplayer == CLIENT ) { @@ -14957,36 +20501,93 @@ bool Entity::setBootSprite(Entity* leg, int spriteOffset) return false; } - switch ( myStats->type ) + Monster monsterType = myStats->type; + if ( behavior == &actPlayer ) + { + if ( effectShapeshift != NOTHING ) + { + monsterType = static_cast(effectShapeshift); + } + else if ( myStats->playerRace > 0 || myStats->getEffectActive(EFF_POLYMORPH) || effectPolymorph != NOTHING ) + { + monsterType = getMonsterFromPlayerRace(myStats->playerRace); + if ( effectPolymorph != NOTHING ) + { + if ( effectPolymorph > NUMMONSTERS ) + { + monsterType = HUMAN; + } + else + { + monsterType = static_cast(effectPolymorph); + } + } + } + } + + bool shortSprite = forceShort; + switch ( monsterType ) { case HUMAN: - if ( myStats->shoes->type == LEATHER_BOOTS || myStats->shoes->type == LEATHER_BOOTS_SPEED ) - { - leg->sprite = 148 + myStats->sex + spriteOffset; - } - else if ( myStats->shoes->type == IRON_BOOTS || myStats->shoes->type == IRON_BOOTS_WATERWALKING ) - { - leg->sprite = 152 + myStats->sex + spriteOffset; - } - else if ( myStats->shoes->type >= STEEL_BOOTS && myStats->shoes->type <= STEEL_BOOTS_FEATHER ) + if ( !shortSprite ) { - leg->sprite = 156 + myStats->sex + spriteOffset; - } - else if ( myStats->shoes->type == CRYSTAL_BOOTS ) - { - leg->sprite = 499 + myStats->sex + spriteOffset; - } - else if ( myStats->shoes->type == ARTIFACT_BOOTS ) - { - leg->sprite = 521 + myStats->sex + spriteOffset; - } - else if ( myStats->shoes->type == SUEDE_BOOTS ) - { - leg->sprite = 808 + (spriteOffset > 0 ? 1 : 0); - } - else - { - return false; + if ( myStats->shoes->type == LEATHER_BOOTS || myStats->shoes->type == LEATHER_BOOTS_SPEED ) + { + leg->sprite = 148 + myStats->sex + spriteOffset; + } + else if ( myStats->shoes->type == IRON_BOOTS || myStats->shoes->type == IRON_BOOTS_WATERWALKING ) + { + leg->sprite = 152 + myStats->sex + spriteOffset; + } + else if ( myStats->shoes->type >= STEEL_BOOTS && myStats->shoes->type <= STEEL_BOOTS_FEATHER ) + { + leg->sprite = 156 + myStats->sex + spriteOffset; + } + else if ( myStats->shoes->type == CRYSTAL_BOOTS ) + { + leg->sprite = 499 + myStats->sex + spriteOffset; + } + else if ( myStats->shoes->type == ARTIFACT_BOOTS ) + { + leg->sprite = 521 + myStats->sex + spriteOffset; + } + else if ( myStats->shoes->type == SUEDE_BOOTS ) + { + leg->sprite = 808 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == BONE_BOOTS ) + { + leg->sprite = 2067 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == BLACKIRON_BOOTS ) + { + leg->sprite = 2065 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == SILVER_BOOTS ) + { + leg->sprite = 2069 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == QUILTED_BOOTS ) + { + leg->sprite = 2073 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == LOAFERS ) + { + leg->sprite = 1439 + (spriteOffset > 0 ? -1 : 0); + } + else if ( myStats->shoes->type == CHAIN_BOOTS ) + { + leg->sprite = 2071 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == CLEAT_BOOTS ) + { + leg->sprite = 2063 + (spriteOffset > 0 ? 1 : 0); + } + else + { + return false; + } + return true; } break; // fall throughs below @@ -15001,69 +20602,145 @@ bool Entity::setBootSprite(Entity* leg, int spriteOffset) case VAMPIRE: case SUCCUBUS: case SHOPKEEPER: - if ( myStats->shoes->type == LEATHER_BOOTS || myStats->shoes->type == LEATHER_BOOTS_SPEED ) - { - leg->sprite = 148 + spriteOffset; - } - else if ( myStats->shoes->type == IRON_BOOTS || myStats->shoes->type == IRON_BOOTS_WATERWALKING ) - { - leg->sprite = 152 + spriteOffset; - } - else if ( myStats->shoes->type >= STEEL_BOOTS && myStats->shoes->type <= STEEL_BOOTS_FEATHER ) - { - leg->sprite = 156 + spriteOffset; - } - else if ( myStats->shoes->type == CRYSTAL_BOOTS ) - { - leg->sprite = 499 + spriteOffset; - } - else if ( myStats->shoes->type == ARTIFACT_BOOTS ) - { - leg->sprite = 521 + spriteOffset; - } - else if ( myStats->shoes->type == SUEDE_BOOTS ) + case DRYAD: + case MYCONID: + case SALAMANDER: + if ( monsterType == DRYAD && (sprite == 1514 || sprite == 1515 || sprite == 1992 || sprite == 1993) ) { - leg->sprite = 808 + (spriteOffset > 0 ? 1 : 0); + shortSprite = true; } - else + if ( monsterType == MYCONID && (sprite == 1520 || sprite == 1998) ) { - return false; + shortSprite = true; } break; case GNOME: - if ( myStats->shoes->type == LEATHER_BOOTS || myStats->shoes->type == LEATHER_BOOTS_SPEED ) - { - leg->sprite = 1463 + (spriteOffset > 0 ? 1 : 0); - } - else if ( myStats->shoes->type == IRON_BOOTS || myStats->shoes->type == IRON_BOOTS_WATERWALKING ) - { - leg->sprite = 1467 + (spriteOffset > 0 ? 1 : 0); - } - else if ( myStats->shoes->type >= STEEL_BOOTS && myStats->shoes->type <= STEEL_BOOTS_FEATHER ) - { - leg->sprite = 1471 + (spriteOffset > 0 ? 1 : 0); - } - else if ( myStats->shoes->type == CRYSTAL_BOOTS ) - { - leg->sprite = 1465 + (spriteOffset > 0 ? 1 : 0); - } - else if ( myStats->shoes->type == ARTIFACT_BOOTS ) - { - leg->sprite = 1461 + (spriteOffset > 0 ? 1 : 0); - } - else if ( myStats->shoes->type == SUEDE_BOOTS ) - { - leg->sprite = 1473 + (spriteOffset > 0 ? 1 : 0); - } - else - { - return false; - } + case GREMLIN: + shortSprite = true; break; default: break; } + if ( !shortSprite ) + { + if ( myStats->shoes->type == LEATHER_BOOTS || myStats->shoes->type == LEATHER_BOOTS_SPEED ) + { + leg->sprite = 148 + spriteOffset; + } + else if ( myStats->shoes->type == IRON_BOOTS || myStats->shoes->type == IRON_BOOTS_WATERWALKING ) + { + leg->sprite = 152 + spriteOffset; + } + else if ( myStats->shoes->type >= STEEL_BOOTS && myStats->shoes->type <= STEEL_BOOTS_FEATHER ) + { + leg->sprite = 156 + spriteOffset; + } + else if ( myStats->shoes->type == CRYSTAL_BOOTS ) + { + leg->sprite = 499 + spriteOffset; + } + else if ( myStats->shoes->type == ARTIFACT_BOOTS ) + { + leg->sprite = 521 + spriteOffset; + } + else if ( myStats->shoes->type == SUEDE_BOOTS ) + { + leg->sprite = 808 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == BONE_BOOTS ) + { + leg->sprite = 2067 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == BLACKIRON_BOOTS ) + { + leg->sprite = 2065 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == SILVER_BOOTS ) + { + leg->sprite = 2069 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == QUILTED_BOOTS ) + { + leg->sprite = 2073 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == LOAFERS ) + { + leg->sprite = 1439 + (spriteOffset > 0 ? -1 : 0); + } + else if ( myStats->shoes->type == CHAIN_BOOTS ) + { + leg->sprite = 2071 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == CLEAT_BOOTS ) + { + leg->sprite = 2063 + (spriteOffset > 0 ? 1 : 0); + } + else + { + return false; + } + } + else + { + if ( myStats->shoes->type == LEATHER_BOOTS || myStats->shoes->type == LEATHER_BOOTS_SPEED ) + { + leg->sprite = 1463 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == IRON_BOOTS || myStats->shoes->type == IRON_BOOTS_WATERWALKING ) + { + leg->sprite = 1467 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type >= STEEL_BOOTS && myStats->shoes->type <= STEEL_BOOTS_FEATHER ) + { + leg->sprite = 1471 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == CRYSTAL_BOOTS ) + { + leg->sprite = 1465 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == ARTIFACT_BOOTS ) + { + leg->sprite = 1461 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == SUEDE_BOOTS ) + { + leg->sprite = 1473 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == BONE_BOOTS ) + { + leg->sprite = 2079 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == BLACKIRON_BOOTS ) + { + leg->sprite = 2077 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == SILVER_BOOTS ) + { + leg->sprite = 2081 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == QUILTED_BOOTS ) + { + leg->sprite = 2085 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == LOAFERS ) + { + leg->sprite = 1469 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == CHAIN_BOOTS ) + { + leg->sprite = 2083 + (spriteOffset > 0 ? 1 : 0); + } + else if ( myStats->shoes->type == CLEAT_BOOTS ) + { + leg->sprite = 2075 + (spriteOffset > 0 ? 1 : 0); + } + else + { + return false; + } + } + return true; } @@ -15122,11 +20799,15 @@ bool isLevitating(Stat* mystats) } } - if ( mystats->EFFECTS[EFF_LEVITATING] == true ) + if ( mystats->getEffectActive(EFF_LEVITATING) ) + { + return true; + } + else if ( mystats->getEffectActive(EFF_FLUTTER) ) { return true; } - else if ( mystats->EFFECTS[EFF_FLUTTER] ) + else if ( mystats->type == SALAMANDER && mystats->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && mystats->getEffectActive(EFF_SALAMANDER_HEART) <= 2 ) { return true; } @@ -15163,26 +20844,51 @@ returns the proficiency for the weapon equipped. -------------------------------------------------------------------------------*/ -int getWeaponSkill(Item* weapon) +int getWeaponSkill(const Item* weapon) { if ( weapon == NULL ) { return PRO_UNARMED; } - if ( weapon->type == QUARTERSTAFF || weapon->type == IRON_SPEAR || weapon->type == STEEL_HALBERD || weapon->type == ARTIFACT_SPEAR || weapon->type == CRYSTAL_SPEAR ) + if ( weapon->type == QUARTERSTAFF || weapon->type == IRON_SPEAR + || weapon->type == STEEL_HALBERD || weapon->type == ARTIFACT_SPEAR + || weapon->type == CRYSTAL_SPEAR + || weapon->type == LANCE_SPEAR + || weapon->type == BONE_SPEAR + || weapon->type == BLACKIRON_TRIDENT + || weapon->type == SILVER_GLAIVE ) { return PRO_POLEARM; } - if ( weapon->type == BRONZE_SWORD || weapon->type == IRON_SWORD || weapon->type == STEEL_SWORD || weapon->type == ARTIFACT_SWORD || weapon->type == CRYSTAL_SWORD ) + if ( weapon->type == BRONZE_SWORD || weapon->type == IRON_SWORD || weapon->type == STEEL_SWORD + || weapon->type == ARTIFACT_SWORD || weapon->type == CRYSTAL_SWORD + || weapon->type == RAPIER + || weapon->type == STEEL_FALSHION + || weapon->type == BLACKIRON_SWORD + || weapon->type == SILVER_SWORD + || weapon->type == BONE_SWORD + || weapon->type == CLAYMORE_SWORD + || weapon->type == ANELACE_SWORD ) { return PRO_SWORD; } - if ( weapon->type == BRONZE_MACE || weapon->type == IRON_MACE || weapon->type == STEEL_MACE || weapon->type == ARTIFACT_MACE || weapon->type == CRYSTAL_MACE ) + if ( weapon->type == BRONZE_MACE || weapon->type == IRON_MACE || weapon->type == STEEL_MACE + || weapon->type == STEEL_FLAIL + || weapon->type == ARTIFACT_MACE || weapon->type == CRYSTAL_MACE + || weapon->type == SHILLELAGH_MACE + || weapon->type == BLACKIRON_MACE + || weapon->type == BONE_MACE + || weapon->type == SILVER_MACE ) { return PRO_MACE; } - if ( weapon->type == BRONZE_AXE || weapon->type == IRON_AXE || weapon->type == STEEL_AXE || weapon->type == ARTIFACT_AXE || weapon->type == CRYSTAL_BATTLEAXE ) + if ( weapon->type == BRONZE_AXE || weapon->type == IRON_AXE || weapon->type == STEEL_AXE + || weapon->type == ARTIFACT_AXE || weapon->type == CRYSTAL_BATTLEAXE + || weapon->type == STEEL_GREATAXE + || weapon->type == BLACKIRON_AXE + || weapon->type == SILVER_AXE + || weapon->type == BONE_AXE ) { return PRO_AXE; } @@ -15226,15 +20932,15 @@ int getStatForProficiency(int skill) case PRO_RANGED: // base attribute: dex statForProficiency = STAT_DEX; break; - case PRO_SWIMMING: // base attribute: con case PRO_SHIELD: // base attribute: con + case PRO_THAUMATURGY: // base attribute: con statForProficiency = STAT_CON; break; - case PRO_SPELLCASTING: // base attribute: int - case PRO_MAGIC: // base attribute: int - case PRO_ALCHEMY: // base attribute: int + case PRO_SORCERY: // base attribute: int + case PRO_MYSTICISM: // base attribute: int statForProficiency = STAT_INT; break; + case PRO_ALCHEMY: // base attribute: per case PRO_LOCKPICKING: // base attribute: per case PRO_APPRAISAL: // base attribute: per statForProficiency = STAT_PER; @@ -15314,14 +21020,15 @@ int Entity::getReflection() const return 0; } - if ( stats->EFFECTS[EFF_MAGICREFLECT] ) + if ( stats->getEffectActive(EFF_MAGICREFLECT) ) { return 3; } if ( stats->shield ) { - if ( stats->shield->type == MIRROR_SHIELD && stats->defending ) + if ( (stats->shield->type == MIRROR_SHIELD + || stats->getEffectActive(EFF_REFLECTOR_SHIELD) > 0) && stats->defending ) { return 3; } @@ -15439,7 +21146,15 @@ int Entity::getAttackPose() const { pose = MONSTER_POSE_MAGIC_WINDUP1; } - else if ( myStats->type == MIMIC ) + else if ( myStats->type == REVENANT_SKULL + || myStats->type == MONSTER_ADORCISED_WEAPON + || myStats->type == FLAME_ELEMENTAL + || myStats->type == EARTH_ELEMENTAL + || myStats->type == MOTH_SMALL ) + { + pose = MONSTER_POSE_MELEE_WINDUP1; + } + else if ( myStats->type == MIMIC || myStats->type == MINIMIMIC ) { pose = MONSTER_POSE_MELEE_WINDUP1; if ( monsterSpecialState == MIMIC_MAGIC ) @@ -15447,6 +21162,37 @@ int Entity::getAttackPose() const pose = MONSTER_POSE_MIMIC_MAGIC1; } } + else if ( myStats->type == DRYAD + && (this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_D + || this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_D_PUSH) ) + { + if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_D ) + { + pose = MONSTER_POSE_RANGED_WINDUP3; + } + else + { + pose = MONSTER_POSE_MAGIC_WINDUP1; + } + } + else if ( myStats->type == MYCONID + && (this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_SHORT + || this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_LONG) ) + { + if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_SHORT ) + { + pose = MONSTER_POSE_MAGIC_WINDUP1; + } + else if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_LONG ) + { + pose = MONSTER_POSE_SPECIAL_WINDUP1; + } + } + else if ( myStats->type == GREMLIN + && this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_G_CAST ) + { + pose = MONSTER_POSE_MAGIC_WINDUP1; + } else if ( itemCategory(myStats->weapon) == MAGICSTAFF ) { if ( myStats->type == KOBOLD || myStats->type == AUTOMATON @@ -15455,6 +21201,8 @@ int Entity::getAttackPose() const || myStats->type == HUMAN || myStats->type == GOBLIN || myStats->type == SKELETON || myStats->type == GNOME || myStats->type == SUCCUBUS || myStats->type == SHOPKEEPER + || myStats->type == DRYAD || myStats->type == MYCONID + || myStats->type == SALAMANDER || myStats->type == GREMLIN || myStats->type == SHADOW ) { pose = MONSTER_POSE_MELEE_WINDUP1; @@ -15532,6 +21280,13 @@ int Entity::getAttackPose() const } } + else if ( myStats->type == GREMLIN ) + { + if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_G_THROW ) + { + pose = MONSTER_POSE_RANGED_WINDUP3; + } + } else if ( myStats->type == INCUBUS ) { if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_INCUBUS_CONFUSION ) @@ -15544,7 +21299,7 @@ int Entity::getAttackPose() const pose = MONSTER_POSE_MELEE_WINDUP1; } } - else if ( this->hasRangedWeapon() ) + else if ( this->hasRangedWeapon(true) && myStats->weapon ) { if ( myStats->type == KOBOLD || myStats->type == AUTOMATON || myStats->type == GOATMAN || myStats->type == INSECTOID @@ -15552,10 +21307,14 @@ int Entity::getAttackPose() const || myStats->type == HUMAN || myStats->type == GOBLIN || myStats->type == SKELETON || myStats->type == GNOME || myStats->type == SUCCUBUS || myStats->type == SHOPKEEPER - || myStats->type == BUGBEAR + || myStats->type == BUGBEAR + || myStats->type == DRYAD || myStats->type == MYCONID + || myStats->type == SALAMANDER || myStats->type == GREMLIN + || myStats->type == SHADOW ) { - if ( myStats->weapon->type == CROSSBOW || myStats->weapon->type == HEAVY_CROSSBOW ) + if ( myStats->weapon->type == CROSSBOW || myStats->weapon->type == HEAVY_CROSSBOW + || myStats->weapon->type == BLACKIRON_CROSSBOW ) { pose = MONSTER_POSE_RANGED_WINDUP1; } @@ -15572,6 +21331,20 @@ int Entity::getAttackPose() const pose = MONSTER_POSE_MELEE_WINDUP1; } } + else if ( myStats->type == MYCONID ) + { + if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_THROW ) + { + pose = MONSTER_POSE_RANGED_WINDUP3; + } + } + else if ( myStats->type == GREMLIN ) + { + if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_G_THROW ) + { + pose = MONSTER_POSE_RANGED_WINDUP3; + } + } else { pose = MONSTER_POSE_MELEE_WINDUP1; @@ -15596,13 +21369,29 @@ int Entity::getAttackPose() const || myStats->type == SKELETON || myStats->type == GNOME || myStats->type == SUCCUBUS || myStats->type == SHOPKEEPER || myStats->type == BUGBEAR + || myStats->type == DRYAD || myStats->type == MYCONID + || myStats->type == SALAMANDER || myStats->type == GREMLIN || myStats->type == SHADOW ) { if ( getWeaponSkill(myStats->weapon) == PRO_AXE || getWeaponSkill(myStats->weapon) == PRO_MACE || myStats->weapon->type == TOOL_WHIP ) { - // axes and maces don't stab - pose = MONSTER_POSE_MELEE_WINDUP1 + local_rng.rand() % 2; + if ( myStats->weapon->type == STEEL_FLAIL ) + { + if ( local_rng.rand() % 5 == 0 ) + { + pose = MONSTER_POSE_FLAIL_SWING_WINDUP; + } + else + { + pose = MONSTER_POSE_MELEE_WINDUP1; + } + } + else + { + // axes and maces don't stab + pose = MONSTER_POSE_MELEE_WINDUP1 + local_rng.rand() % 2; + } } else { @@ -15623,6 +21412,40 @@ int Entity::getAttackPose() const { pose = MONSTER_POSE_MAGIC_WINDUP2; } + else if ( (myStats->type == REVENANT_SKULL + || myStats->type == MONSTER_ADORCISED_WEAPON + || myStats->type == FLAME_ELEMENTAL) && this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_SKULL_CAST ) + { + pose = MONSTER_POSE_MAGIC_WINDUP1; + } + else if ( myStats->type == DRYAD && (this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_D + || this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_D_PUSH) ) + { + if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_D ) + { + pose = MONSTER_POSE_RANGED_WINDUP3; + } + else + { + pose = MONSTER_POSE_MAGIC_WINDUP1; + } + } + else if ( myStats->type == MYCONID && (this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_SHORT + || this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_LONG) ) + { + if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_SHORT ) + { + pose = MONSTER_POSE_MAGIC_WINDUP1; + } + else if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_LONG ) + { + pose = MONSTER_POSE_SPECIAL_WINDUP1; + } + } + else if ( myStats->type == GREMLIN && this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MONSTER_G_CAST ) + { + pose = MONSTER_POSE_MAGIC_WINDUP1; + } else if (type == KOBOLD || type == AUTOMATON || type == GOATMAN || type == INSECTOID || type == INCUBUS || type == VAMPIRE || @@ -15632,11 +21455,16 @@ int Entity::getAttackPose() const type == CREATURE_IMP || type == SUCCUBUS || type == SHOPKEEPER || type == MINOTAUR || type == SHADOW || type == RAT || type == SPIDER || type == CRAB || - type == MIMIC || type == BAT_SMALL || + type == MIMIC || type == MINIMIMIC || + type == BAT_SMALL || + type == DRYAD || type == MYCONID || + type == SALAMANDER || type == GREMLIN || + type == REVENANT_SKULL || type == MONSTER_ADORCISED_WEAPON || + type == FLAME_ELEMENTAL || type == SLIME || (type == SCARAB && sprite != 1078 && sprite != 1079)) { pose = MONSTER_POSE_MELEE_WINDUP1; - if ( type == MIMIC && monsterSpecialState == MIMIC_MAGIC ) + if ( (type == MIMIC || type == MINIMIMIC) && monsterSpecialState == MIMIC_MAGIC ) { pose = MONSTER_POSE_MIMIC_MAGIC1; } @@ -15671,6 +21499,38 @@ int Entity::getAttackPose() const { pose = MONSTER_POSE_MELEE_WINDUP1; } + else if ( myStats->type == MOTH_SMALL ) + { + if ( this->monsterSpecialTimer == MONSTER_SPECIAL_COOLDOWN_MOTH_CAST ) + { + pose = mothGetAttackPose(const_cast(this), MONSTER_POSE_MAGIC_WINDUP1); + } + else + { + pose = mothGetAttackPose(const_cast(this), MONSTER_POSE_MELEE_WINDUP1); + } + } + else if ( myStats->type == EARTH_ELEMENTAL ) + { + switch ( local_rng.rand() % 4 ) + { + case 0: + pose = MONSTER_POSE_MELEE_WINDUP1; + break; + case 1: + pose = MONSTER_POSE_MELEE_WINDUP2; + break; + case 2: + pose = MONSTER_POSE_MELEE_WINDUP3; + break; + case 3: + pose = MONSTER_POSE_RANGED_WINDUP1; + break; + default: + pose = MONSTER_POSE_MELEE_WINDUP1; + break; + } + } else { pose = 1; @@ -15680,9 +21540,26 @@ int Entity::getAttackPose() const return pose; } -bool Entity::hasRangedWeapon() const +bool Entity::hasRangedWeapon(bool ignoreMonsterNPCType) const { Stat *myStats = getStats(); + /*if ( myStats && myStats->type == MOTH_SMALL && myStats->getAttribute("fire_sprite") != "" ) + { + if ( monsterSpecialTimer > 0 ) + { + return false; + } + return true; + }*/ + + if ( !ignoreMonsterNPCType ) + { + if ( myStats && myStats->type == DRYAD && myStats->getAttribute("monster_d_type") == "watcher" ) + { + return true; + } + } + if ( myStats == nullptr || myStats->weapon == nullptr ) { return false; @@ -16129,7 +22006,7 @@ void Entity::handleWeaponArmAttack(Entity* weaponarm) // swing arm to cast spell else if ( monsterAttack == MONSTER_POSE_MAGIC_WINDUP2 ) { - if ( monsterAttackTime == 0 ) + if ( monsterAttackTime <= 1 ) { // init rotations weaponarm->pitch = 0; @@ -16148,6 +22025,21 @@ void Entity::handleWeaponArmAttack(Entity* weaponarm) { this->attack(MONSTER_POSE_MAGIC_CAST1, 0, nullptr); } + else if ( stats && stats->type == DRYAD + && (monsterSpecialState >= MONSTER_D_SPECIAL_CAST1 && monsterSpecialState <= MONSTER_D_SPECIAL_CAST3) ) + { + this->attack(MONSTER_POSE_MAGIC_WINDUP3, 0, nullptr); + } + else if ( stats && stats->type == MYCONID + && (monsterSpecialState >= MONSTER_M_SPECIAL_CAST1 && monsterSpecialState <= MONSTER_M_SPECIAL_CAST3) ) + { + this->attack(MONSTER_POSE_MAGIC_WINDUP3, 0, nullptr); + } + else if ( stats && stats->type == GREMLIN + && monsterSpecialState == MONSTER_G_SPECIAL_CAST1 ) + { + this->attack(MONSTER_POSE_MAGIC_WINDUP3, 0, nullptr); + } else { this->attack(1, 0, nullptr); @@ -16155,6 +22047,95 @@ void Entity::handleWeaponArmAttack(Entity* weaponarm) } } } + else if ( monsterAttack == MONSTER_POSE_FLAIL_SWING_WINDUP ) + { + if ( monsterAttackTime == 0 ) + { + // init rotations + weaponarm->pitch = 0; + this->monsterArmbended = 1; + this->monsterWeaponYaw = 0.0; + weaponarm->roll = 0; + weaponarm->skill[1] = 0; + weaponarm->monsterAnimationLimbDirection = ANIMATE_DIR_NONE; + } + + ++monsterAttackTime; // manually increment counter + + if ( weaponarm->skill[1] == 0 ) + { + limbAnimateToLimit(this, ANIMATE_WEAPON_YAW, 0.15, PI / 4, false, 0.0); + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, -0.15, 5 * PI / 4, true, 0.05) ) + { + if ( monsterAttackTime >= TICKS_PER_SECOND ) + { + weaponarm->skill[1] = 1; + weaponarm->monsterAnimationLimbDirection = ANIMATE_DIR_NONE; + } + } + } + else if ( weaponarm->skill[1] == 1 ) + { + limbAnimateToLimit(this, ANIMATE_WEAPON_YAW, -0.15, 0.0, false, 0.0); + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, 0.3, 7 * PI / 4, false, 0.0) ) + { + if ( multiplayer != CLIENT ) + { + monsterAttackTime = 0; + this->attack(MONSTER_POSE_FLAIL_SWING, 0, nullptr); + } + } + } + } + else if ( monsterAttack == MONSTER_POSE_FLAIL_SWING ) + { + if ( monsterAttackTime == 0 ) + { + // init rotations + this->monsterArmbended = 1; + this->monsterWeaponYaw = 0; + weaponarm->pitch = 13 * PI / 8; + weaponarm->roll = 0; + weaponarm->skill[1] = 0; + } + + ++monsterAttackTime; // manually increment counter + weaponarm->skill[1]++; + + if ( multiplayer != CLIENT ) + { + if ( monsterAttackTime >= 3 * TICKS_PER_SECOND ) + { + this->attack(MONSTER_POSE_FLAIL_SWING_RETURN, 0, nullptr); + } + else if ( monsterAttackTime % 25 == 0 ) + { + this->attack(MONSTER_POSE_FLAIL_SWING, 0, nullptr); + } + } + } + else if ( monsterAttack == MONSTER_POSE_FLAIL_SWING_RETURN ) + { + if ( monsterAttackTime == 0 ) + { + // init rotations + this->monsterArmbended = 1; + this->monsterWeaponYaw = 0; + weaponarm->pitch = 13 * PI / 8; + weaponarm->roll = 0; + } + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, 0.15, 7 * PI / 4, false, 0.0) ) + { + weaponarm->skill[0] = rightbody->skill[0]; + weaponarm->skill[1] = 0; + this->monsterWeaponYaw = 0; + weaponarm->pitch = rightbody->pitch; + weaponarm->roll = 0; + this->monsterArmbended = 0; + monsterAttack = 0; + } + ++monsterAttackTime; // manually increment counter + } return; } @@ -16380,14 +22361,20 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) int monsterType = this->getMonsterTypeFromSprite(); int myAttack = this->monsterAttack; bool isPlayer = this->behavior == &actPlayer; + bool neutralPose = myAttack == 0; if ( isPlayer ) { myAttack = this->skill[9]; + if ( myAttack == MONSTER_POSE_MAGIC_WINDUP1 ) + { + neutralPose = true; + } } if ( weaponLimb->flags[INVISIBLE] == false || weaponLimb->flags[INVISIBLE_DITHER] ) //TODO: isInvisible()? { - if ( weaponLimb->sprite == items[SHORTBOW].index ) + if ( weaponLimb->sprite == items[SHORTBOW].index + || weaponLimb->sprite == items[BONE_SHORTBOW].index ) { weaponLimb->x = weaponArmLimb->x - .5 * cos(weaponArmLimb->yaw); weaponLimb->y = weaponArmLimb->y - .5 * sin(weaponArmLimb->yaw); @@ -16396,6 +22383,8 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) } else if ( weaponLimb->sprite == items[ARTIFACT_BOW].index || weaponLimb->sprite == items[LONGBOW].index + || weaponLimb->sprite == items[BRANCH_BOW].index + || weaponLimb->sprite == items[BRANCH_BOW_INFECTED].index || weaponLimb->sprite == items[COMPOUND_BOW].index ) { if ( isPlayer && monsterType == HUMAN ) @@ -16418,13 +22407,20 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) weaponLimb->x -= .5 * cos(weaponArmLimb->yaw); weaponLimb->y -= .5 * sin(weaponArmLimb->yaw); } + else if ( weaponLimb->sprite == items[BRANCH_BOW].index + || weaponLimb->sprite == items[BRANCH_BOW_INFECTED].index ) + { + weaponLimb->x -= .5 * cos(weaponArmLimb->yaw); + weaponLimb->y -= .5 * sin(weaponArmLimb->yaw); + } else if ( weaponLimb->sprite == items[COMPOUND_BOW].index ) { weaponLimb->x += .5 * cos(weaponArmLimb->yaw); weaponLimb->y += .5 * sin(weaponArmLimb->yaw); } } - else if ( weaponLimb->sprite == items[CROSSBOW].index || weaponLimb->sprite == items[HEAVY_CROSSBOW].index ) + else if ( weaponLimb->sprite == items[CROSSBOW].index || weaponLimb->sprite == items[HEAVY_CROSSBOW].index + || weaponLimb->sprite == items[BLACKIRON_CROSSBOW].index ) { weaponLimb->x = weaponArmLimb->x; weaponLimb->y = weaponArmLimb->y; @@ -16483,7 +22479,7 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) } else { - weaponLimb->z = weaponArmLimb->z - .5 * (myAttack == 0); + weaponLimb->z = weaponArmLimb->z - .5 * (neutralPose); } if ( weaponLimb->pitch > PI / 2 ) { @@ -16502,14 +22498,14 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) // hold sword with pitch aligned to arm rotation. else { - weaponLimb->x = weaponArmLimb->x + .5 * cos(weaponArmLimb->yaw) * (myAttack == 0); - weaponLimb->y = weaponArmLimb->y + .5 * sin(weaponArmLimb->yaw) * (myAttack == 0); + weaponLimb->x = weaponArmLimb->x + .5 * cos(weaponArmLimb->yaw) * (neutralPose); + weaponLimb->y = weaponArmLimb->y + .5 * sin(weaponArmLimb->yaw) * (neutralPose); weaponLimb->z = weaponArmLimb->z - .5; if ( monsterType == BUGBEAR ) { weaponLimb->z = weaponArmLimb->z; } - weaponLimb->pitch = weaponArmLimb->pitch + .25 * (myAttack == 0); + weaponLimb->pitch = weaponArmLimb->pitch + .25 * (neutralPose); if ( monsterType == INCUBUS || monsterType == SUCCUBUS ) { weaponLimb->z += 1; @@ -16518,10 +22514,10 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) } else { - weaponLimb->x = weaponArmLimb->x + .5 * cos(weaponArmLimb->yaw) * (myAttack == 0); - weaponLimb->y = weaponArmLimb->y + .5 * sin(weaponArmLimb->yaw) * (myAttack == 0); - weaponLimb->z = weaponArmLimb->z - .5 * (myAttack == 0); - weaponLimb->pitch = weaponArmLimb->pitch + .25 * (myAttack == 0); + weaponLimb->x = weaponArmLimb->x + .5 * cos(weaponArmLimb->yaw) * (neutralPose); + weaponLimb->y = weaponArmLimb->y + .5 * sin(weaponArmLimb->yaw) * (neutralPose); + weaponLimb->z = weaponArmLimb->z - .5 * (neutralPose); + weaponLimb->pitch = weaponArmLimb->pitch + .25 * (neutralPose); if ( monsterType == BUGBEAR ) { if ( !isPlayer && this->monsterArmbended ) @@ -16532,12 +22528,20 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) weaponLimb->pitch = weaponArmLimb->pitch; } } + if ( myAttack == MONSTER_POSE_PARRY ) + { + weaponLimb->pitch += 1.0; + weaponLimb->x += 2.0 * cos(weaponArmLimb->yaw) - 0.0 * cos(weaponArmLimb->yaw + PI / 2); + weaponLimb->y += 2.0 * sin(weaponArmLimb->yaw) - 0.0 * sin(weaponArmLimb->yaw + PI / 2); + weaponLimb->z -= 2.0; + } } } } weaponLimb->yaw = weaponArmLimb->yaw; bool isPotion = false; + if ( myAttack == MONSTER_POSE_RANGED_WINDUP3 && monsterType == GOATMAN && !isPlayer ) { // specific for potion throwing goatmen. @@ -16558,7 +22562,7 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) { weaponLimb->roll += (PI / 2); // sprite rotated weaponLimb->pitch -= PI / 8; - weaponLimb->pitch += .25 * (myAttack != 0); // add 0.25 if attacking + weaponLimb->pitch += .25 * (!neutralPose); // add 0.25 if attacking } else if ( weaponLimb->sprite == items[FOOD_CREAMPIE].index ) { @@ -16610,6 +22614,28 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) weaponLimb->sprite = items[TOOL_WHIP].index; } } + else if ( weaponLimb->sprite >= items[STEEL_FLAIL].index && weaponLimb->sprite <= items[STEEL_FLAIL].index + 2 ) + { + if ( myAttack == MONSTER_POSE_FLAIL_SWING ) + { + real_t spin = weaponArmLimb->skill[1] * -0.35; + weaponLimb->roll += spin; + weaponLimb->pitch += 0.1 * cos(spin + 0.5 * PI); // wobbly + weaponLimb->yaw += 0.1 * sin(spin + 0.5 * PI); // wobbly + weaponLimb->sprite = items[STEEL_FLAIL].index + 1; + } + else + { + if ( myAttack > 0 && !neutralPose && myAttack != MONSTER_POSE_FLAIL_SWING_RETURN ) + { + weaponLimb->sprite = items[STEEL_FLAIL].index + 2; + } + else + { + weaponLimb->sprite = items[STEEL_FLAIL].index; + } + } + } else if ( weaponLimb->sprite == items[TOOL_DECOY].index || weaponLimb->sprite == items[TOOL_DUMMYBOT].index ) { weaponLimb->scalex = 0.8; @@ -16628,12 +22654,8 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) { weaponLimb->focalx = limbs[monsterType][6][0]; // 2.5 weaponLimb->focaly = limbs[monsterType][6][1]; // 0 - if ( weaponLimb->sprite == items[CROSSBOW].index || weaponLimb->sprite == items[HEAVY_CROSSBOW].index ) - { - weaponLimb->focalx += 2.1; - weaponLimb->focaly -= 0.1; - } weaponLimb->focalz = limbs[monsterType][6][2]; // -.5 + if ( isPlayer && isPotion ) { weaponLimb->focalz += 1; @@ -16658,6 +22680,11 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) case GOATMAN: case INSECTOID: case GOBLIN: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case GNOME: weaponLimb->x += 0.5 * cos(weaponArmLimb->yaw + PI / 2); weaponLimb->y += 0.5 * sin(weaponArmLimb->yaw + PI / 2); break; @@ -16698,10 +22725,21 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) weaponLimb->focalz -= 1; } } + else if ( weaponLimb->sprite == items[CROSSBOW].index + || weaponLimb->sprite == items[HEAVY_CROSSBOW].index + || weaponLimb->sprite == items[BLACKIRON_CROSSBOW].index ) + { + weaponLimb->focalx += 2.1; + weaponLimb->focaly -= 0.1; + } else if ( weaponLimb->sprite == items[SHORTBOW].index || weaponLimb->sprite == items[ARTIFACT_BOW].index - || weaponLimb->sprite == items[LONGBOW].index || weaponLimb->sprite == items[COMPOUND_BOW].index ) + || weaponLimb->sprite == items[LONGBOW].index || weaponLimb->sprite == items[BRANCH_BOW].index + || weaponLimb->sprite == items[BRANCH_BOW_INFECTED].index + || weaponLimb->sprite == items[COMPOUND_BOW].index + || weaponLimb->sprite == items[BONE_SHORTBOW].index ) { - if ( weaponLimb->sprite == items[SHORTBOW].index ) + if ( weaponLimb->sprite == items[SHORTBOW].index + || weaponLimb->sprite == items[BONE_SHORTBOW].index ) { switch ( monsterType ) { @@ -16720,6 +22758,11 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) case INSECTOID: case SUCCUBUS: case INCUBUS: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case GNOME: weaponLimb->x += -.1 * cos(weaponArmLimb->yaw + PI / 2) + 0.25 * cos(weaponArmLimb->yaw); weaponLimb->y += -.1 * sin(weaponArmLimb->yaw + PI / 2) + 0.25 * sin(weaponArmLimb->yaw); weaponLimb->z += -1; @@ -16741,7 +22784,9 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) } } else if ( weaponLimb->sprite == items[ARTIFACT_BOW].index - || weaponLimb->sprite == items[LONGBOW].index ) + || weaponLimb->sprite == items[LONGBOW].index + || weaponLimb->sprite == items[BRANCH_BOW].index + || weaponLimb->sprite == items[BRANCH_BOW_INFECTED].index ) { switch ( monsterType ) { @@ -16754,7 +22799,9 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) weaponLimb->focalx += -0.5; weaponLimb->focaly += 0; weaponLimb->focalz += 1.75; - if ( weaponLimb->sprite == items[LONGBOW].index ) + if ( weaponLimb->sprite == items[LONGBOW].index + || weaponLimb->sprite == items[BRANCH_BOW].index + || weaponLimb->sprite == items[BRANCH_BOW_INFECTED].index ) { weaponLimb->x += -0.25 * cos(weaponArmLimb->yaw); weaponLimb->y += -0.25 * sin(weaponArmLimb->yaw); @@ -16767,6 +22814,11 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) case GOBLIN: case GOATMAN: case INSECTOID: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case GNOME: weaponLimb->x += -.1 * cos(weaponArmLimb->yaw + PI / 2) + 0.5 * cos(weaponArmLimb->yaw); weaponLimb->y += -.1 * sin(weaponArmLimb->yaw + PI / 2) + 0.5 * sin(weaponArmLimb->yaw); weaponLimb->z += -1; @@ -16795,7 +22847,9 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) default: break; } - if ( weaponLimb->sprite == items[LONGBOW].index ) + if ( weaponLimb->sprite == items[LONGBOW].index + || weaponLimb->sprite == items[BRANCH_BOW].index + || weaponLimb->sprite == items[BRANCH_BOW_INFECTED].index ) { // this applies to all offsets for all monsters. weaponLimb->x += -.1 * cos(weaponArmLimb->yaw + PI / 2) + 0.75 * cos(weaponArmLimb->yaw); @@ -16823,6 +22877,11 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) case GOBLIN: case GOATMAN: case INSECTOID: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case GNOME: weaponLimb->x += -.1 * cos(weaponArmLimb->yaw + PI / 2) + 0.5 * cos(weaponArmLimb->yaw); weaponLimb->y += -.1 * sin(weaponArmLimb->yaw + PI / 2) + 0.5 * sin(weaponArmLimb->yaw); weaponLimb->z += -1; @@ -16870,11 +22929,27 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) case AUTOMATON: case INSECTOID: case GOBLIN: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case GNOME: weaponLimb->focaly -= 0.05; // minor z-fighting fix. break; default: break; } + + if ( monsterType == DRYAD || monsterType == MYCONID || monsterType == SALAMANDER || monsterType == GREMLIN + || monsterType == GNOME ) + { + weaponLimb->x += limbs[monsterType][17][0] * cos(weaponArmLimb->yaw + PI / 2) + limbs[monsterType][17][1] * cos(weaponArmLimb->yaw); + weaponLimb->y += limbs[monsterType][17][0] * sin(weaponArmLimb->yaw + PI / 2) + limbs[monsterType][17][1] * sin(weaponArmLimb->yaw); + weaponLimb->z += limbs[monsterType][17][2]; + + weaponLimb->focalx += 0.5; + weaponLimb->focalz -= 0.5; + } } } else @@ -16925,6 +23000,11 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) case GOATMAN: case INSECTOID: case GOBLIN: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case GNOME: weaponLimb->x += 0.5 * cos(weaponArmLimb->yaw + PI / 2); weaponLimb->y += 0.5 * sin(weaponArmLimb->yaw + PI / 2); break; @@ -16963,11 +23043,270 @@ void Entity::handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb) weaponLimb->yaw -= sin(weaponArmLimb->roll) * PI / 2; weaponLimb->pitch += cos(weaponArmLimb->roll) * PI / 2; + + if ( weaponLimb->sprite == items[STEEL_FLAIL].index + 1 ) + { + weaponLimb->x += 3.25 * cos(weaponArmLimb->yaw); + weaponLimb->y += 3.25 * sin(weaponArmLimb->yaw); + + // overwrite the focals since this animation is fixed positioned + weaponLimb->focalx = -2; + weaponLimb->focaly = 2.25; + weaponLimb->focalz = 0.5; + + weaponLimb->pitch += 4.75 * PI / 8; + //weaponLimb->roll += 0.0 * PI / 8; + weaponLimb->yaw += -1.5 * PI / 8; + + switch ( monsterType ) + { + case SKELETON: + weaponLimb->x += -0.25 * cos(weaponArmLimb->yaw + PI / 2) + 0.25 * cos(weaponArmLimb->yaw); + weaponLimb->y += -0.25 * sin(weaponArmLimb->yaw + PI / 2) + 0.25 * sin(weaponArmLimb->yaw); + break; + case SUCCUBUS: + case INCUBUS: + weaponLimb->x += -0.25 * cos(weaponArmLimb->yaw + PI / 2) + 0.5 * cos(weaponArmLimb->yaw); + weaponLimb->y += -0.25 * sin(weaponArmLimb->yaw + PI / 2) + 0.5 * sin(weaponArmLimb->yaw); + weaponLimb->z += 0.5; + break; + default: + break; + } + } + } + + + if ( weaponLimb->sprite == items[BOLAS].index ) + { + weaponLimb->focalx += 0.25; + weaponLimb->focalz += 2.75; + weaponLimb->focaly += -0.5; + } + else if ( weaponLimb->sprite == items[GREASE_BALL].index + || weaponLimb->sprite == items[DUST_BALL].index + || weaponLimb->sprite == items[SLOP_BALL].index ) + { + weaponLimb->focalz += 1; + } + else if ( weaponLimb->sprite == items[BLACKIRON_DART].index ) + { + weaponLimb->focalx += 1.0; + weaponLimb->focalz += 1.5; + } + else if ( weaponLimb->sprite == items[SILVER_PLUMBATA].index ) + { + weaponLimb->focalx += 1.5; + weaponLimb->focalz += 1.0; + } + else if ( weaponLimb->sprite == items[SHILLELAGH_MACE].index ) + { + weaponLimb->focalx += 0.5; + weaponLimb->focalz -= 1.0; + } + else if ( weaponLimb->sprite == items[CLAYMORE_SWORD].index + || weaponLimb->sprite == items[SILVER_SWORD].index ) + { + weaponLimb->focalz -= 0.5; + } + else if ( weaponLimb->sprite == items[STEEL_FALSHION].index ) + { + weaponLimb->focalz -= 1.0; + } + else if ( weaponLimb->sprite == items[SILVER_AXE].index || weaponLimb->sprite == items[SILVER_GLAIVE].index ) + { + weaponLimb->focalx += 0.5; + } + + if ( monsterType == DRYAD ) + { + if ( sprite == 1514 || sprite == 1515 || sprite == 1992 || sprite == 1993 ) + { + // short arm move weapon closer + weaponLimb->x += -0.5 * cos(weaponArmLimb->yaw + PI / 2); + weaponLimb->y += -0.5 * sin(weaponArmLimb->yaw + PI / 2); + } + } + else if ( monsterType == MYCONID ) + { + if ( sprite == 1520 || sprite == 1998 ) + { + // short arm move weapon closer + weaponLimb->x += -0.5 * cos(weaponArmLimb->yaw + PI / 2); + weaponLimb->y += -0.5 * sin(weaponArmLimb->yaw + PI / 2); + } + } + + if ( isPlayer && myAttack == MONSTER_POSE_MAGIC_WINDUP1 && weaponLimb->ticks % 10 == 0 ) + { + bool doSpawnGib = true; + if ( doSpawnGib ) + { + Entity* gib = spawnGib(weaponArmLimb, 16); + gib->flags[INVISIBLE] = false; + gib->flags[SPRITE] = true; + gib->flags[NOUPDATE] = true; + gib->flags[UPDATENEEDED] = false; + gib->lightBonus = vec4(0.2f, 0.2f, 0.2f, 0.f); + gib->x += 1.5 * cos(weaponArmLimb->yaw); + gib->y += 1.5 * sin(weaponArmLimb->yaw); + gib->z = weaponLimb->z + 1.0; + if ( weaponLimb->flags[INVISIBLE] && !weaponLimb->flags[INVISIBLE_DITHER] ) + { + gib->z += 1.0; + } + if ( monsterType == CREATURE_IMP ) + { + gib->z += 3.0; + gib->x += 3.5 * cos(weaponArmLimb->yaw); + gib->y += 3.5 * sin(weaponArmLimb->yaw); + } + else if ( monsterType == TROLL ) + { + gib->z += 2.0; + gib->x += 2.5 * cos(weaponArmLimb->yaw); + gib->y += 2.5 * sin(weaponArmLimb->yaw); + } + gib->scalex = 0.25f; //MAKE 'EM SMALL PLEASE! + gib->scaley = 0.25f; + gib->scalez = 0.25f; + gib->sprite = 16; //TODO: Originally. 22. 16 -- spark sprite instead? + gib->yaw = ((local_rng.rand() % 6) * 60) * PI / 180.0; + gib->pitch = (local_rng.rand() % 360) * PI / 180.0; + gib->roll = (local_rng.rand() % 360) * PI / 180.0; + gib->vel_x = cos(weaponArmLimb->yaw) * .1; + gib->vel_y = sin(weaponArmLimb->yaw) * .1; + gib->vel_z = -.15; + gib->fskill[3] = 0.01; + gib->fskill[4] = 0.01; // GIB_SHRINK + gib->skill[4] = 25; // GIB_LIFESPAN + gib->skill[11] = this->skill[2]; + gib->actGibDisableDrawForLocalPlayer = 1 + this->skill[2]; + } + } + else if ( isPlayer && myAttack == MONSTER_POSE_MAGIC_WINDUP2 ) + { + doParticleEffectForTouchSpell(*this, weaponArmLimb, (Monster)monsterType); } return; } +void doParticleEffectForTouchSpell(Entity& my, Entity* focalLimb, Monster monsterType) +{ + if ( my.behavior != &actPlayer ) + { + return; + } + + real_t dir = focalLimb->yaw; + real_t x = focalLimb->x + 2.5 * cos(dir); + real_t y = focalLimb->y + 2.5 * sin(dir); + real_t z = focalLimb->z - 0.5; + + real_t forwardOffset = 0.0; + if ( monsterType == GOBLIN || monsterType == SKELETON + || monsterType == INSECTOID + || monsterType == GOATMAN + || monsterType == DRYAD + || monsterType == GREMLIN + || monsterType == GNOME + || monsterType == MYCONID ) + { + z += 0.5; + } + else if ( monsterType == SALAMANDER ) + { + z += 1.0; + } + else if ( monsterType == SUCCUBUS ) + { + forwardOffset -= 0.5; + z += 1.5; + } + else if ( monsterType == INCUBUS ) + { + forwardOffset -= 0.5; + z += 1.5; + } + else if ( monsterType == TROLL ) + { + forwardOffset += 3.0; + z += 1.5; + } + else if ( monsterType == CREATURE_IMP ) + { + forwardOffset += 3.5; + z += 3.5; + x -= 0.5 * cos(dir + PI / 2); + y -= 0.5 * sin(dir + PI / 2); + } + else if ( monsterType == SPIDER ) + { + forwardOffset += 3.5; + } + else if ( monsterType == RAT ) + { + forwardOffset += 4.0; + } + x += forwardOffset * cos(dir); + y += forwardOffset * sin(dir); + + for ( int i = 1; i < 3; ++i ) + { + //if ( i == 1 || i == 3 ) { continue; } + Uint32 animTick = std::min(20, my.skill[10]); // PLAYER_ATTACKTIME + + Entity* entity = newEntity(1243, 1, map.entities, nullptr); //Particle entity. + entity->x = x - 0.01 * (5 + local_rng.rand() % 11); + entity->y = y - 0.01 * (5 + local_rng.rand() % 11); + entity->z = z - 0.01 * (10 + local_rng.rand() % 21); + + real_t scaleOut = -2.0; + if ( i == 1 ) + { + entity->x += (scaleOut + scaleOut * sin(2 * PI * (animTick % 40) / 40.f)) * cos(dir + PI / 2); + entity->y += (scaleOut + scaleOut * sin(2 * PI * (animTick % 40) / 40.f)) * sin(dir + PI / 2); + } + else + { + entity->x += (-scaleOut - scaleOut * sin(2 * PI * (animTick % 40) / 40.f)) * cos(dir + PI / 2); + entity->y += (-scaleOut - scaleOut * sin(2 * PI * (animTick % 40) / 40.f)) * sin(dir + PI / 2); + } + + entity->focalz = -2; + + real_t scale = 0.05f; + scale += (animTick) * 0.025f; + scale = std::min(scale, 0.5); + + entity->scalex = scale; + entity->scaley = scale; + entity->scalez = scale; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = dir; + entity->roll = i * 2 * PI / 3; + entity->pitch = PI + ((animTick % 40) / 40.f) * 2 * PI; + entity->ditheringDisabled = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = false; + //entity->flags[OVERDRAW] = true; + entity->lightBonus = vec4(0.25f, 0.25f, + 0.25f, 0.f); + entity->behavior = &actTouchCastThirdPersonParticle; + entity->vel_z = 0; + entity->skill[11] = my.skill[2]; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + void Entity::lookAtEntity(Entity& target) { double tangent = atan2(target.y - y, target.x - x); @@ -17064,7 +23403,7 @@ void actAmbientParticleEffectIdle2(Entity* my) return; } -void Entity::spawnAmbientParticles2(int chance, int particleSprite, int duration, double particleScale, bool shrink) +Entity* Entity::spawnAmbientParticles2(int chance, int particleSprite, int duration, double particleScale, bool shrink) { if ( local_rng.rand() % chance == 0 ) { @@ -17090,10 +23429,12 @@ void Entity::spawnAmbientParticles2(int chance, int particleSprite, int duration spawnParticle->behavior = &actAmbientParticleEffectIdle2; spawnParticle->flags[PASSABLE] = true; spawnParticle->setUID(-3); + return spawnParticle; } + return nullptr; } -void Entity::spawnAmbientParticles(int chance, int particleSprite, int duration, double particleScale, bool shrink) +Entity* Entity::spawnAmbientParticles(int chance, int particleSprite, int duration, double particleScale, bool shrink) { if ( local_rng.rand() % chance == 0 ) { @@ -17110,56 +23451,388 @@ void Entity::spawnAmbientParticles(int chance, int particleSprite, int duration, spawnParticle->particleDuration = duration; if ( shrink ) { - spawnParticle->particleShrink = 1; + spawnParticle->particleShrink = 1; + } + else + { + spawnParticle->particleShrink = 0; + } + spawnParticle->behavior = &actAmbientParticleEffectIdle; + spawnParticle->flags[PASSABLE] = true; + spawnParticle->setUID(-3); + return spawnParticle; + } + return nullptr; +} + +void Entity::handleEffectsClient() +{ + int bodypart = 0; + for ( auto node = this->children.first; node; node = node->next, bodypart++ ) + { + if ( behavior == &actPlayer ) + { + if ( bodypart < 1 ) + { + continue; + } + } + else + { + if ( bodypart < 2 ) + { + continue; + } + } + if ( Entity* entity = (Entity*)node->element ) + { + entity->flags[STASIS_DITHER] = flags[STASIS_DITHER]; + } + } + + Stat* myStats = getStats(); + + if ( !myStats ) + { + return; + } + + if ( myStats->getEffectActive(EFF_MAGICREFLECT) ) + { + spawnAmbientParticles(80, 579, 10 + local_rng.rand() % 40, 1.0, false); + } + + if ( myStats->getEffectActive(EFF_FEAR) ) + { + if ( ticks % 25 == 0 || ticks % 40 == 0 ) + { + spawnAmbientParticles(1, 864, 20 + local_rng.rand() % 10, 0.5, true); + } + } + + if ( myStats->getEffectActive(EFF_TROLLS_BLOOD) ) + { + spawnAmbientParticles(80, 169, 20 + local_rng.rand() % 10, 0.5, true); + } + + if ( myStats->getEffectActive(EFF_VAMPIRICAURA) ) + { + spawnAmbientParticles(30, 600, 20 + local_rng.rand() % 30, 0.5, true); + } + + if ( myStats->getEffectActive(EFF_PACIFY) ) + { + spawnAmbientParticles(30, 685, 20 + local_rng.rand() % 30, 0.5, true); + } + + if ( myStats->getEffectActive(EFF_THORNS) ) + { + if ( ticks % 25 == 0 ) + { + if ( Entity* particle = createParticleAestheticOrbit(this, 262, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_THORNS_ORBIT) ) + { + particle->sizex = 8; + particle->sizey = 8; + particle->flags[GENIUS] = true; + particle->z += (local_rng.rand() % 8) * 0.25; + particle->flags[SPRITE] = true; + particle->yaw = this->yaw - (3 * PI / 8) + ((ticks / 25) % 4) * (2 / 4.0) * PI / 2; + //particle->flags[INVISIBLE] = true; + } } - else + } + + if ( myStats->getEffectActive(EFF_CONFUSED) ) + { + if ( ticks % 25 == 0 ) { - spawnParticle->particleShrink = 0; + if ( !strncmp(map.name, "Sanctum", 7) && getMonsterTypeFromSprite() == AUTOMATON && !monsterAllyGetPlayerLeader() ) + { + // boss automatons + } + else if ( Entity* particle = createParticleAestheticOrbit(this, 2203, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_CONFUSE_ORBIT) ) + { + particle->yaw = 0.0; + particle->actmagicOrbitDist = 4; + particle->scalex = 0.05; + particle->scaley = 0.05; + particle->scalez = 0.05; + particle->z -= 4; + particle->z = std::max(-6.0, particle->z); + } } - spawnParticle->behavior = &actAmbientParticleEffectIdle; - spawnParticle->flags[PASSABLE] = true; - spawnParticle->setUID(-3); } -} - -void Entity::handleEffectsClient() -{ - Stat* myStats = getStats(); - if ( !myStats ) + if ( myStats->getEffectActive(EFF_STASIS) ) { - return; + if ( ticks % 25 == 0 ) + { + if ( Entity* particle = createParticleAestheticOrbit(this, 2178, TICKS_PER_SECOND, PARTICLE_EFFECT_STASIS_RIFT_ORBIT) ) + { + particle->flags[INVISIBLE] = true; + } + } } - if ( myStats->EFFECTS[EFF_MAGICREFLECT] ) + if ( myStats->getEffectActive(EFF_STATIC) ) { - spawnAmbientParticles(80, 579, 10 + local_rng.rand() % 40, 1.0, false); + int interval = 40; + if ( ticks % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 1758, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * ((ticks / interval) % 3); + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += ((ticks / interval) % 3) * 2 * PI / 3; + fx->actmagicNoLight = 1; + } } - if ( myStats->EFFECTS[EFF_FEAR] ) + if ( myStats->getEffectActive(EFF_MAXIMISE) ) { - if ( ticks % 25 == 0 || ticks % 40 == 0 ) + int interval = 40; + if ( (ticks + 20) % interval == 0 ) { - spawnAmbientParticles(1, 864, 20 + local_rng.rand() % 10, 0.5, true); + Entity* fx = createParticleAestheticOrbit(this, 2335, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * ((ticks / interval) % 3); + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += ((ticks / interval) % 3) * 2 * PI / 3; + fx->actmagicNoLight = 1; } } - - if ( myStats->EFFECTS[EFF_TROLLS_BLOOD] ) + else if ( myStats->getEffectActive(EFF_MINIMISE) ) { - spawnAmbientParticles(80, 169, 20 + local_rng.rand() % 10, 0.5, true); + int interval = 40; + if ( (ticks + 20) % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 2341, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * ((ticks / interval) % 3); + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += ((ticks / interval) % 3) * 2 * PI / 3; + fx->actmagicNoLight = 1; + } } - if ( myStats->EFFECTS[EFF_VAMPIRICAURA] ) + if ( myStats->getEffectActive(EFF_MAGICIANS_ARMOR) ) { - spawnAmbientParticles(30, 600, 20 + local_rng.rand() % 30, 0.5, true); + int interval = 80; + if ( ticks % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 276, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_MAGICIANS_ARMOR_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + //fx->flags[GENIUS] = true; + fx->scalex = 0.0125; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + + if ( Entity* fx = createParticleAOEIndicator(this, this->x, this->y, 0.0, 2 * TICKS_PER_SECOND, 16.0) ) + { + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(101, 16, 145); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 2; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + indicator->cacheType = AOEIndicators_t::CACHE_MAGICIANS_ARMOR; + } + } + } } - if ( myStats->EFFECTS[EFF_PACIFY] ) + if ( myStats->getEffectActive(EFF_GUARD_BODY) || myStats->getEffectActive(EFF_GUARD_SPIRIT) || myStats->getEffectActive(EFF_DIVINE_GUARD) ) { - spawnAmbientParticles(30, 685, 20 + local_rng.rand() % 30, 0.5, true); + int interval = 80; + if ( ticks % interval == 0 ) + { + if ( Entity* fx = createParticleAOEIndicator(this, this->x, this->y, 0.0, 2 * TICKS_PER_SECOND, 16.0) ) + { + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(255, 255, 255); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 2; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + indicator->cacheType = AOEIndicators_t::CACHE_THAUM_ARMOR; + } + } + } + + if ( myStats->getEffectActive(EFF_GUARD_BODY) ) + { + if ( ticks % interval == 20 ) + { + Entity* fx = createParticleAestheticOrbit(this, 280, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_GUARD_BODY_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + fx->flags[GENIUS] = true; + fx->scalex = 0.05; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + } + } + if ( myStats->getEffectActive(EFF_GUARD_SPIRIT) ) + { + if ( ticks % interval == 40 ) + { + Entity* fx = createParticleAestheticOrbit(this, 281, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_GUARD_SPIRIT_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + fx->flags[GENIUS] = true; + fx->scalex = 0.05; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + } + } + if ( myStats->getEffectActive(EFF_DIVINE_GUARD) ) + { + if ( ticks % interval == 60 ) + { + Entity* fx = createParticleAestheticOrbit(this, 282, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_GUARD_DIVINE_ORBIT); + fx->flags[SPRITE] = true; + fx->z = 4.0 - 2.0 * ((ticks / interval) % 4); + fx->vel_z = -0.025; + fx->sizex = 4; + fx->sizey = 4; + fx->flags[GENIUS] = true; + fx->scalex = 0.05; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = this->yaw + PI; + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + } + } + } + + if ( myStats->getEffectActive(EFF_FLAME_CLOAK) || + (myStats->type == SALAMANDER && myStats->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && myStats->getEffectActive(EFF_SALAMANDER_HEART) <= 2) ) + { + int interval = 40; + if ( ticks % interval == 0 ) + { + Entity* fx = createParticleAestheticOrbit(this, 233, TICKS_PER_SECOND, PARTICLE_EFFECT_IGNITE_ORBIT_FOLLOW); + fx->flags[SPRITE] = true; + fx->z = this->z;// 7.5 - 2.0 * ((ticks / interval) % 3); + fx->vel_z = 0.25; + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 3; + fx->fskill[2] = this->yaw + PI; + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 0; + } + if ( (myStats->type == SALAMANDER && myStats->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && myStats->getEffectActive(EFF_SALAMANDER_HEART) <= 2) ) + { + if ( ((ticks % 10 == 0) && (abs(this->vel_x) > 0.1 || abs(this->vel_y) > 0.1)) ) + { + //if ( abs(this->vel_x) > 0.05 || abs(this->vel_y) > 0.05 ) + { + Entity* fx = createParticleAestheticOrbit(this, 233, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_RADIANT_ORBIT_FOLLOW); + fx->flags[SPRITE] = true; + fx->z = this->z;// 7.5 - 2.0 * ((ticks / interval) % 3); + fx->vel_z = 0.25; + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 1; + fx->z += 8; + fx->fskill[2] = this->yaw + PI; + fx->fskill[4] = 0.1; // rotate + fx->x = this->x + fx->actmagicOrbitDist * cos(this->yaw + PI); + fx->y = this->y + fx->actmagicOrbitDist * sin(this->yaw + PI); + //fx->fskill[2] += ((ticks / interval) % 3) * 2 * PI / 3; + fx->yaw = fx->fskill[2]; + if ( (abs(this->vel_x) > 0.1 || abs(this->vel_y) > 0.1) ) + { + fx->actmagicNoLight = 0; + } + else + { + fx->actmagicNoLight = 1; + } + } + } + } + + if ( myStats->getEffectActive(EFF_FLAME_CLOAK) && (ticks % interval == 0) ) + { + if ( Entity* fx = createParticleAOEIndicator(this, this->x, this->y, 0.0, TICKS_PER_SECOND, 16.0) ) + { + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(255, 128, 0); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 2; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + indicator->cacheType = AOEIndicators_t::CACHE_FLAME_CLOAK; + } + } + } } - if ( myStats->EFFECTS[EFF_SHADOW_TAGGED] ) + if ( myStats->getEffectActive(EFF_SHADOW_TAGGED) ) { if ( ticks % 25 == 0 || ticks % 40 == 0 ) { @@ -17167,7 +23840,31 @@ void Entity::handleEffectsClient() } } - if ( myStats->EFFECTS[EFF_POLYMORPH] ) + if ( myStats->getEffectActive(EFF_MAGIC_GREASE) ) + { + if ( ticks % 25 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(this, 245, 1.0, 0.5) ) + { + fx->ditheringDisabled = true; + real_t dir = atan2(this->vel_y, this->vel_x); + //dir += local_rng.rand() % 2 == 0 ? PI / 32 : -PI / 32; + real_t spd = sqrt(this->vel_x * this->vel_x + this->vel_y * this->vel_y); + fx->vel_x = spd * 0.05 * cos(dir); + fx->vel_y = spd * 0.05 * sin(dir); + fx->x += 2.0 * cos(dir); + fx->y += 2.0 * sin(dir); + fx->flags[BRIGHT] = true; + fx->pitch = PI / 2; + fx->roll = 0.0; + fx->yaw = dir; + fx->vel_z = 0.0; + fx->flags[SPRITE] = true; + } + } + } + + if ( myStats->getEffectActive(EFF_POLYMORPH) ) { if ( ticks % 25 == 0 || ticks % 40 == 0 ) { @@ -17175,10 +23872,26 @@ void Entity::handleEffectsClient() } } - if ( myStats->EFFECTS[EFF_INVISIBLE] && getMonsterTypeFromSprite() == SHADOW ) + if ( myStats->getEffectActive(EFF_INVISIBLE) && getMonsterTypeFromSprite() == SHADOW ) { spawnAmbientParticles(20, 175, 20 + local_rng.rand() % 30, 0.5, true); } + + if ( myStats->getEffectActive(EFF_DUSTED) ) + { + if ( ticks % 25 == 0 || ticks % 40 == 0 ) + { + if ( Entity* fx = spawnAmbientParticles(1, local_rng.rand() % 2 ? 156 : 155, 20 + local_rng.rand() % 10, 1.0, true) ) + { + fx->flags[SPRITE] = true; + fx->x = this->x + (-4 + local_rng.rand() % 9); + fx->y = this->y + (-4 + local_rng.rand() % 9); + fx->vel_z = -0.25; + fx->z = 4.0; + fx->flags[BRIGHT] = true; + } + } + } } void Entity::serverUpdateEffectsForEntity(bool guarantee) @@ -17211,21 +23924,44 @@ void Entity::serverUpdateEffectsForEntity(bool guarantee) strcpy((char*)net_packet->data, "EFFE"); SDLNet_Write32(static_cast(getUID()), &net_packet->data[4]); - net_packet->data[8] = 0; - net_packet->data[9] = 0; - net_packet->data[10] = 0; - net_packet->data[11] = 0; - net_packet->data[12] = 0; + + int numBytes = NUMEFFECTS / 8; + for ( int i = 0; i < numBytes; ++i ) + { + net_packet->data[8 + i] = 0; + } + + std::vector> effectStrengths; for ( int i = 0; i < NUMEFFECTS; ++i ) { - if ( myStats->EFFECTS[i] ) + Uint8 effectValue = myStats->getEffectActive(i); + if ( effectValue > 0 ) { net_packet->data[8 + i / 8] |= power(2, i - (i / 8) * 8); + if ( effectValue > 1 ) + { + // effect index, then value + effectStrengths.push_back(std::make_pair(static_cast(i & 0xFF), effectValue)); + } + } + } + net_packet->data[8 + numBytes] = (Uint8)effectStrengths.size(); + net_packet->len = 8 + numBytes + 1; + for ( auto& pair : effectStrengths ) + { + if ( net_packet->len + 1 >= NET_PACKET_SIZE ) + { + // no more room + break; } + net_packet->data[net_packet->len + 0] = pair.first; + net_packet->data[net_packet->len + 1] = pair.second; + net_packet->len += 2; } + net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 13; + if ( guarantee ) { sendPacketSafe(net_sock, -1, net_packet, player - 1); @@ -17238,7 +23974,9 @@ void Entity::serverUpdateEffectsForEntity(bool guarantee) } } -bool Entity::setEffect(int effect, bool value, int duration, bool updateClients, bool guarantee) +bool Entity::setEffect(int effect, std::variant value, int duration, + bool updateClients, bool guarantee, bool overrideEffectStrength, + bool overrideDuration) { Stat* myStats = getStats(); @@ -17247,7 +23985,8 @@ bool Entity::setEffect(int effect, bool value, int duration, bool updateClients, return false; } - if ( value == true ) + Uint8 effectStrength = std::holds_alternative(value) ? (std::get(value) ? 1 : 0) : std::get(value); + if ( effectStrength > 0 ) { switch ( effect ) { @@ -17268,7 +24007,26 @@ bool Entity::setEffect(int effect, bool value, int duration, bool updateClients, } } break; + case EFF_POISONED: + if ( myStats->amulet && myStats->amulet->type == AMULET_POISONRESISTANCE ) + { + this->degradeAmuletProc(myStats, AMULET_POISONRESISTANCE); + return false; + } + break; case EFF_GREASY: + if ( myStats->type == GOATMAN ) + { + return false; + } + if ( myStats->type == LICH || myStats->type == DEVIL + || myStats->type == LICH_FIRE || myStats->type == LICH_ICE + || myStats->type == MINOTAUR || myStats->type == MIMIC || myStats->type == MINIMIMIC ) + { + return false; + } + break; + case EFF_MAGIC_GREASE: if ( myStats->type == GOATMAN ) { return false; @@ -17277,7 +24035,15 @@ bool Entity::setEffect(int effect, bool value, int duration, bool updateClients, case EFF_CONFUSED: if ( myStats->type == LICH || myStats->type == DEVIL || myStats->type == LICH_FIRE || myStats->type == LICH_ICE - || myStats->type == MINOTAUR || myStats->type == MIMIC ) + || myStats->type == MINOTAUR || myStats->type == MIMIC || myStats->type == MINIMIMIC ) + { + return false; + } + break; + case EFF_STASIS: + if ( myStats->type == LICH || myStats->type == DEVIL + || myStats->type == LICH_FIRE || myStats->type == LICH_ICE + || myStats->type == SHADOW ) { return false; } @@ -17301,17 +24067,53 @@ bool Entity::setEffect(int effect, bool value, int duration, bool updateClients, } } break; + case EFF_LIFT: + if ( myStats->type == LICH || myStats->type == DEVIL + || myStats->type == LICH_FIRE || myStats->type == LICH_ICE ) + { + return false; + } + break; case EFF_DISORIENTED: case EFF_ROOTED: + case EFF_PENANCE: + case EFF_COMMAND: if ( myStats->type == LICH || myStats->type == DEVIL || myStats->type == LICH_FIRE || myStats->type == LICH_ICE || myStats->type == SHADOW || myStats->type == SHOPKEEPER ) { return false; } + if ( myStats->type == MONSTER_ADORCISED_WEAPON ) + { + if ( myStats->getEffectActive(EFF_ROOTED) && myStats->EFFECTS_TIMERS[EFF_ROOTED] < 0 ) + { + return false; + } + } break; case EFF_FEAR: + case EFF_COWARDICE: if ( myStats->type == LICH || myStats->type == DEVIL + || myStats->type == LICH_FIRE || myStats->type == LICH_ICE + || myStats->type == SHADOW || myStats->type == MINOTAUR ) + { + return false; + } + break; + case EFF_CURSE_FLESH: + if ( myStats->type == LICH || myStats->type == DEVIL + || myStats->type == SKELETON + || myStats->type == REVENANT_SKULL + || myStats->type == MONSTER_ADORCISED_WEAPON + || myStats->type == HOLOGRAM + || myStats->type == MOTH_SMALL + || myStats->type == FLAME_ELEMENTAL + || myStats->type == EARTH_ELEMENTAL + || monsterIsTinkeringCreation() + || myStats->type == MIMIC + || myStats->type == MINIMIMIC + || myStats->type == CRYSTALGOLEM || myStats->type == LICH_FIRE || myStats->type == LICH_ICE || myStats->type == SHADOW ) { @@ -17319,7 +24121,7 @@ bool Entity::setEffect(int effect, bool value, int duration, bool updateClients, } break; case EFF_POLYMORPH: - //if ( myStats->EFFECTS[EFF_POLYMORPH] || effectPolymorph != 0 ) + //if ( myStats->getEffectActive(EFF_POLYMORPH) || effectPolymorph != 0 ) //{ // return false; //} @@ -17341,8 +24143,26 @@ bool Entity::setEffect(int effect, bool value, int duration, bool updateClients, myStats->monsterMimicLockedBy = 0; } } - myStats->EFFECTS[effect] = value; - myStats->EFFECTS_TIMERS[effect] = duration; + + if ( effectStrength > 0 ) + { + overrideEffectStrength + ? myStats->setEffectValueUnsafe(effect, effectStrength) + : myStats->setEffectActive(effect, effectStrength); + } + else + { + myStats->clearEffect(effect); + } + + if ( overrideDuration || effectStrength == 0 ) + { + myStats->EFFECTS_TIMERS[effect] = duration; + } + else + { + myStats->EFFECTS_TIMERS[effect] = std::max(myStats->EFFECTS_TIMERS[effect], duration); + } int player = -1; for ( int i = 0; i < MAXPLAYERS; ++i ) @@ -17386,11 +24206,54 @@ void Entity::monsterAcquireAttackTarget(const Entity& target, Sint32 state, bool bool hadOldTarget = (uidToEntity(monsterTarget) != nullptr); Sint32 oldMonsterState = monsterState; - if ( target.getRace() == GYROBOT || target.isInertMimic() || target.isUntargetableBat() ) + if ( target.getRace() == MONSTER_ADORCISED_WEAPON ) + { + if ( Stat* targetStats = target.getStats() ) + { + if ( targetStats->getAttribute("spirit_weapon") != "" ) + { + if ( Entity* caster = uidToEntity(target.parent) ) + { + monsterAcquireAttackTarget(*caster, state, false); + } + return; + } + } + } + + if ( target.getRace() == DUCK_SMALL ) { return; } - else if ( myStats->type == GYROBOT ) + + if ( target.getRace() == MOTH_SMALL ) + { + if ( Stat* targetStats = target.getStats() ) + { + if ( targetStats->getAttribute("fire_sprite") != "" ) + { + if ( Entity* caster = uidToEntity(target.parent) ) + { + monsterAcquireAttackTarget(*caster, state, false); + } + if ( Entity* caster = uidToEntity(targetStats->leader_uid) ) + { + monsterAcquireAttackTarget(*caster, state, false); + } + return; + } + } + } + + if ( target.behavior == &actMonster || target.behavior == &actPlayer ) + { + if ( !target.monsterIsTargetable() ) + { + return; + } + } + + if ( myStats->type == GYROBOT ) { if ( state == MONSTER_STATE_ATTACK ) { @@ -17412,6 +24275,10 @@ void Entity::monsterAcquireAttackTarget(const Entity& target, Sint32 state, bool { return; } + else if ( myStats->type == HOLOGRAM ) + { + return; + } else if ( monsterIsImmobileTurret(this, myStats) ) { if ( monsterAllyIndex >= 0 && target.behavior == &actPlayer ) @@ -17492,7 +24359,7 @@ void Entity::monsterAcquireAttackTarget(const Entity& target, Sint32 state, bool } } - if ( myStats->EFFECTS[EFF_DISORIENTED] ) + if ( myStats->getEffectActive(EFF_DISORIENTED) == 1 ) { return; } @@ -17506,7 +24373,11 @@ void Entity::monsterAcquireAttackTarget(const Entity& target, Sint32 state, bool { // check to see if holding ranged weapon, set hittime to be ready to attack. // set melee hittime close to max in hardcore mode... - if ( ((svFlags & SV_FLAG_HARDCORE) || hasRangedWeapon()) && monsterSpecialTimer <= 0 ) + if ( myStats->type == MONSTER_ADORCISED_WEAPON && myStats->getAttribute("spirit_weapon") != "" ) + { + monsterHitTime = HITRATE * 1.5 - 10; + } + else if ( ((svFlags & SV_FLAG_HARDCORE) || hasRangedWeapon()) && monsterSpecialTimer <= 0 ) { if ( hasRangedWeapon() ) { @@ -17549,7 +24420,8 @@ void Entity::monsterAcquireAttackTarget(const Entity& target, Sint32 state, bool monsterState = state; } - if ( (myStats->type == SHOPKEEPER || myStats->type == HUMAN) && monsterTarget != target.getUID() ) + if ( (myStats->type == SHOPKEEPER || myStats->type == HUMAN || (target.behavior == &actPlayer && monsterCanTradeWith(-1))) + && monsterTarget != target.getUID() ) { Stat* targetStats = target.getStats(); if ( targetStats ) @@ -17569,26 +24441,29 @@ void Entity::monsterAcquireAttackTarget(const Entity& target, Sint32 state, bool } else { - if ( monsterAllyIndex < 0 || (monsterAllyIndex >= 0 && local_rng.getU8() % 8 == 0) ) + if ( myStats->type == HUMAN ) { - for (int c = 0; c < MAXPLAYERS; ++c) + if ( monsterAllyIndex < 0 || (monsterAllyIndex >= 0 && local_rng.getU8() % 8 == 0) ) { - if (local_rng.getU8() % 2) { - players[c]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), - Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_ATTACK, Language::get(516 + local_rng.uniform(0, 1)), - Language::get(4234 + local_rng.uniform(0, 16)), getMonsterLocalizedName(targetStats->type).c_str()); - } else { - players[c]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), - Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_ATTACK, Language::get(518 + local_rng.uniform(0, 1)), - Language::get(4217 + local_rng.uniform(0, 16)), getMonsterLocalizedName(targetStats->type).c_str()); + for (int c = 0; c < MAXPLAYERS; ++c) + { + if (local_rng.getU8() % 2) { + players[c]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), + Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_ATTACK, Language::get(516 + local_rng.uniform(0, 1)), + Language::get(4234 + local_rng.uniform(0, 16)), getMonsterLocalizedName(targetStats->type).c_str()); + } else { + players[c]->worldUI.worldTooltipDialogue.createDialogueTooltip(getUID(), + Player::WorldUI_t::WorldTooltipDialogue_t::DIALOGUE_ATTACK, Language::get(518 + local_rng.uniform(0, 1)), + Language::get(4217 + local_rng.uniform(0, 16)), getMonsterLocalizedName(targetStats->type).c_str()); + } } } } } - if ( myStats->type == SHOPKEEPER && target.behavior == &actPlayer ) + if ( target.behavior == &actPlayer ) { - if ( oldMonsterState == MONSTER_STATE_TALK && monsterState != MONSTER_STATE_TALK ) + if ( oldMonsterState == MONSTER_STATE_TALK && monsterState != MONSTER_STATE_TALK && (myStats->type == SHOPKEEPER || monsterCanTradeWith(-1)) ) { for ( int i = 0; i < MAXPLAYERS; ++i ) { @@ -17632,7 +24507,8 @@ void Entity::monsterAcquireAttackTarget(const Entity& target, Sint32 state, bool } - if ( monsterAllyIndex > 0 && monsterAllyIndex < MAXPLAYERS ) + if ( (monsterAllyIndex > 0 && monsterAllyIndex < MAXPLAYERS) + || ((myStats->getEffectActive(EFF_COMMAND) >= 1 && myStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1)) ) { serverUpdateEntitySkill(this, 1); // update monsterTarget for player leaders. } @@ -17674,7 +24550,8 @@ bool Entity::monsterReleaseAttackTarget(bool force) monsterTarget = 0; - if ( monsterAllyIndex > 0 && monsterAllyIndex < MAXPLAYERS ) + if ( (monsterAllyIndex > 0 && monsterAllyIndex < MAXPLAYERS) + || ((myStats->getEffectActive(EFF_COMMAND) >= 1 && myStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1)) ) { serverUpdateEntitySkill(this, 1); // update monsterTarget for player leaders. } @@ -17695,34 +24572,41 @@ void Entity::checkGroundForItems() } // Calls the function for a monster to pick up an item, if it's a monster that picks up items, only if they are not Asleep - if ( myStats->EFFECTS[EFF_ASLEEP] == false ) + if ( !myStats->getEffectActive(EFF_ASLEEP) ) { - switch ( myStats->type ) + if ( monsterCanTradeWith(-1) ) { - case GOBLIN: - case HUMAN: - if ( !strcmp(myStats->name, "") ) - { - //checkBetterEquipment(myStats); - monsterAddNearbyItemToInventory(myStats, 16, 9); - } - break; - case GOATMAN: - //Goatman boss picks up items too. - monsterAddNearbyItemToInventory(myStats, 16, 9); //Replaces checkBetterEquipment(), because more better. Adds items to inventory, and swaps out current equipped with better stuff on the ground. - //checkBetterEquipment(myStats); - break; - case AUTOMATON: - monsterAddNearbyItemToInventory(myStats, 16, 5); - break; - case SHOPKEEPER: - if ( myStats->MISC_FLAGS[STAT_FLAG_MYSTERIOUS_SHOPKEEP] > 0 ) - { - monsterAddNearbyItemToInventory(myStats, 16, 99); - } - break; - default: - return; + // no pickup + } + else + { + switch ( myStats->type ) + { + case GOBLIN: + case HUMAN: + if ( myStats->getAttribute("special_npc") == "" ) + { + //checkBetterEquipment(myStats); + monsterAddNearbyItemToInventory(myStats, 16, 9); + } + break; + case GOATMAN: + //Goatman boss picks up items too. + monsterAddNearbyItemToInventory(myStats, 16, 9); //Replaces checkBetterEquipment(), because more better. Adds items to inventory, and swaps out current equipped with better stuff on the ground. + //checkBetterEquipment(myStats); + break; + case AUTOMATON: + monsterAddNearbyItemToInventory(myStats, 16, 5); + break; + case SHOPKEEPER: + if ( myStats->MISC_FLAGS[STAT_FLAG_MYSTERIOUS_SHOPKEEP] > 0 ) + { + monsterAddNearbyItemToInventory(myStats, 16, 99); + } + break; + default: + return; + } } } } @@ -17893,6 +24777,31 @@ bool Entity::monsterAddNearbyItemToInventory(Stat* myStats, int rangeToFind, int continue; } + bool donationItem = false; + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( players[c]->mechanics.donationRevealedOnFloor > 0 && entity->getUID() == players[c]->mechanics.donationRevealedOnFloor ) + { + donationItem = true; + } + } + + if ( donationItem ) + { + if ( item->interactNPCUid == getUID() ) + { + // item being interacted with, can interact with item. + } + else + { + if ( item != nullptr ) + { + free(item); + } + continue; + } + } + int playerOwned = -1; if ( entity->itemOriginalOwner != 0 ) { @@ -17902,7 +24811,7 @@ bool Entity::monsterAddNearbyItemToInventory(Stat* myStats, int rangeToFind, int { if ( players[c]->entity->getUID() == entity->itemOriginalOwner ) { - if ( players[c]->entity->checkFriend(this) ) + if ( players[c]->entity->checkFriend(this) || (myStats->getEffectActive(EFF_COMMAND) >= 1 && myStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1) ) { // player owned. playerOwned = c; @@ -17917,7 +24826,8 @@ bool Entity::monsterAddNearbyItemToInventory(Stat* myStats, int rangeToFind, int } else if ( (playerOwned >= 0 && (entity->ticks < 5 * TICKS_PER_SECOND - || (monsterAllyPickupItems != ALLY_PICKUP_ALL && monsterAllyIndex >= 0)) + || (monsterAllyPickupItems != ALLY_PICKUP_ALL + && (monsterAllyIndex >= 0 || (myStats->getEffectActive(EFF_COMMAND) >= 1 && myStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1) ) )) ) ) { @@ -18026,11 +24936,22 @@ bool Entity::monsterAddNearbyItemToInventory(Stat* myStats, int rangeToFind, int Entity* dropped = dropItemMonster((*shouldWield), this, myStats); //And I threw it on the ground! if ( dropped && item && item->interactNPCUid == getUID() ) { - if ( monsterAllyIndex >= 0 && monsterAllyIndex < MAXPLAYERS ) + if ( monsterAllyIndex >= 0 && monsterAllyIndex < MAXPLAYERS + || (myStats->getEffectActive(EFF_COMMAND) >= 1 && myStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1) ) { - if ( players[monsterAllyIndex] && players[monsterAllyIndex]->entity ) + if ( (myStats->getEffectActive(EFF_COMMAND) >= 1 && myStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1) ) + { + if ( players[myStats->getEffectActive(EFF_COMMAND) - 1] && players[myStats->getEffectActive(EFF_COMMAND) - 1]->entity ) + { + dropped->itemOriginalOwner = players[myStats->getEffectActive(EFF_COMMAND) - 1]->entity->getUID(); + } + } + else { - dropped->itemOriginalOwner = players[monsterAllyIndex]->entity->getUID(); + if ( players[monsterAllyIndex] && players[monsterAllyIndex]->entity ) + { + dropped->itemOriginalOwner = players[monsterAllyIndex]->entity->getUID(); + } } } } @@ -18278,6 +25199,20 @@ bool Entity::monsterWantsItem(const Item& item, Item**& shouldEquip, node_t*& re { return false; // no want broken. } + if ( item.type == GEM_JEWEL ) + { + if ( myStats->type == AUTOMATON ) + { + if ( this->monsterAllyGetPlayerLeader() ) + { + return false; + } + } + else + { + return false; + } + } switch ( myStats->type ) { @@ -18312,7 +25247,9 @@ bool Entity::monsterWantsItem(const Item& item, Item**& shouldEquip, node_t*& re if ( item.type == ARTIFACT_ORB_BLUE || item.type == ARTIFACT_ORB_GREEN || item.type == ARTIFACT_ORB_RED - || item.type == ARTIFACT_ORB_PURPLE ) + || item.type == ARTIFACT_ORB_PURPLE + || items[item.type].hasAttribute("UNBURNABLE") + || items[item.type].hasAttribute("UNVOIDABLE") ) { return false; } @@ -18615,6 +25552,14 @@ double Entity::monsterRotate() } yaw -= dir / ratio; } + else if ( race == MOTH_SMALL ) + { + yaw -= dir / 16; + } + else if ( race == DUCK_SMALL ) + { + yaw -= dir / 4; + } else if ( race == DUMMYBOT ) { yaw -= dir / 4; @@ -18713,16 +25658,47 @@ Item* Entity::getBestShieldIHave() const return currentBest; } -void Entity::degradeArmor(Stat& hitstats, Item& armor, int armornum) +//static int broken = 0; +//static ConsoleCommand ccmd_hiteverything("/hiteverything", "", [](int argc, const char** argv) { +// for ( node_t* node = map.entities->first; node; node = node->next ) +// { +// if ( Entity* entity = (Entity*)node->element ) +// { +// if ( entity != players[0]->entity ) +// { +// if ( entity->behavior == &actMonster ) +// { +// if ( players[0]->entity ) +// { +// if ( Stat* myStats = entity->getStats() ) +// { +// while ( myStats->HP > 0 ) +// { +// int prevHP = myStats->HP; +// players[0]->entity->attack(0, 0, entity); +// if ( prevHP == myStats->HP ) +// { +// break; +// } +// } +// } +// } +// } +// } +// } +// } +//}); + +bool Entity::degradeArmor(Stat& hitstats, Item& armor, int armornum) { if ( hitstats.type == SHADOW || hitstats.type == LICH || hitstats.type == LICH_FIRE || hitstats.type == LICH_ICE ) { - return; //Shadows' armor and shields don't break. + return false; //Shadows' armor and shields don't break. } if ( hitstats.type == SKELETON && behavior == &actMonster && monsterAllySummonRank > 0 ) { - return; // conjured skeleton armor doesn't break. + return false; // conjured skeleton armor doesn't break. } if ( armor.type == ARTIFACT_BOOTS @@ -18733,13 +25709,13 @@ void Entity::degradeArmor(Stat& hitstats, Item& armor, int armornum) || armor.type == ARTIFACT_BREASTPIECE || armor.type == MASK_ARTIFACT_VISOR ) { - return; + return false; } if ( itemTypeIsQuiver(armor.type) ) { // quivers don't break. - return; + return false; } int playerhit = -1; @@ -18749,6 +25725,22 @@ void Entity::degradeArmor(Stat& hitstats, Item& armor, int armornum) playerhit = this->skill[2]; } + if ( hitstats.shield == &armor && itemCategory(&armor) != SPELLBOOK + && hitstats.getEffectActive(EFF_FORCE_SHIELD) > 0 ) + { + /*hitstats.setEffectActive(EFF_FORCE_SHIELD, hitstats.getEffectActive(EFF_FORCE_SHIELD) - (Uint8)1); + if ( !hitstats.getEffectActive(EFF_FORCE_SHIELD) ) + { + setEffect(EFF_FORCE_SHIELD, false, 0, true); + }*/ + return false; + } + + if ( spellEffectPreserveItem(&armor) ) + { + return false; + } + if ( armor.type == TOOL_TORCH && armor.count > 1 && playerhit >= 0 && &armor == stats[playerhit]->shield ) { std::string itemName = armor.getName(); @@ -18791,7 +25783,7 @@ void Entity::degradeArmor(Stat& hitstats, Item& armor, int armornum) Compendium_t::Events_t::eventUpdate(playerhit, Compendium_t::CPDM_BROKEN_BY_BLOCKING, armor.type, 1); } Compendium_t::Events_t::eventUpdate(playerhit, Compendium_t::CPDM_BROKEN, armor.type, 1); - return; + return true; } if ( (playerhit >= 0 && players[playerhit]->isLocalPlayer()) || playerhit < 0 ) @@ -18850,11 +25842,13 @@ void Entity::degradeArmor(Stat& hitstats, Item& armor, int armornum) strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = armornum; net_packet->data[5] = armor.status; + SDLNet_Write16((int)armor.type, &net_packet->data[6]); net_packet->address.host = net_clients[playerhit - 1].host; net_packet->address.port = net_clients[playerhit - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, playerhit - 1); } + return true; } void Entity::removeLightField() @@ -18877,19 +25871,23 @@ bool Entity::shouldRetreat(Stat& myStats) // retreating monsters will not try path when losing sight of target - if ( myStats.EFFECTS[EFF_PACIFY] || myStats.EFFECTS[EFF_FEAR] ) + if ( myStats.getEffectActive(EFF_PACIFY) || myStats.getEffectActive(EFF_FEAR) || myStats.getEffectActive(EFF_COWARDICE) ) + { + return true; + } + if ( myStats.getEffectActive(EFF_KNOCKBACK) ) { return true; } - if ( myStats.EFFECTS[EFF_KNOCKBACK] ) + if ( myStats.type == MINIMIMIC ) { return true; } - if ( (myStats.EFFECTS[EFF_DASH] || (myStats.weapon && myStats.weapon->type == SPELLBOOK_DASH)) && behavior == &actMonster ) + if ( (myStats.getEffectActive(EFF_DASH) || (myStats.weapon && myStats.weapon->type == SPELLBOOK_DASH)) && behavior == &actMonster ) { return false; } - if ( myStats.EFFECTS[EFF_ROOTED] ) + if ( myStats.getEffectActive(EFF_ROOTED) ) { return false; } @@ -18909,6 +25907,18 @@ bool Entity::shouldRetreat(Stat& myStats) { return false; } + else if ( myStats.type == MYCONID ) + { + return false; + } + else if ( myStats.type == MONSTER_ADORCISED_WEAPON + || myStats.type == REVENANT_SKULL + || myStats.type == FLAME_ELEMENTAL + || myStats.type == EARTH_ELEMENTAL + || myStats.type == MOTH_SMALL ) + { + return false; + } else if ( myStats.type == GNOME ) { if ( myStats.getAttribute("gnome_type").find("_melee") != std::string::npos ) @@ -18942,7 +25952,7 @@ bool Entity::shouldRetreat(Stat& myStats) { if ( Stat* targetStats = target->getStats() ) { - if ( targetStats->EFFECTS[EFF_WEBBED] ) + if ( targetStats->getEffectActive(EFF_WEBBED) ) { return false; } @@ -18982,6 +25992,11 @@ bool Entity::shouldRetreat(Stat& myStats) return false; } + if ( myStats.getEffectActive(EFF_COURAGE) ) + { + return false; + } + if ( myStats.MAXHP >= 100 ) { if ( myStats.HP <= myStats.MAXHP / 8 && this->getCHR() >= -2 ) @@ -19000,23 +26015,27 @@ bool Entity::shouldRetreat(Stat& myStats) bool Entity::backupWithRangedWeapon(Stat& myStats, int dist, int hasrangedweapon) { int distanceLimit = 100; - if ( hasrangedweapon && myStats.weapon ) + if ( hasrangedweapon ) { - if ( distanceLimit >= getMonsterEffectiveDistanceOfRangedWeapon(myStats.weapon) ) + if ( myStats.weapon || (myStats.type == DRYAD && myStats.getAttribute("monster_d_type") == "watcher") ) { - distanceLimit = getMonsterEffectiveDistanceOfRangedWeapon(myStats.weapon) - 20; + if ( distanceLimit >= getMonsterEffectiveDistanceOfRangedWeapon(myStats.weapon) ) + { + distanceLimit = getMonsterEffectiveDistanceOfRangedWeapon(myStats.weapon) - 20; + } } } + if ( dist >= distanceLimit || !hasrangedweapon ) { return false; } - if ( (myStats.EFFECTS[EFF_DASH] || (myStats.weapon && myStats.weapon->type == SPELLBOOK_DASH)) && behavior == &actMonster ) + if ( (myStats.getEffectActive(EFF_DASH) || (myStats.weapon && myStats.weapon->type == SPELLBOOK_DASH)) && behavior == &actMonster ) { return false; } - if ( myStats.EFFECTS[EFF_ROOTED] ) + if ( myStats.getEffectActive(EFF_ROOTED) ) { return false; } @@ -19028,11 +26047,15 @@ bool Entity::backupWithRangedWeapon(Stat& myStats, int dist, int hasrangedweapon { return false; } - if ( monsterIsImmobileTurret(this, &myStats) ) + if ( monsterIsImmobileTurret(this, &myStats) || myStats.type == MONSTER_ADORCISED_WEAPON ) + { + return false; + } + if ( myStats.type == VAMPIRE && (monsterSpecialState > 0 || (myStats.getAttribute("special_npc") == "bram kindly")) ) { return false; } - if ( myStats.type == VAMPIRE && (monsterSpecialState > 0 || MonsterData_t::nameMatchesSpecialNPCName(myStats, "bram kindly")) ) + if ( myStats.getEffectActive(EFF_COURAGE) ) { return false; } @@ -19125,7 +26148,7 @@ void Entity::playerStatIncrease(int playerClass, int chosenStats[3]) //{ // messagePlayer(0, "%2d, ", *i); //} - if ( behavior == &actPlayer && playerClass == CLASS_SHAMAN && stats[skill[2]] ) + if ( behavior == &actPlayer /*&& playerClass == CLASS_SHAMAN*/ && stats[skill[2]] ) { if ( stats[skill[2]]->type == RAT ) { @@ -19145,7 +26168,7 @@ void Entity::playerStatIncrease(int playerClass, int chosenStats[3]) else if ( stats[skill[2]]->type == CREATURE_IMP ) { // STR DEX CON INT PER CHR - statWeights = { 1, 3, 1, 6, 1, 1 }; + statWeights = { 1, 1, 1, 3, 1, 6 }; } } @@ -19165,6 +26188,77 @@ void Entity::playerStatIncrease(int playerClass, int chosenStats[3]) statWeights[STAT_INT] = 0; } } + if ( behavior == &actPlayer ) + { + if ( stat->type == MYCONID ) + { + for ( int i = 0; i < NUMSTATS; ++i ) + { + if ( i != STAT_DEX ) + { + statWeights[i] *= 3; + } + else + { + if ( stat->DEX < 5 ) + { + statWeights[i] *= 3; + } + } + } + } + if ( stat->type == GNOME ) + { + for ( int i = 0; i < NUMSTATS; ++i ) + { + if ( i != STAT_STR ) + { + statWeights[i] *= 3; + } + else + { + if ( stat->STR < 5 ) + { + statWeights[i] *= 3; + } + } + } + } + if ( stat->type == GREMLIN ) + { + for ( int i = 0; i < NUMSTATS; ++i ) + { + if ( i != STAT_CON ) + { + statWeights[i] *= 3; + } + else + { + if ( stat->CON < 5 ) + { + statWeights[i] *= 3; + } + } + } + } + if ( stat->type == DRYAD ) + { + for ( int i = 0; i < NUMSTATS; ++i ) + { + if ( i != STAT_CHR ) + { + statWeights[i] *= 3; + } + else + { + if ( stat->CHR < 5 ) + { + statWeights[i] *= 3; + } + } + } + } + } } chosenStats[0] = local_rng.rand() % 6; // get first stat randomly. statWeights[chosenStats[0]] = 0; // remove the chance of the local stat vector. @@ -19200,6 +26294,7 @@ void Entity::createPathBoundariesNPC(int maxTileDistance) if ( myStats->MISC_FLAGS[STAT_FLAG_NPC] != 0 || myStats->type == SHOPKEEPER + || monsterCanTradeWith(-1) || monsterAllyState == ALLY_STATE_DEFEND ) { // is NPC, find the bounds which movement is restricted to by finding the "box" it spawned in. @@ -19446,10 +26541,14 @@ node_t* Entity::chooseAttackSpellbookFromInventory() int Entity::getManaringFromEffects(Entity* my, Stat& myStats) { int manaring = 0; - if ( myStats.EFFECTS[EFF_MP_REGEN] && myStats.type != AUTOMATON ) + if ( myStats.getEffectActive(EFF_MP_REGEN) && myStats.type != AUTOMATON ) { manaring += 2; } + if ( myStats.getEffectActive(EFF_RATION_SWEET) ) + { + manaring += 1; + } return manaring; } @@ -19515,9 +26614,9 @@ int Entity::getManaringFromEquipment(Entity* my, Stat& myStats, bool isPlayer) return manaring; } -int Entity::getManaRegenInterval(Entity* my, Stat& myStats, bool isPlayer) +int Entity::getManaRegenInterval(Entity* my, Stat& myStats, bool isPlayer, bool excludeItemsEffectsBonus) { - int regenTime = getBaseManaRegen(my, myStats); + int regenTime = getBaseManaRegen(my, myStats, excludeItemsEffectsBonus); int manaring = 0; if ( isPlayer && myStats.type != HUMAN ) { @@ -19528,20 +26627,23 @@ int Entity::getManaRegenInterval(Entity* my, Stat& myStats, bool isPlayer) } int bonusManaring = 0; - bonusManaring += Entity::getManaringFromEquipment(my, myStats, true); - bonusManaring += Entity::getManaringFromEffects(my, myStats); + if ( !excludeItemsEffectsBonus ) + { + bonusManaring += Entity::getManaringFromEquipment(my, myStats, true); + bonusManaring += Entity::getManaringFromEffects(my, myStats); + } manaring += bonusManaring; if ( my && bonusManaring >= 2 && ::ticks % TICKS_PER_SECOND == 0 && isPlayer ) { - bool oldRegen = myStats.EFFECTS[EFF_MP_REGEN]; - myStats.EFFECTS[EFF_MP_REGEN] = false; + Uint8 oldRegen = myStats.getEffectActive(EFF_MP_REGEN); + myStats.clearEffect(EFF_MP_REGEN); int bonusManaringNoRegen = Entity::getManaringFromEquipment(my, myStats, true) + Entity::getManaringFromEffects(my, myStats); if ( bonusManaringNoRegen >= 2 ) { steamAchievementEntity(my, "BARONY_ACH_ARCANE_LINK"); } - myStats.EFFECTS[EFF_MP_REGEN] = oldRegen; + myStats.setEffectValueUnsafe(EFF_MP_REGEN, oldRegen); } if ( manaring > 3 ) @@ -19580,17 +26682,63 @@ int Entity::getManaRegenInterval(Entity* my, Stat& myStats, bool isPlayer) return floatRegenTime; } - if ( manaring > 0 ) + if ( !isPlayer ) { - return regenTime / (manaring * 2); // 1 MP each 6 seconds base - } - else if ( manaring < 0 ) - { - return regenTime * abs(manaring) * 4; // 1 MP each 24 seconds if negative regen + if ( manaring > 0 ) + { + return regenTime / (manaring * 2); // 1 MP each 6 seconds base + } + else if ( manaring < 0 ) + { + return regenTime * abs(manaring) * 4; // 1 MP each 24 seconds if negative regen + } + else if ( manaring == 0 ) + { + return regenTime; + } } - else if ( manaring == 0 ) + else { - return regenTime; + int player = -1; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( stats[i] == &myStats ) + { + player = i; + break; + } + } + if ( player >= 0 ) + { + real_t regenPerMinute = 60 * TICKS_PER_SECOND / (real_t)(regenTime); + + if ( !excludeItemsEffectsBonus ) + { + if ( stats[player]->type == DRYAD ) + { + int bonus = 0; + if ( !stats[player]->helmet && stats[player]->getEffectActive(EFF_GROWTH) > 1 ) + { + bonus = std::min(3, stats[player]->getEffectActive(EFF_GROWTH) - 1); + } + regenPerMinute *= 1.0 + std::max(0, (bonus)) * 0.1; + } + } + + if ( manaring > 0 ) + { + regenPerMinute *= (abs(manaring) + 1); + } + else if ( manaring < 0 ) + { + regenPerMinute /= abs(manaring) * 4.0; + } + + const int regenTicks = TICKS_PER_SECOND * 60 / regenPerMinute; + int regenCap = 1 * TICKS_PER_SECOND; + + return std::max(regenCap, regenTicks); + } } return MAGIC_REGEN_TIME; } @@ -19598,9 +26746,9 @@ int Entity::getManaRegenInterval(Entity* my, Stat& myStats, bool isPlayer) int Entity::getHealringFromEffects(Entity* my, Stat& myStats) { double healring = 0; - if ( myStats.EFFECTS[EFF_HP_REGEN] ) + if ( myStats.getEffectActive(EFF_HP_REGEN) ) { - if ( my && my->monsterAllyGetPlayerLeader() && my->monsterAllySpecial == ALLY_SPECIAL_CMD_REST && myStats.EFFECTS[EFF_ASLEEP] ) + if ( my && my->monsterAllyGetPlayerLeader() && my->monsterAllySpecial == ALLY_SPECIAL_CMD_REST && myStats.getEffectActive(EFF_ASLEEP) ) { healring += 1; } @@ -19609,7 +26757,11 @@ int Entity::getHealringFromEffects(Entity* my, Stat& myStats) healring += 2; } } - if ( myStats.EFFECTS[EFF_TROLLS_BLOOD] ) + if ( myStats.getEffectActive(EFF_TROLLS_BLOOD) ) + { + healring += 1; + } + if ( myStats.getEffectActive(EFF_RATION_SWEET) ) { healring += 1; } @@ -19674,51 +26826,55 @@ int Entity::getHealringFromEquipment(Entity* my, Stat& myStats, bool isPlayer) return healring; } -int Entity::getHealthRegenInterval(Entity* my, Stat& myStats, bool isPlayer) +int Entity::getHealthRegenInterval(Entity* my, Stat& myStats, bool isPlayer, bool excludeItemsEffectsBonus) { - if ( myStats.EFFECTS[EFF_VAMPIRICAURA] ) + if ( !excludeItemsEffectsBonus ) { - if ( isPlayer && myStats.EFFECTS_TIMERS[EFF_VAMPIRICAURA] > 0 ) + if ( myStats.getEffectActive(EFF_VAMPIRICAURA) ) { - return -1; + if ( isPlayer && myStats.EFFECTS_TIMERS[EFF_VAMPIRICAURA] > 0 ) + { + return -1; + } } - } - if ( myStats.HP <= 0 ) - { - return -1; - } - if ( myStats.breastplate && myStats.breastplate->type == VAMPIRE_DOUBLET ) - { - return -1; - } + if ( myStats.HP <= 0 ) + { + return -1; + } - if ( svFlags & SV_FLAG_HUNGER ) - { - if ( isPlayer ) + if ( myStats.breastplate && myStats.breastplate->type == VAMPIRE_DOUBLET ) + { + return -1; + } + if ( svFlags & SV_FLAG_HUNGER ) { - if ( myStats.HUNGER <= 0 ) + if ( isPlayer ) { - bool doStarvation = true; - if ( myStats.type == AUTOMATON ) + if ( myStats.HUNGER <= 0 ) { - if ( myStats.MP > 0 ) + bool doStarvation = true; + if ( myStats.type == AUTOMATON ) + { + if ( myStats.MP > 0 ) + { + doStarvation = false; + } + } + else if ( myStats.type == SKELETON ) { doStarvation = false; } - } - else if ( myStats.type == SKELETON ) - { - doStarvation = false; - } - if ( doStarvation ) - { - return -1; + if ( doStarvation ) + { + return -1; + } } } } } + double healring = 0; double bonusHealring = 0.0; if ( myStats.type == SKELETON && isPlayer ) @@ -19727,30 +26883,36 @@ int Entity::getHealthRegenInterval(Entity* my, Stat& myStats, bool isPlayer) } if ( !(svFlags & SV_FLAG_HUNGER) && isPlayer ) { - bonusHealring += Entity::getHealringFromEquipment(my, myStats, isPlayer); - bonusHealring += Entity::getHealringFromEffects(my, myStats); - if ( bonusHealring < 0.01 && myStats.type != SKELETON ) + if ( !excludeItemsEffectsBonus ) { - return -1; + bonusHealring += Entity::getHealringFromEquipment(my, myStats, isPlayer); + bonusHealring += Entity::getHealringFromEffects(my, myStats); + if ( bonusHealring < 0.01 && myStats.type != SKELETON ) + { + return -1; + } } } else { - bonusHealring += Entity::getHealringFromEquipment(my, myStats, isPlayer); - bonusHealring += Entity::getHealringFromEffects(my, myStats); + if ( !excludeItemsEffectsBonus ) + { + bonusHealring += Entity::getHealringFromEquipment(my, myStats, isPlayer); + bonusHealring += Entity::getHealringFromEffects(my, myStats); + } } healring += bonusHealring; if ( my && bonusHealring >= 2.0 && ::ticks % TICKS_PER_SECOND == 0 && isPlayer ) { - bool oldRegen = myStats.EFFECTS[EFF_HP_REGEN]; - myStats.EFFECTS[EFF_HP_REGEN] = false; + Uint8 oldRegen = myStats.getEffectActive(EFF_HP_REGEN); + myStats.clearEffect(EFF_HP_REGEN); int bonusHealringNoRegen = Entity::getHealringFromEquipment(my, myStats, isPlayer) + Entity::getHealringFromEffects(my, myStats); if ( bonusHealringNoRegen >= 2 ) { steamAchievementEntity(my, "BARONY_ACH_TROLLS_BLOOD"); } - myStats.EFFECTS[EFF_HP_REGEN] = oldRegen; + myStats.setEffectValueUnsafe(EFF_HP_REGEN, oldRegen); } if ( healring > 3 ) @@ -19763,45 +26925,123 @@ int Entity::getHealthRegenInterval(Entity* my, Stat& myStats, bool isPlayer) healring = 25; // these guys like regenerating } - if ( healring > 0 ) + if ( !isPlayer ) { - if ( !(svFlags & SV_FLAG_HUNGER) && isPlayer ) + if ( healring > 0 ) + { + if ( !(svFlags & SV_FLAG_HUNGER) && isPlayer ) + { + return (HEAL_TIME / (healring * 4)); // 1 HP each 12 sec base + } + else + { + return (HEAL_TIME / (healring * 6)); // 1 HP each 12 sec base + } + } + else if ( healring < 0 ) { - return (HEAL_TIME / (healring * 4)); // 1 HP each 12 sec base + return (abs(healring) * HEAL_TIME * 4); // 1 HP each 48 sec if negative regen } else { - return (HEAL_TIME / (healring * 6)); // 1 HP each 12 sec base + return HEAL_TIME; } } - else if ( healring < 0 ) - { - return (abs(healring) * HEAL_TIME * 4); // 1 HP each 48 sec if negative regen - } - else if ( healring == 0 ) + else { - return HEAL_TIME; + int player = -1; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( stats[i] == &myStats ) + { + player = i; + break; + } + } + if ( player >= 0 ) + { + real_t regenPerMinute = 60 * TICKS_PER_SECOND / (real_t)(HEAL_TIME); + auto& baseGrowths = ClassBaseGrowths::getClassBaseGrowths(client_classes[player]); + real_t factor = ClassBaseGrowths::hpRegenFactor; + static ConsoleVariable cvar_class_hp_regen_factor("/class_hp_regen_factor", 1.0); + if ( svFlags & SV_FLAG_CHEATS ) + { + factor *= *cvar_class_hp_regen_factor; + } + regenPerMinute += baseGrowths.baseRegenHP * std::max(0, (myStats.LVL - 1)) * factor; + for ( int i = 0; i < NUMSTATS; ++i ) + { + if ( ClassBaseGrowths::statRegenHP[i] > 0.0 ) + { + Sint32 statVal = 0; + switch ( i ) + { + case STAT_STR: + statVal = std::max(statGetSTR(&myStats, my), 0); + break; + case STAT_DEX: + statVal = std::max(statGetDEX(&myStats, my), 0); + break; + case STAT_CON: + statVal = std::max(statGetCON(&myStats, my), 0); + break; + case STAT_INT: + statVal = std::max(statGetINT(&myStats, my), 0); + break; + case STAT_PER: + statVal = std::max(statGetPER(&myStats, my), 0); + break; + case STAT_CHR: + statVal = std::max(statGetCHR(&myStats, my), 0); + break; + default: + break; + } + regenPerMinute += statVal * ClassBaseGrowths::statRegenHP[i]; + } + } + if ( healring > 0 ) + { + regenPerMinute *= (abs(healring) + 1); + } + else if ( healring < 0 ) + { + regenPerMinute /= abs(healring) * 4.0; + } + + const int regenTicks = TICKS_PER_SECOND * 60 / regenPerMinute; + int regenCap = 2 * TICKS_PER_SECOND; + if ( healring > 0 ) + { + regenCap = 1 * TICKS_PER_SECOND; + } + return std::max(regenCap, regenTicks); + } } return HEAL_TIME; } -int getBaseManaRegen(Entity* my, Stat& myStats) +int getBaseManaRegen(Entity* my, Stat& myStats, bool excludeItemsEffectsBonus) { // reduced time from intelligence and spellcasting ability, 0-200 ticks of 300. - int profMultiplier = (myStats.getModifiedProficiency(PRO_SPELLCASTING) / 20) + 1; // 1 to 6 - int statMultiplier = std::max(statGetINT(&myStats, my), 0); // get intelligence + //int profMultiplier = (myStats.getModifiedProficiency(PRO_LEGACY_SPELLCASTING) / 20) + 1; // 1 to 6 + //int statMultiplier = std::max(statGetINT(&myStats, my), 0); // get intelligence if ( myStats.type == AUTOMATON ) { - return MAGIC_REGEN_TIME; + return MAGIC_REGEN_AUTOMATON_TIME; } - int multipliedTotal = profMultiplier * statMultiplier; + //int multipliedTotal = profMultiplier * statMultiplier; - if ( myStats.weapon && myStats.weapon->type == ARTIFACT_MACE ) + real_t statMultiplier = 1.0; + real_t perMinModifier = 0.0; + if ( myStats.weapon && myStats.weapon->type == ARTIFACT_MACE && !excludeItemsEffectsBonus ) { real_t amount = 0.0; getArtifactWeaponEffectChance(myStats.weapon->type, myStats, &amount); - multipliedTotal += amount; + amount /= 100.0; + statMultiplier += 4 * amount; // stats count for 5x + perMinModifier = 3 * amount; // extra 3 MP per min } // unused - this is never hit by insectoid mana regen, old code @@ -19819,7 +27059,67 @@ int getBaseManaRegen(Entity* my, Stat& myStats) // return (base - static_cast(std::min(multipliedTotal, 100))); // return 100-33 ticks, 2-0.67 seconds. //} - return (MAGIC_REGEN_TIME - static_cast(std::min(multipliedTotal, 200))); // return 300-100 ticks, 6-2 seconds. + int player = -1; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( stats[i] == &myStats ) + { + player = i; + break; + } + } + + if ( player >= 0 ) + { + real_t regenPerMinute = 60 * TICKS_PER_SECOND / (real_t)(MAGIC_REGEN_TIME); + auto& baseGrowths = ClassBaseGrowths::getClassBaseGrowths(client_classes[player]); + real_t factor = ClassBaseGrowths::mpRegenFactor; + static ConsoleVariable cvar_class_mp_regen_factor("/class_mp_regen_factor", 1.0); + if ( svFlags & SV_FLAG_CHEATS ) + { + factor *= *cvar_class_mp_regen_factor; + } + regenPerMinute += baseGrowths.baseRegenMP * std::max(0, (myStats.LVL - 1)) * factor; + for ( int i = 0; i < NUMSTATS; ++i ) + { + if ( ClassBaseGrowths::statRegenMP[i] > 0.0 ) + { + Sint32 statVal = 0; + switch ( i ) + { + case STAT_STR: + statVal = std::max(statGetSTR(&myStats, my), 0); + break; + case STAT_DEX: + statVal = std::max(statGetDEX(&myStats, my), 0); + break; + case STAT_CON: + statVal = std::max(statGetCON(&myStats, my), 0); + break; + case STAT_INT: + statVal = std::max(statGetINT(&myStats, my), 0); + break; + case STAT_PER: + statVal = std::max(statGetPER(&myStats, my), 0); + break; + case STAT_CHR: + statVal = std::max(statGetCHR(&myStats, my), 0); + break; + default: + break; + } + regenPerMinute += statVal * ClassBaseGrowths::statRegenMP[i] * statMultiplier; + } + } + regenPerMinute += perMinModifier; + + const int regenTicks = TICKS_PER_SECOND * 60 / regenPerMinute; + int regenCap = 1 * TICKS_PER_SECOND; + + return std::max(regenCap, regenTicks); + } + + return MAGIC_REGEN_TIME; } void Entity::setRangedProjectileAttack(Entity& marksman, Stat& myStats, int optionalOverrideForArrowType) @@ -19843,7 +27143,11 @@ void Entity::setRangedProjectileAttack(Entity& marksman, Stat& myStats, int opti if ( myStats.weapon->type != SLING ) { // get armor pierce chance. - int statChance = std::min(std::max(marksman.getPER() / 2, 0), 50); // 0 to 50 value. + int statChance = std::min(std::max(marksman.getPER(), 0), 50); // 0 to 50 value. + if ( behavior == &actMonster ) + { + statChance = std::min(std::max(marksman.getPER() / 2, 0), 50); + } if ( myStats.weapon->type == HEAVY_CROSSBOW ) { statChance += 50; @@ -19950,13 +27254,39 @@ void Entity::setRangedProjectileAttack(Entity& marksman, Stat& myStats, int opti case QUIVER_HUNTING: sprite = 930; break; + case QUIVER_BONE: + sprite = 2304; + break; + case QUIVER_BLACKIRON: + sprite = 2305; + break; default: break; } } + if ( myStats.weapon ) + { + if ( myStats.weapon->type == BRANCH_BOW ) + { + if ( local_rng.rand() % 20 < 3 ) + { + sprite = 1881; // root seed + this->arrowQuiverType = 0; + } + } + else if ( myStats.weapon->type == BRANCH_BOW_INFECTED ) + { + if ( local_rng.rand() % 20 < 6 ) + { + sprite = 1882; // poison seed + this->arrowQuiverType = 0; + } + } + } + // get arrow power. - attack += marksman.getRangedAttack(); + attack = marksman.getRangedAttack(attack); real_t variance = 20; real_t baseSkillModifier = 50.0; // 40-60 base real_t skillModifier = baseSkillModifier - (variance / 2) + (myStats.getModifiedProficiency(PRO_RANGED) / 2.0); @@ -19973,6 +27303,7 @@ bool Entity::setArrowProjectileProperties(int weaponType) { return false; } + if ( multiplayer == CLIENT && weaponType == TOOL_SENTRYBOT ) { // hack for arrow traps. @@ -19982,7 +27313,8 @@ bool Entity::setArrowProjectileProperties(int weaponType) return true; } - if ( weaponType == CROSSBOW || weaponType == SLING || weaponType == HEAVY_CROSSBOW ) + if ( weaponType == CROSSBOW || weaponType == SLING || weaponType == HEAVY_CROSSBOW + || weaponType == BLACKIRON_CROSSBOW ) { this->vel_z = -0.2; this->arrowSpeed = 6; @@ -20007,13 +27339,14 @@ bool Entity::setArrowProjectileProperties(int weaponType) { this->vel_z = -0.6; this->arrowFallSpeed = 0.08; - if ( weaponType == SHORTBOW || weaponType == COMPOUND_BOW || weaponType == ARTIFACT_BOW ) + if ( weaponType == SHORTBOW || weaponType == COMPOUND_BOW || weaponType == ARTIFACT_BOW + || weaponType == BONE_SHORTBOW ) { this->arrowSpeed = 7; this->vel_z = -0.6; this->arrowFallSpeed = 0.08; } - else if ( weaponType == LONGBOW ) + else if ( weaponType == LONGBOW || weaponType == BRANCH_BOW || weaponType == BRANCH_BOW_INFECTED ) { this->arrowSpeed = 8; this->vel_z = -0.4; @@ -20043,7 +27376,7 @@ bool Entity::setArrowProjectileProperties(int weaponType) * Entities with Stats will have their fire time (char_fire) and chance to stop being on fire (chanceToPutOutFire) reduced by their CON * Calculations for reductions is outlined in this function */ -void Entity::SetEntityOnFire(Entity* sourceOfFire) +bool Entity::SetEntityOnFire(Entity* sourceOfFire) { // Check if the Entity can be set on fire if ( this->flags[BURNABLE] ) @@ -20055,15 +27388,20 @@ void Entity::SetEntityOnFire(Entity* sourceOfFire) { if ( myStats->type == SKELETON ) { - return; + return false; } if ( myStats->type == AUTOMATON ) { - return; + return false; } if ( myStats->breastplate && myStats->breastplate->type == MACHINIST_APRON ) { - return; + return false; + } + if ( myStats->amulet && myStats->amulet->type == AMULET_BURNINGRESIST ) + { + this->degradeAmuletProc(myStats, AMULET_BURNINGRESIST); + return false; } } } @@ -20092,12 +27430,19 @@ void Entity::SetEntityOnFire(Entity* sourceOfFire) // If the Entity is not a Monster, it wont have Stats, end here if ( this->getStats() == nullptr ) { - return; // The Entity was set on fire, it does not have Stats, so it is on fire for maximum duration + return true; // The Entity was set on fire, it does not have Stats, so it is on fire for maximum duration } if ( this->behavior == &actPlayer ) { - messagePlayerColor(this->skill[2], MESSAGE_COMBAT, makeColorRGB(255, 0, 0), Language::get(4324)); + if ( getStats() && getStats()->getEffectActive(EFF_FLAME_CLOAK) ) + { + messagePlayerColor(this->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6860)); + } + else + { + messagePlayerColor(this->skill[2], MESSAGE_COMBAT, makeColorRGB(255, 0, 0), Language::get(4324)); + } } // Determine decrease in time on fire based on the Entity's CON @@ -20106,7 +27451,7 @@ void Entity::SetEntityOnFire(Entity* sourceOfFire) // If the Entity's CON is <= 1 then their time is just MAX_TICKS_ON_FIRE if ( entityCON <= 1 ) { - return; // The Entity was set on fire, with maximum duration and chance + return true; // The Entity was set on fire, with maximum duration and chance } // If the Entity's CON is <= 4 then their chance is just MAX_CHANCE_STOP_FIRE @@ -20142,11 +27487,11 @@ void Entity::SetEntityOnFire(Entity* sourceOfFire) } } - return; // The Entity was set on fire, with a reduced duration + return true; // The Entity was set on fire, with a reduced duration } } - return; // The Entity can/should not be set on fire + return false; // The Entity can/should not be set on fire } /*------------------------------------------------------------------------------- @@ -20358,6 +27703,10 @@ char const * playerClassLangEntry(int classnum, int playernum) { return Language::get(1900 + classnum); } + else if ( classnum >= CLASS_BARD && classnum <= CLASS_PALADIN ) + { + return Language::get(6784 + classnum - CLASS_BARD); + } else if ( classnum >= CLASS_CONJURER ) { return Language::get(3223 + classnum - CLASS_CONJURER); @@ -20372,6 +27721,21 @@ char const * playerClassLangEntry(int classnum, int playernum) } } +void Entity::setTorsoLimbOffset(Entity* torso) +{ + int monster = getMonsterTypeFromSprite(); + if ( int resultMonsterSprite = EquipmentModelOffsets.modelOffsetExists(monster, torso->sprite, sprite) ) + { + auto& entry = EquipmentModelOffsets.getModelOffset(resultMonsterSprite, torso->sprite); + torso->focalx += entry.focalx; + torso->focaly += entry.focaly; + torso->focalz += entry.focalz; + torso->scalex += entry.scalex; + torso->scaley += entry.scaley; + torso->scalez += entry.scalez; + } +} + /*------------------------------------------------------------------------------- setHelmetLimbOffset @@ -20386,9 +27750,9 @@ void Entity::setHelmetLimbOffset(Entity* helm) helm->scalez = 1.01; // for non-armor helmets, they are rotated so focaly acts as up/down postion. int monster = getMonsterTypeFromSprite(); - if ( EquipmentModelOffsets.modelOffsetExists(monster, helm->sprite) ) + if ( int resultMonsterSprite = EquipmentModelOffsets.modelOffsetExists(monster, helm->sprite, sprite) ) { - auto& entry = EquipmentModelOffsets.getModelOffset(monster, helm->sprite); + auto& entry = EquipmentModelOffsets.getModelOffset(resultMonsterSprite, helm->sprite); helm->focalx = limbs[monster][entry.limbsIndex][0] + entry.focalx; helm->focaly = limbs[monster][entry.limbsIndex][1] + entry.focaly; helm->focalz = limbs[monster][entry.limbsIndex][2] + entry.focalz; @@ -20443,7 +27807,7 @@ void Entity::setHelmetLimbOffset(Entity* helm) helm->roll = PI / 2; } else if ( (helm->sprite >= items[HAT_HOOD].index && helm->sprite < items[HAT_HOOD].index + items[HAT_HOOD].variations) - || helm->sprite == items[HAT_HOOD_RED].index || helm->sprite == items[HAT_HOOD_SILVER].index + || helm->sprite == items[HAT_HOOD_RED].index || helm->sprite == items[HAT_HOOD_SILVER].index || helm->sprite == items[PUNISHER_HOOD].index || (helm->sprite >= items[HAT_HOOD_APPRENTICE].index && helm->sprite < items[HAT_HOOD_APPRENTICE].index + items[HAT_HOOD_APPRENTICE].variations) || (helm->sprite >= items[HAT_HOOD_ASSASSIN].index && helm->sprite < items[HAT_HOOD_ASSASSIN].index + items[HAT_HOOD_ASSASSIN].variations) @@ -20922,6 +28286,7 @@ void Entity::addToWorldUIList(list_t *list) } } +real_t Entity::magicResistancePerPoint = 0.3; int Entity::getMagicResistance(Stat* myStats) { int resistance = 0; @@ -20955,11 +28320,11 @@ int Entity::getMagicResistance(Stat* myStats) resistance += 1; } } - if ( myStats->EFFECTS[EFF_MAGICRESIST] ) + if ( myStats->getEffectActive(EFF_MAGICRESIST) ) { resistance += 1; } - if ( myStats->EFFECTS[EFF_SHRINE_BLUE_BUFF] ) + if ( myStats->getEffectActive(EFF_SHRINE_BLUE_BUFF) ) { resistance += 1; } @@ -21042,6 +28407,7 @@ bool monsterNameIsGeneric(Stat& monsterStats) || strstr(monsterStats.name, "damaged") || strstr(monsterStats.name, "corrupted") || strstr(monsterStats.name, "cultist") + || strstr(monsterStats.name, "encased") || strstr(monsterStats.name, "knight") || strstr(monsterStats.name, "sentinel") || strstr(monsterStats.name, "mage") @@ -21050,8 +28416,10 @@ bool monsterNameIsGeneric(Stat& monsterStats) || strstr(monsterStats.name, "Training") || strstr(monsterStats.name, "Mysterious") || strstr(monsterStats.name, "shaman") + || !strcmp(monsterStats.name, Language::get(6807)) // revenant skeleton || !strcmp(monsterStats.name, Language::get(6302)) // gnome thief || !strcmp(monsterStats.name, Language::get(6303)) // gnome thief leader + || MonsterData_t::nameMatchesSpecialNPCName(monsterStats, "fire sprite") || strstr(monsterStats.name, getMonsterLocalizedName(SLIME).c_str()) ) { // If true, pretend the monster doesn't have a name and use the generic message "You hit the lesser skeleton!" @@ -21138,7 +28506,6 @@ list_t* TileEntityListHandler::getTileList(int x, int y) std::vector TileEntityListHandler::getEntitiesWithinRadius(int u, int v, int radius) { std::vector return_val; - for ( int i = u - radius; i <= u + radius; ++i ) { for ( int j = v - radius; j <= v + radius; ++j ) @@ -21174,13 +28541,209 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) } switch ( race ) { - case GNOME: - if ( limbType == LIMB_HUMANOID_TORSO ) + case GREMLIN: + if ( limbType == LIMB_HUMANOID_CLOAK ) + { + limb->x -= cos(this->yaw) * 1.25; + limb->y -= sin(this->yaw) * 1.25; + limb->z += 0.75; + if ( limb->sprite == items[CLOAK_BACKPACK].index ) + { + limb->z -= 0.75; + } + } + else if ( limbType == LIMB_HUMANOID_TORSO ) { limb->x -= .25 * cos(this->yaw); limb->y -= .25 * sin(this->yaw); limb->z += 1.25; + /*if ( limb->sprite == 1431 ) + { + limb->focalx += 0.25; + } + else if ( limb->sprite == 296 ) + { + limb->focalx += 0.5; + }*/ + + if ( limb->sprite == 1583 || limb->sprite == 1584 || limb->sprite == 2061 || limb->sprite == 2062 ) + { + limb->focalz += 0.5; // default torsos + } + else + { + limb->focalz -= 0.0; // armor torsos + limb->focalx += 1.0; + } + if ( limb->sprite == items[MACHINIST_APRON].indexShort ) + { + //limb->focalx -= 0.25; + limb->focalz += 0.25; + } + + limb->scalex = limbs[GREMLIN][11][0]; + limb->scaley = limbs[GREMLIN][11][1]; + limb->scalez = limbs[GREMLIN][11][2]; + + this->setTorsoLimbOffset(limb); + } + else if ( limbType == LIMB_HUMANOID_RIGHTLEG ) + { + limb->x += 1.15 * cos(this->yaw + PI / 2); + limb->y += 1.15 * sin(this->yaw + PI / 2); + limb->z += 3.75; + if ( limb->sprite == 1469 || limb->sprite == 1470 ) // loafer + { + limb->focalx += 0.25; + } + if ( true /*|| !(limb->sprite >= 1579 && limb->sprite <= 1582) + && !(limb->sprite >= 2057 && limb->sprite <= 2060)*/ ) // non-default boots + { + Entity* torso = nullptr; + if ( behavior == &actMonster ) + { + if ( auto node = list_Node(&children, 2) ) + { + torso = (Entity*)node->element; + } + } + else if ( behavior == &actPlayer ) + { + if ( auto node = list_Node(&children, 1) ) + { + torso = (Entity*)node->element; + } + } + if ( !torso || (torso // non-default torsos + && !(torso->sprite == 1583 + || torso->sprite == 1584 + || torso->sprite == 2061 + || torso->sprite == 2062)) ) + { + limb->z -= 0.2; + } + } + if ( this->z >= 3.9 && this->z <= 4.1 ) + { + limb->yaw += PI / 8; + limb->pitch = -PI / 2; + } + } + else if ( limbType == LIMB_HUMANOID_LEFTLEG ) + { + limb->x -= 1.15 * cos(this->yaw + PI / 2); + limb->y -= 1.15 * sin(this->yaw + PI / 2); + limb->z += 3.75; + if ( limb->sprite == 1469 || limb->sprite == 1470 ) // loafer + { + limb->focalx += 0.25; + } + if ( true /*|| !(limb->sprite >= 1579 && limb->sprite <= 1582) + && !(limb->sprite >= 2057 && limb->sprite <= 2060)*/ ) // non-default boots + { + Entity* torso = nullptr; + if ( behavior == &actMonster ) + { + if ( auto node = list_Node(&children, 2) ) + { + torso = (Entity*)node->element; + } + } + else if ( behavior == &actPlayer ) + { + if ( auto node = list_Node(&children, 1) ) + { + torso = (Entity*)node->element; + } + } + if ( !torso || (torso // non-default torsos + && !(torso->sprite == 1583 + || torso->sprite == 1584 + || torso->sprite == 2061 + || torso->sprite == 2062)) ) + { + limb->z -= 0.2; + } + } + if ( this->z >= 3.9 && this->z <= 4.1 ) + { + limb->yaw -= PI / 8; + limb->pitch = -PI / 2; + } + } + else if ( limbType == LIMB_HUMANOID_RIGHTARM ) + { + limb->x += 2.5 * cos(this->yaw + PI / 2) - .5 * cos(this->yaw); + limb->y += 2.5 * sin(this->yaw + PI / 2) - .5 * sin(this->yaw); + limb->z += .5; + if ( this->z >= 3.9 && this->z <= 4.1 ) + { + limb->pitch = 0; + } + + if ( limb->sprite != 1573 + && limb->sprite != 1577 + && limb->sprite != 2050 + && limb->sprite != 2054 ) // non default arms + { + limb->x -= 0.25 * cos(this->yaw + PI / 2); + limb->y -= 0.25 * sin(this->yaw + PI / 2); + } + else + { + limb->focalx += 0.25; + limb->focalz -= 0.25; + } + /*if ( limb->sprite == 1434 ) + { + limb->focalz -= 0.25; + }*/ + } + else if ( limbType == LIMB_HUMANOID_LEFTARM ) + { + limb->x -= 2.5 * cos(this->yaw + PI / 2) + .5 * cos(this->yaw); + limb->y -= 2.5 * sin(this->yaw + PI / 2) + .5 * sin(this->yaw); + limb->z += .5; + if ( this->z >= 3.9 && this->z <= 4.1 ) + { + limb->pitch = 0; + } + if ( limb->sprite != 1571 + && limb->sprite != 1575 + && limb->sprite != 2049 + && limb->sprite != 2053 ) // non default arms + { + limb->x += 0.25 * cos(this->yaw + PI / 2); + limb->y += 0.25 * sin(this->yaw + PI / 2); + } + else + { + limb->focalx += 0.25; + limb->focalz -= 0.25; + } + /*if ( limb->sprite == 1436 ) + { + limb->focalz -= 0.25; + }*/ + } + break; + case GNOME: + if ( limbType == LIMB_HUMANOID_CLOAK ) + { + limb->x -= cos(this->yaw) * 1.5; + limb->y -= sin(this->yaw) * 1.5; + limb->z -= 0.5; + if ( limb->sprite == items[CLOAK_BACKPACK].index ) + { + limb->z -= 0.75; + } + } + else if ( limbType == LIMB_HUMANOID_TORSO ) + { + limb->x -= .25 * cos(this->yaw); + limb->y -= .25 * sin(this->yaw); + limb->z += 1.25; if ( limb->sprite == 1431 ) { limb->focalx += 0.25; @@ -21190,26 +28753,32 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) limb->focalx += 0.5; } - if ( limb->sprite != 1427 && limb->sprite != 1431 && limb->sprite != 296 ) + if ( limb->sprite != 1427 && limb->sprite != 1431 && limb->sprite != 296 && limb->sprite != 2215 && limb->sprite != 2216 ) { limb->focalz -= 0.25; } + if ( limb->sprite == 2216 ) + { + limb->focalx += 0.25; + } if (limb->sprite == items[MACHINIST_APRON].indexShort) { - limb->focalx -= 0.25; + //limb->focalx -= 0.25; limb->focalz += 0.5; } limb->scalex = limbs[GNOME][11][0]; limb->scaley = limbs[GNOME][11][1]; limb->scalez = limbs[GNOME][11][2]; + + this->setTorsoLimbOffset(limb); } else if ( limbType == LIMB_HUMANOID_RIGHTLEG ) { limb->x += 1.25 * cos(this->yaw + PI / 2); limb->y += 1.25 * sin(this->yaw + PI / 2); limb->z += 2.25; - if ( limb->sprite == 1469 || limb->sprite == 1470 ) + if ( limb->sprite == 1469 || limb->sprite == 1470 ) // loafer { limb->focalx += 0.5; } @@ -21224,7 +28793,7 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) limb->x -= 1.25 * cos(this->yaw + PI / 2); limb->y -= 1.25 * sin(this->yaw + PI / 2); limb->z += 2.25; - if ( limb->sprite == 1469 || limb->sprite == 1470 ) + if ( limb->sprite == 1469 || limb->sprite == 1470 ) // loafer { limb->focalx += 0.5; } @@ -21245,7 +28814,8 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) } if ( limb->sprite != 1434 - && limb->sprite != 299 ) + && limb->sprite != 299 + && limb->sprite != 2221 ) { limb->x -= 0.25 * cos(this->yaw + PI / 2); limb->y -= 0.25 * sin(this->yaw + PI / 2); @@ -21254,6 +28824,11 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) { limb->focalz -= 0.25; } + else if ( limb->sprite == 2221 ) + { + limb->focalx += 0.25; + limb->focalz -= 0.25; + } } else if ( limbType == LIMB_HUMANOID_LEFTARM ) { @@ -21266,7 +28841,8 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) } if ( limb->sprite != 1436 - && limb->sprite != 301 ) + && limb->sprite != 301 + && limb->sprite != 2222 ) { limb->x += 0.25 * cos(this->yaw + PI / 2); limb->y += 0.25 * sin(this->yaw + PI / 2); @@ -21275,6 +28851,11 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) { limb->focalz -= 0.25; } + else if ( limb->sprite == 2222 ) + { + limb->focalx += 0.25; + limb->focalz -= 0.25; + } } break; case CREATURE_IMP: @@ -21317,6 +28898,8 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) limb->x -= .25 * cos(this->yaw); limb->y -= .25 * sin(this->yaw); limb->z += 2.5; + + this->setTorsoLimbOffset(limb); } else if ( limbType == LIMB_HUMANOID_RIGHTLEG ) { @@ -21441,6 +29024,7 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) { limb->scalez = 1.f; } + this->setTorsoLimbOffset(limb); } else if ( limbType == LIMB_HUMANOID_RIGHTLEG ) { @@ -21481,47 +29065,175 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) && limb->sprite != 471 && limb->sprite != 472 ) { // wearing gloves (not default arms), position tighter to body. - limb->x += 1.75 * cos(this->yaw + PI / 2) - .20 * cos(this->yaw); - limb->y += 1.75 * sin(this->yaw + PI / 2) - .20 * sin(this->yaw); + limb->x += 1.75 * cos(this->yaw + PI / 2) + (-.20) * cos(this->yaw); + limb->y += 1.75 * sin(this->yaw + PI / 2) + (-.20) * sin(this->yaw); } else { limb->x += 2.f * cos(this->yaw + PI / 2) - .20 * cos(this->yaw); limb->y += 2.f * sin(this->yaw + PI / 2) - .20 * sin(this->yaw); } - limb->z += .6; - if ( this->z >= 1.9 && this->z <= 2.1 ) + limb->z += .6; + if ( this->z >= 1.9 && this->z <= 2.1 ) + { + limb->pitch = 0; + } + } + else if ( limbType == LIMB_HUMANOID_LEFTARM ) + { + if ( limb->sprite != 688 && limb->sprite != 690 + && limb->sprite != 1045 && limb->sprite != 1047 + && limb->sprite != 231 && limb->sprite != 232 + && limb->sprite != 744 && limb->sprite != 746 + && limb->sprite != 469 && limb->sprite != 470 ) + { + // wearing gloves (not default arms), position tighter to body. + limb->x -= 1.75 * cos(this->yaw + PI / 2) + .20 * cos(this->yaw); + limb->y -= 1.75 * sin(this->yaw + PI / 2) + .20 * sin(this->yaw); + } + else + { + limb->x -= 2.f * cos(this->yaw + PI / 2) + .20 * cos(this->yaw); + limb->y -= 2.f * sin(this->yaw + PI / 2) + .20 * sin(this->yaw); + } + limb->z += .6; + if ( this->z >= 1.9 && this->z <= 2.1 ) + { + limb->pitch = 0; + } + } + break; + case GOBLIN: + case GOATMAN: + case INSECTOID: + case DRYAD: + case MYCONID: + case SALAMANDER: + { + real_t sleepHeight = 2.5; + if ( race == DRYAD ) + { + sleepHeight = 3.0; + if ( sprite == 1514 || sprite == 1515 || sprite == 1992 || sprite == 1993 ) + { + sleepHeight = 3.75; + if ( limbType == LIMB_HUMANOID_LEFTLEG || limbType == LIMB_HUMANOID_RIGHTLEG ) + { + limb->z -= 1.5; + if ( !(limb->sprite == 1507 || limb->sprite == 1510 + || limb->sprite == 1517 || limb->sprite == 1518 + || limb->sprite == 1985 || limb->sprite == 1988 + || limb->sprite == 1995 || limb->sprite == 1996) ) + { + // non-default boots + if ( limbType == LIMB_HUMANOID_LEFTLEG ) + { + limb->focaly += 0.25; + } + if ( limbType == LIMB_HUMANOID_RIGHTLEG ) + { + limb->focaly -= 0.25; + limb->focalx -= 0.4; + } + //limb->focalx += 0.3; + } + } + else if ( limbType == LIMB_HUMANOID_LEFTARM ) + { + limb->focaly += 0.5; // short torso move arms to body + } + else if ( limbType == LIMB_HUMANOID_RIGHTARM ) + { + limb->focaly -= 0.5; // short torso move arms to body + } + else if ( limbType == LIMB_HUMANOID_CLOAK ) + { + limb->z += 0.5; + if ( limb->sprite == items[CLOAK_BACKPACK].index ) + { + limb->z -= 0.75; + } + } + } + else + { + if ( limbType == LIMB_HUMANOID_TORSO ) + { + limb->focalz += 0.25; + } + } + } + else if ( race == MYCONID ) + { + sleepHeight = 3.0; + if ( sprite == 1520 || sprite == 1998 ) + { + sleepHeight = 3.75; + if ( limbType == LIMB_HUMANOID_LEFTLEG || limbType == LIMB_HUMANOID_RIGHTLEG ) + { + limb->z -= 1; + limb->focalz -= 0.5; + + if ( !(limb->sprite == 1531 || limb->sprite == 1533 + || limb->sprite == 2009 || limb->sprite == 2011) ) + { + limb->z += 0.25; + // non-default boots + if ( limbType == LIMB_HUMANOID_LEFTLEG ) + { + limb->focaly += 0.25; + } + if ( limbType == LIMB_HUMANOID_RIGHTLEG ) + { + limb->focaly -= 0.25; + } + } + } + else if ( limbType == LIMB_HUMANOID_LEFTARM ) + { + limb->focaly += 0.5; // short torso move arms to body + } + else if ( limbType == LIMB_HUMANOID_RIGHTARM ) + { + limb->focaly -= 0.5; // short torso move arms to body + } + else if ( limbType == LIMB_HUMANOID_CLOAK ) + { + limb->z += 0.75; + if ( limb->sprite == items[CLOAK_BACKPACK].index ) + { + limb->z -= 0.75; + } + } + } + + if ( limbType == LIMB_HUMANOID_LEFTLEG || limbType == LIMB_HUMANOID_RIGHTLEG ) { - limb->pitch = 0; + limb->x -= .5 * cos(this->yaw); + limb->y -= .5 * sin(this->yaw); } } - else if ( limbType == LIMB_HUMANOID_LEFTARM ) + else if ( race == SALAMANDER ) { - if ( limb->sprite != 688 && limb->sprite != 690 - && limb->sprite != 1045 && limb->sprite != 1047 - && limb->sprite != 231 && limb->sprite != 232 - && limb->sprite != 744 && limb->sprite != 746 - && limb->sprite != 469 && limb->sprite != 470 ) - { - // wearing gloves (not default arms), position tighter to body. - limb->x -= 1.75 * cos(this->yaw + PI / 2) + .20 * cos(this->yaw); - limb->y -= 1.75 * sin(this->yaw + PI / 2) + .20 * sin(this->yaw); - } - else + if ( limbType == LIMB_HUMANOID_LEFTLEG || limbType == LIMB_HUMANOID_RIGHTLEG ) { - limb->x -= 2.f * cos(this->yaw + PI / 2) + .20 * cos(this->yaw); - limb->y -= 2.f * sin(this->yaw + PI / 2) + .20 * sin(this->yaw); + limb->z += 0.75; + if ( (limb->sprite >= 1554 && limb->sprite <= 1559) + || (limb->sprite >= 2032 && limb->sprite <= 2037) ) + { + // base feet, push backward + limb->focalx -= .5; + } } - limb->z += .6; - if ( this->z >= 1.9 && this->z <= 2.1 ) + else if ( limbType == LIMB_HUMANOID_LEFTARM + || limbType == LIMB_HUMANOID_RIGHTARM ) { - limb->pitch = 0; + limb->z += .5; + limb->x += .25 * cos(this->yaw); + limb->y += .25 * sin(this->yaw); } } - break; - case GOBLIN: - case GOATMAN: - case INSECTOID: + if ( limbType == LIMB_HUMANOID_TORSO ) { limb->x -= .25 * cos(this->yaw); @@ -21529,7 +29241,7 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) limb->z += 2; if ( race == INSECTOID ) { - if ( limb->sprite != 727 && limb->sprite != 458 + if ( limb->sprite != 727 && limb->sprite != 458 && limb->sprite != 761 && limb->sprite != 1060 ) { @@ -21538,6 +29250,60 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) } } + if ( race == MYCONID ) + { + limb->scalex = 1.01; + limb->scaley = 1.01; + limb->scalez = 1.01; + + if ( sprite == 1520 || sprite == 1998 ) + { + //if ( limb->sprite != 1535 ) + //{ + // // wearing armor, offset + //} + //limb->z -= 0.5; + } + } + else if ( race == DRYAD ) + { + limb->scalex = 1.01; + limb->scaley = 1.01; + limb->scalez = 1.01; + + if ( sprite == 1514 || sprite == 1515 || sprite == 1992 || sprite == 1993 ) + { + if ( limb->sprite != 1513 && limb->sprite != 1516 + && limb->sprite != 1991 && limb->sprite != 1994 ) + { + // wearing armor, offset + //limb->z -= 0.5; + } + } + if ( limb->sprite == 1511 || limb->sprite == 1512 || limb->sprite == 1989 || limb->sprite == 1990 ) + { + limb->focaly += 0.25; // center the torso + } + else if ( limb->sprite == 1513 || limb->sprite == 1516 || limb->sprite == 1991 || limb->sprite == 1994 ) + { + limb->focalz -= 0.25; + } + } + else if ( race == SALAMANDER ) + { + limb->scalex = 1.01; + limb->scaley = 1.01; + limb->scalez = 1.01; + + if ( limb->sprite != 1560 && limb->sprite != 1561 && limb->sprite != 1562 + && limb->sprite != 2038 && limb->sprite != 2039 && limb->sprite != 2040 ) + { + // wearing armor, offset + limb->focalx += 1.0; + limb->focalz += 0.75; + } + } + /*if ( limb->sprite == items[WIZARD_DOUBLET].index || limb->sprite == items[HEALER_DOUBLET].index || limb->sprite == items[TUNIC].index @@ -21545,13 +29311,14 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) { limb->z += 0.25; }*/ + this->setTorsoLimbOffset(limb); } else if ( limbType == LIMB_HUMANOID_RIGHTLEG ) { limb->x += 1 * cos(this->yaw + PI / 2) + .25 * cos(this->yaw); limb->y += 1 * sin(this->yaw + PI / 2) + .25 * sin(this->yaw); limb->z += 4; - if ( this->z >= 2.4 && this->z <= 2.6 ) + if ( this->z >= (sleepHeight - 0.1) && this->z <= (sleepHeight + 0.1) ) { limb->yaw += PI / 8; limb->pitch = -PI / 2; @@ -21560,13 +29327,25 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) { limb->pitch = 0; } + + if ( race == INSECTOID ) + { + if ( limb->sprite == 456 || limb->sprite == 457 + || limb->sprite == 732 || limb->sprite == 733 + || limb->sprite == 766 || limb->sprite == 767 + || limb->sprite == 1058 || limb->sprite == 1059 ) + { + // default legs offset rotation a bit + limb->focalx -= 0.6; + } + } } else if ( limbType == LIMB_HUMANOID_LEFTLEG ) { limb->x -= 1 * cos(this->yaw + PI / 2) - .25 * cos(this->yaw); limb->y -= 1 * sin(this->yaw + PI / 2) - .25 * sin(this->yaw); limb->z += 4; - if ( this->z >= 2.4 && this->z <= 2.6 ) + if ( this->z >= (sleepHeight - 0.1) && this->z <= (sleepHeight + 0.1) ) { limb->yaw -= PI / 8; limb->pitch = -PI / 2; @@ -21575,13 +29354,26 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) { limb->pitch = 0; } + + if ( race == INSECTOID ) + { + if ( limb->sprite == 456 || limb->sprite == 457 + || limb->sprite == 732 || limb->sprite == 733 + || limb->sprite == 766 || limb->sprite == 767 + || limb->sprite == 1058 || limb->sprite == 1059 ) + { + // default legs offset rotation a bit + limb->focalx -= 0.6; + } + } } else if ( limbType == LIMB_HUMANOID_RIGHTARM ) { limb->x += 2.5 * cos(this->yaw + PI / 2) - .20 * cos(this->yaw); limb->y += 2.5 * sin(this->yaw + PI / 2) - .20 * sin(this->yaw); limb->z += .5; - if ( this->z >= 2.4 && this->z <= 2.6 ) + + if ( this->z >= (sleepHeight - 0.1) && this->z <= (sleepHeight + 0.1) ) { limb->pitch = 0; } @@ -21591,11 +29383,13 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) limb->x -= 2.5 * cos(this->yaw + PI / 2) + .20 * cos(this->yaw); limb->y -= 2.5 * sin(this->yaw + PI / 2) + .20 * sin(this->yaw); limb->z += .5; - if ( this->z >= 2.4 && this->z <= 2.6 ) + + if ( this->z >= (sleepHeight - 0.1) && this->z <= (sleepHeight + 0.1) ) { limb->pitch = 0; } } + } break; case INCUBUS: case SUCCUBUS: @@ -21612,6 +29406,8 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) { limb->z += 0.5; } + + this->setTorsoLimbOffset(limb); } else if ( limbType == LIMB_HUMANOID_RIGHTLEG ) { @@ -21667,6 +29463,19 @@ void Entity::setHumanoidLimbOffset(Entity* limb, Monster race, int limbType) default: break; } + if ( limbType == LIMB_HUMANOID_TORSO || limbType == LIMB_HUMANOID_RIGHTARM || limbType == LIMB_HUMANOID_LEFTARM ) + { + auto find = EquipmentModelOffsets.miscItemsBaseOffsets.find(limb->sprite); + if ( find != EquipmentModelOffsets.miscItemsBaseOffsets.end() ) + { + limb->focalx += find->second.focalx; + limb->focaly += find->second.focaly; + limb->focalz += find->second.focalz; + limb->scalex += find->second.scalex; + limb->scaley += find->second.scaley; + limb->scalez += find->second.scalez; + } + } } void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) @@ -21683,6 +29492,8 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) player = this->skill[2]; } Entity* flameEntity = nullptr; + auto& shieldLimbFociAnimRotate = shieldLimb->fskill[0]; + auto& shieldLimbFociRotateSpin = shieldLimb->fskill[1]; shieldLimb->focalx = limbs[race][7][0]; shieldLimb->focaly = limbs[race][7][1]; @@ -21728,8 +29539,27 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) shieldLimb->scaley = 0.8; shieldLimb->scalez = 0.8; } + else if ( itemSpriteIsFociThirdPersonModel(shieldLimb->sprite) ) + { + shieldLimb->pitch = shieldArmLimb->pitch; + shieldLimb->yaw += PI / 6; + shieldLimb->roll = PI / 2; + shieldLimb->focalx += 3.0; + shieldLimb->focaly += 5.5; + shieldLimb->focalz += -1.75; + if ( !(shieldLimb->sprite == items[TOOL_FOCI_FIRE].index + || shieldLimb->sprite == items[TOOL_FOCI_SNOW].index + || shieldLimb->sprite == items[TOOL_FOCI_NEEDLES].index + || shieldLimb->sprite == items[TOOL_FOCI_ARCS].index + || shieldLimb->sprite == items[TOOL_FOCI_SAND].index) ) + { + shieldLimb->focalz -= 1.0; + } + shieldLimb->z -= 2.0; + } break; case GNOME: + case GREMLIN: shieldLimb->x -= 2.5 * cos(this->yaw + PI / 2) + .20 * cos(this->yaw); shieldLimb->y -= 2.5 * sin(this->yaw + PI / 2) + .20 * sin(this->yaw); shieldLimb->z += 1; @@ -21750,6 +29580,10 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) flameEntity->x += 2 * cos(shieldArmLimb->yaw); flameEntity->y += 2 * sin(shieldArmLimb->yaw); flameEntity->z -= 2; + if ( race == GREMLIN ) + { + flameEntity->z += 1; + } } } else if ( shieldLimb->sprite == items[TOOL_CRYSTALSHARD].index ) @@ -21766,6 +29600,10 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) flameEntity->x += 2 * cos(shieldArmLimb->yaw); flameEntity->y += 2 * sin(shieldArmLimb->yaw); flameEntity->z += 1; + if ( race == GREMLIN ) + { + flameEntity->z += 1; + } } } } @@ -21784,9 +29622,19 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) shieldLimb->yaw += PI / 6; shieldLimb->focalx -= 4; shieldLimb->focalz += .5; - shieldLimb->x += 0.5 * cos(this->yaw + PI / 2) + .5 * cos(this->yaw); - shieldLimb->y += 0.5 * sin(this->yaw + PI / 2) + .5 * sin(this->yaw); - shieldLimb->z -= 1; + if ( race == GREMLIN ) + { + shieldLimb->focalz -= 2.25; + } + if ( race == GNOME ) + { + shieldLimb->x += -0.75 * cos(this->yaw); + shieldLimb->y += -0.75 * sin(this->yaw); + shieldLimb->z += -0.5; + } + shieldLimb->x += 0.55 * cos(this->yaw + PI / 2) + 0.5 * cos(this->yaw); + shieldLimb->y += 0.55 * sin(this->yaw + PI / 2) + 0.5 * sin(this->yaw); + shieldLimb->z += 0; shieldLimb->scalex = 0.8; shieldLimb->scaley = 0.8; shieldLimb->scalez = 0.8; @@ -22051,6 +29899,9 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) case INSECTOID: case INCUBUS: case SUCCUBUS: + case DRYAD: + case MYCONID: + case SALAMANDER: shieldLimb->x -= 2.5 * cos(this->yaw + PI / 2) + .20 * cos(this->yaw); shieldLimb->y -= 2.5 * sin(this->yaw + PI / 2) + .20 * sin(this->yaw); shieldLimb->z += 2.5; @@ -22114,9 +29965,31 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) { shieldLimb->focalz -= 1.5; } + else if ( race == DRYAD ) + { + if ( sprite == 1514 || sprite == 1515 || sprite == 1992 || sprite == 1993 ) + { + shieldLimb->focaly += 0.1; + } + shieldLimb->z -= 1.0; + } + else if ( race == MYCONID ) + { + if ( sprite == 1520 || sprite == 1998 ) + { + shieldLimb->focaly += 0.1; + } + shieldLimb->z -= 1.0; + } + else if ( race == SALAMANDER ) + { + shieldLimb->focalx -= 1.0; + shieldLimb->focalz -= 1.0; + shieldLimb->z -= 1.0; + } shieldLimb->x += 0.5 * cos(this->yaw + PI / 2) + .5 * cos(this->yaw); shieldLimb->y += 0.5 * sin(this->yaw + PI / 2) + .5 * sin(this->yaw); - shieldLimb->z -= 1; + shieldLimb->z -= 0.5; shieldLimb->scalex = 0.8; shieldLimb->scaley = 0.8; shieldLimb->scalez = 0.8; @@ -22187,7 +30060,185 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) break; } - if ( shieldLimb->sprite == items[TOOL_TINKERING_KIT].index ) + if ( !itemSpriteIsFociThirdPersonModel(shieldLimb->sprite) ) + { + shieldLimbFociAnimRotate = 0.0; + shieldLimbFociRotateSpin = 0.0; + } + + if ( shieldLimb->sprite >= items[TOOL_DUCK].index && shieldLimb->sprite < items[TOOL_DUCK].index + MAXPLAYERS ) + { + shieldLimb->focalz -= 1.0; + shieldLimb->focalx += 0.25; + if ( this->fskill[8] > PI / 32 ) + { + + } + else + { + shieldLimb->yaw += PI / 12; + } + } + else if ( shieldLimb->sprite == items[TOOL_FRYING_PAN].index ) + { + if ( this->fskill[8] > PI / 32 ) + { + shieldLimb->yaw += -0.7; + shieldLimb->roll += 0.5; + shieldLimb->pitch += 0.25; + shieldLimb->focalz += -3; + shieldLimb->focalx += -0.5; + shieldLimb->focaly += 1.25; + if ( race == GREMLIN ) + { + shieldLimb->x += 0.25 * cos(this->yaw); + shieldLimb->y += 0.25 * sin(this->yaw); + } + } + else + { + shieldLimb->yaw += -1.1; + shieldLimb->roll += 0.5; + shieldLimb->pitch += 0.0; + shieldLimb->focalx += -1.75; + shieldLimb->focaly += 1.75; + shieldLimb->focalz += -2.25; + } + + if ( race == AUTOMATON ) + { + shieldLimb->x += 0.6 * cos(this->yaw + PI / 2) + -0.5 * cos(this->yaw); + shieldLimb->y += 0.6 * sin(this->yaw + PI / 2) + -0.5 * sin(this->yaw); + shieldLimb->z += -1.75; + } + else if ( race == SKELETON ) + { + shieldLimb->x += 0.75 * cos(this->yaw + PI / 2) + -0.5 * cos(this->yaw); + shieldLimb->y += 0.75 * sin(this->yaw + PI / 2) + -0.5 * sin(this->yaw); + shieldLimb->z += -0.5; + } + else if ( race == GREMLIN ) + { + shieldLimb->focaly += 0.75; + } + } + else if ( shieldLimb->sprite == items[SILVER_SHIELD].index ) + { + shieldLimb->focalx += 0.5; + } + else if ( shieldLimb->sprite == items[INSTRUMENT_DRUM].index + || shieldLimb->sprite == items[INSTRUMENT_FLUTE].index + || shieldLimb->sprite == items[INSTRUMENT_LUTE].index + || shieldLimb->sprite == items[INSTRUMENT_LYRE].index + || shieldLimb->sprite == items[INSTRUMENT_HORN].index ) + { + shieldLimb->focaly += 1.0; + if ( shieldLimb->sprite == items[INSTRUMENT_HORN].index ) + { + shieldLimb->focalx += 1.25; + } + else if ( shieldLimb->sprite == items[INSTRUMENT_DRUM].index ) + { + shieldLimb->focalx += 0.75; + } + } + else if ( itemSpriteIsFociThirdPersonModel(shieldLimb->sprite) ) + { + shieldLimb->focaly += 1; + if ( race == AUTOMATON ) + { + shieldLimb->focaly -= 0.75; + shieldLimb->focalz -= 0.5; + } + else if ( race == SALAMANDER ) + { + shieldLimb->focaly += 0.75; + shieldLimb->focalz -= 0.5; + } + else if ( race == DRYAD ) + { + shieldLimb->focalz -= 0.5; + } + else if ( race == MYCONID ) + { + shieldLimb->focaly += 0.5; + } + else if ( race == GREMLIN ) + { + shieldLimb->focaly += 0.5; + } + if ( this->fskill[8] > PI / 32 ) //MONSTER_SHIELDYAW and PLAYER_SHIELDYAW defending animation + { + int particleRate = 20; + shieldLimbFociAnimRotate += 1.0 / TICKS_PER_SECOND; + shieldLimbFociAnimRotate = std::min(1.0, shieldLimbFociAnimRotate); + if ( shieldLimbFociAnimRotate < 1.0 ) + { + particleRate = 10; + } + + shieldLimbFociRotateSpin += 0.15 + 0.25 * (1.0 - shieldLimbFociAnimRotate); + real_t animRatio = 1.0; + + if ( race == CREATURE_IMP ) + { + shieldLimb->focaly -= 5; + shieldLimb->focalz += 4; + } + + real_t xDist = shieldLimb->focalx * animRatio; + real_t yDist = shieldLimb->focaly * animRatio; + xDist += 1.0; + yDist += 1.0; + shieldLimb->focalx *= 1.0 - animRatio; + shieldLimb->focaly *= 1.0 - animRatio; + shieldLimb->roll *= 1.0 - animRatio; + shieldLimb->pitch *= 1.0 - animRatio; + shieldLimb->x += yDist * cos(this->yaw + PI / 2) + xDist * cos(this->yaw); + shieldLimb->y += yDist * sin(this->yaw + PI / 2) + xDist * sin(this->yaw); + + shieldLimb->yaw += shieldLimbFociRotateSpin; + + if ( ticks % particleRate == 0 ) + { + Entity* entity = spawnGib(shieldLimb, 16); + entity->flags[INVISIBLE] = false; + entity->flags[SPRITE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->lightBonus = vec4(0.2f, 0.2f, 0.2f, 0.f); + //entity->x = shieldLimb->x; + //entity->y = shieldLimb->y; + entity->z = shieldLimb->z - 1; + if ( race == CREATURE_IMP ) + { + entity->z += 3.0; + } + entity->scalex = 0.25f; //MAKE 'EM SMALL PLEASE! + entity->scaley = 0.25f; + entity->scalez = 0.25f; + entity->yaw = ((local_rng.rand() % 6) * 60) * PI / 180.0; + entity->pitch = (local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + entity->vel_x = cos(entity->yaw) * .1; + entity->vel_y = sin(entity->yaw) * .1; + entity->vel_z = -.15; + entity->fskill[3] = 0.01; + entity->fskill[4] = 0.01; // GIB_SHRINK + entity->skill[4] = 25; // GIB_LIFESPAN + } + } + else + { + if ( shieldLimbFociAnimRotate > 0.0 ) + { + shieldLimb->bNeedsRenderPositionInit = true; + } + shieldLimbFociAnimRotate = 0.0; + shieldLimbFociRotateSpin = 0.0; + } + } + else if ( shieldLimb->sprite == items[TOOL_TINKERING_KIT].index ) { //shieldLimb->pitch = 0; shieldLimb->yaw += PI / 6; @@ -22241,6 +30292,43 @@ void Entity::handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb) } } +bool Entity::isSmiteWeakMonster() +{ + Stat* myStats = getStats(); + if ( myStats ) + { + switch ( myStats->type ) + { + case SKELETON: + case CREATURE_IMP: + case GHOUL: + case DEMON: + case SUCCUBUS: + case INCUBUS: + case VAMPIRE: + case LICH: + case LICH_ICE: + case LICH_FIRE: + case DEVIL: + case SHADOW: + case REVENANT_SKULL: + case MONSTER_ADORCISED_WEAPON: + return true; + break; + case MIMIC: + case MINIMIMIC: + if ( myStats->getEffectActive(EFF_MIMIC_VOID) ) + { + return true; + } + break; + default: + break; + } + } + return false; +} + bool Entity::isBossMonster() { Stat* myStats = getStats(); @@ -22248,12 +30336,14 @@ bool Entity::isBossMonster() { if ( myStats->type == MINOTAUR || myStats->type == SHOPKEEPER + || monsterCanTradeWith(-1) || myStats->type == SHADOW || myStats->type == LICH || myStats->type == LICH_FIRE || myStats->type == LICH_ICE || myStats->type == DEVIL - || (myStats->type == VAMPIRE && MonsterData_t::nameMatchesSpecialNPCName(*myStats, "bram kindly")) + || (myStats->type == VAMPIRE && (myStats->getAttribute("special_npc") == "bram kindly")) + || (myStats->type == INCUBUS && (myStats->getAttribute("special_npc") == "johann")) || (myStats->type == COCKATRICE && !strncmp(map.name, "Cockatrice Lair", 15)) ) { @@ -22269,56 +30359,153 @@ bool Entity::isBossMonster() void Entity::handleKnockbackDamage(Stat& myStats, Entity* knockedInto) { - if ( knockedInto != NULL && myStats.EFFECTS[EFF_KNOCKBACK] && myStats.HP > 0 ) + if ( knockedInto != NULL && (myStats.getEffectActive(EFF_KNOCKBACK) || myStats.getEffectActive(EFF_DASH) >= 2) + && myStats.HP > 0 && myStats.type != MONSTER_ADORCISED_WEAPON ) { - int damageOnHit = 5 + local_rng.rand() % 6; - if ( knockedInto->behavior == &actDoor ) + Entity* whoKnockedMe = nullptr; + int spellID = SPELL_NONE; + int playerSource = -1; + if ( myStats.getEffectActive(EFF_DASH) >= 2 && behavior == &actPlayer ) { - playSoundEntity(this, 28, 64); - this->modHP(-damageOnHit); - Entity* whoKnockedMe = uidToEntity(this->monsterKnockbackUID); - if ( myStats.HP <= 0 ) + int effect = myStats.getEffectActive(EFF_DASH) - 2; + int type = effect / (MAXPLAYERS + 1); + if ( type == 0 ) + { + spellID = SPELL_DASH; + } + else if ( type == 1 ) + { + spellID = SPELL_KINETIC_FIELD; + } + if ( (effect % (MAXPLAYERS + 1)) >= 0 && (effect % (MAXPLAYERS + 1)) < MAXPLAYERS ) + { + playerSource = effect % (MAXPLAYERS + 1); + whoKnockedMe = players[playerSource]->entity; + } + } + else + { + if ( behavior == &actMonster ) + { + whoKnockedMe = uidToEntity(this->monsterKnockbackUID); + } + else + { + return; // players no normal knockback impacts + } + } + + int damageOnHit = 0; + bool spellEvent = false; + bool immuneDamageOnHit = spellID == SPELL_DASH; + if ( knockedInto->behavior == &actDoor || knockedInto->behavior == &::actIronDoor ) + { + damageOnHit = 5 + local_rng.rand() % 6; + if ( !immuneDamageOnHit ) + { + playSoundEntity(this, 28, 64); + this->modHP(-damageOnHit); + this->setObituary(Language::get(6853)); + myStats.killer = KilledBy::DEATH_KNOCKBACK; + } + + if ( myStats.HP <= 0 && !immuneDamageOnHit ) { - if ( whoKnockedMe ) + if ( whoKnockedMe && whoKnockedMe != this ) { whoKnockedMe->awardXP(this, true, true); } } - if ( whoKnockedMe && whoKnockedMe->behavior == &actPlayer ) - { - steamStatisticUpdateClient(whoKnockedMe->skill[2], STEAM_STAT_TAKE_THIS_OUTSIDE, STEAM_STAT_INT, 1); - Compendium_t::Events_t::eventUpdateWorld(whoKnockedMe->skill[2], Compendium_t::CPDM_DOOR_BROKEN, "door", 1); - } - knockedInto->doorHealth = 0; // smash doors instantly - playSoundEntity(knockedInto, 28, 64); - if ( knockedInto->doorHealth <= 0 ) + + if ( knockedInto->behavior == &actDoor ) { - // set direction of splinters - if ( !knockedInto->doorDir ) + if ( whoKnockedMe && whoKnockedMe->behavior == &actPlayer ) { - knockedInto->doorSmacked = (this->x > knockedInto->x); + steamStatisticUpdateClient(whoKnockedMe->skill[2], STEAM_STAT_TAKE_THIS_OUTSIDE, STEAM_STAT_INT, 1); + Compendium_t::Events_t::eventUpdateWorld(whoKnockedMe->skill[2], Compendium_t::CPDM_DOOR_BROKEN, "door", 1); } - else + playSoundEntity(knockedInto, 28, 64); + spellEvent = true; + } + else if ( knockedInto->behavior == &::actIronDoor ) + { + setEffect(EFF_DASH, false, 0, false); + setEffect(EFF_KNOCKBACK, false, 0, false); + } + knockedInto->doorHandleDamageMagic(knockedInto->doorHealth, *this, whoKnockedMe, false); + } + else if ( knockedInto->isDamageableCollider() ) + { + damageOnHit = 5 + local_rng.rand() % 6; + if ( !immuneDamageOnHit ) + { + playSoundEntity(this, 28, 64); + this->modHP(-damageOnHit); + this->setObituary(Language::get(6853)); + myStats.killer = KilledBy::DEATH_KNOCKBACK; + } + + if ( myStats.HP <= 0 && !immuneDamageOnHit ) + { + if ( whoKnockedMe && whoKnockedMe != this ) { - knockedInto->doorSmacked = (this->y < knockedInto->y); + whoKnockedMe->awardXP(this, true, true); } } + + spellEvent = true; + knockedInto->colliderHandleDamageMagic(20, *this, whoKnockedMe, false); + if ( knockedInto->colliderCurrentHP > 0 ) + { + setEffect(EFF_KNOCKBACK, false, 0, false); + setEffect(EFF_DASH, false, 0, false); + } } else if ( knockedInto->behavior == &::actFurniture ) { // break it down! - playSoundEntity(this, 28, 64); - this->modHP(-damageOnHit); - if ( myStats.HP <= 0 ) + damageOnHit = 5 + local_rng.rand() % 6; + if ( !immuneDamageOnHit ) + { + playSoundEntity(this, 28, 64); + this->modHP(-damageOnHit); + this->setObituary(Language::get(6853)); + myStats.killer = KilledBy::DEATH_KNOCKBACK; + } + + if ( myStats.HP <= 0 && !immuneDamageOnHit ) + { + if ( whoKnockedMe && whoKnockedMe != this ) + { + whoKnockedMe->awardXP(this, true, true); + } + } + + spellEvent = true; + knockedInto->furnitureHandleDamageMagic(knockedInto->furnitureHealth, *this, whoKnockedMe, false); + } + + if ( damageOnHit > 0 && whoKnockedMe ) + { + if ( getStats() && !immuneDamageOnHit ) { - Entity* whoKnockedMe = uidToEntity(this->monsterKnockbackUID); - if ( whoKnockedMe ) + // update enemy bar for attacker + if ( !strcmp(getStats()->name, "") ) + { + updateEnemyBar(whoKnockedMe, this, getMonsterLocalizedName(getStats()->type).c_str(), getStats()->HP, getStats()->MAXHP, + false, DamageGib::DMG_DEFAULT); + } + else { - whoKnockedMe->awardXP(this, true, true); + updateEnemyBar(whoKnockedMe, this, getStats()->name, getStats()->HP, getStats()->MAXHP, + false, DamageGib::DMG_DEFAULT); } } - knockedInto->furnitureHealth = 0; // smash furniture instantly - playSoundEntity(knockedInto, 28, 64); + if ( spellEvent && spellID > SPELL_NONE && playerSource >= 0 ) + { + magicOnSpellCastEvent(whoKnockedMe, whoKnockedMe, nullptr, + spellID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } } } } @@ -22340,9 +30527,9 @@ void Entity::setHelmetLimbOffsetWithMask(Entity* helm, Entity* mask) helm->scalez = 1.01; int monster = getMonsterTypeFromSprite(); - if ( EquipmentModelOffsets.modelOffsetExists(monster, helm->sprite) ) + if ( int resultMonsterSprite = EquipmentModelOffsets.modelOffsetExists(monster, helm->sprite, sprite) ) { - auto& entry = EquipmentModelOffsets.getModelOffset(monster, helm->sprite); + auto& entry = EquipmentModelOffsets.getModelOffset(resultMonsterSprite, helm->sprite); helm->scalex += entry.scalex; helm->scaley += entry.scaley; helm->scalez += entry.scalez; @@ -22367,6 +30554,7 @@ void Entity::setHelmetLimbOffsetWithMask(Entity* helm, Entity* mask) || (helm->sprite >= items[HAT_HOOD].index && helm->sprite < items[HAT_HOOD].index + items[HAT_HOOD].variations) || helm->sprite == items[HAT_HOOD_RED].index || helm->sprite == items[HAT_HOOD_SILVER].index + || helm->sprite == 2004 || helm->sprite == 1526 || (helm->sprite >= items[HAT_HOOD_APPRENTICE].index && helm->sprite < items[HAT_HOOD_APPRENTICE].index + items[HAT_HOOD_APPRENTICE].variations) || (helm->sprite >= items[HAT_HOOD_ASSASSIN].index && helm->sprite < items[HAT_HOOD_ASSASSIN].index + items[HAT_HOOD_ASSASSIN].variations) || (helm->sprite >= items[HAT_HOOD_WHISPERS].index && helm->sprite < items[HAT_HOOD_WHISPERS].index + items[HAT_HOOD_WHISPERS].variations) @@ -22376,9 +30564,9 @@ void Entity::setHelmetLimbOffsetWithMask(Entity* helm, Entity* mask) helm->scaley = 1.05; helm->scalez = 1.05; - if ( EquipmentModelOffsets.maskHasAdjustmentForExpandedHelm(monster, helm->sprite, mask->sprite) ) + if ( int resultMonsterSprite = EquipmentModelOffsets.maskHasAdjustmentForExpandedHelm(monster, helm->sprite, mask->sprite, sprite) ) { - auto offsetMask = EquipmentModelOffsets.getMaskOffsetForExpandHelm(monster, helm->sprite, mask->sprite); + auto offsetMask = EquipmentModelOffsets.getMaskOffsetForExpandHelm(resultMonsterSprite, helm->sprite, mask->sprite); mask->focalx += offsetMask.focalx; mask->focaly += offsetMask.focaly; mask->focalz += offsetMask.focalz; @@ -22395,13 +30583,13 @@ void Entity::setHelmetLimbOffsetWithMask(Entity* helm, Entity* mask) } } } - else if ( EquipmentModelOffsets.expandHelmToFitMask(monster, helm->sprite, mask->sprite) ) + else if ( int resultMonsterSprite = EquipmentModelOffsets.expandHelmToFitMask(monster, helm->sprite, mask->sprite, sprite) ) { helm->scalex = 1.05; helm->scaley = 1.05; helm->scalez = 1.05; - auto offsetHelm = EquipmentModelOffsets.getExpandHelmOffset(monster, helm->sprite, mask->sprite); + auto offsetHelm = EquipmentModelOffsets.getExpandHelmOffset(resultMonsterSprite, helm->sprite, mask->sprite); helm->focalx += offsetHelm.focalx; helm->focaly += offsetHelm.focaly; helm->focalz += offsetHelm.focalz; @@ -22409,7 +30597,7 @@ void Entity::setHelmetLimbOffsetWithMask(Entity* helm, Entity* mask) helm->scaley += offsetHelm.scaley; helm->scalez += offsetHelm.scalez; - auto offsetMask = EquipmentModelOffsets.getMaskOffsetForExpandHelm(monster, helm->sprite, mask->sprite); + auto offsetMask = EquipmentModelOffsets.getMaskOffsetForExpandHelm(resultMonsterSprite, helm->sprite, mask->sprite); mask->focalx += offsetMask.focalx; mask->focaly += offsetMask.focaly; mask->focalz += offsetMask.focalz; @@ -22419,9 +30607,9 @@ void Entity::setHelmetLimbOffsetWithMask(Entity* helm, Entity* mask) return; } - else if ( EquipmentModelOffsets.maskHasAdjustmentForExpandedHelm(monster, helm->sprite, mask->sprite) ) + else if ( int resultMonsterSprite = EquipmentModelOffsets.maskHasAdjustmentForExpandedHelm(monster, helm->sprite, mask->sprite, sprite) ) { - auto offsetMask = EquipmentModelOffsets.getMaskOffsetForExpandHelm(monster, helm->sprite, mask->sprite); + auto offsetMask = EquipmentModelOffsets.getMaskOffsetForExpandHelm(resultMonsterSprite, helm->sprite, mask->sprite); mask->focalx += offsetMask.focalx; mask->focaly += offsetMask.focaly; mask->focalz += offsetMask.focalz; @@ -22558,7 +30746,17 @@ bool monsterChangesColorWhenAlly(Stat* myStats, Entity* entity) } if ( race == HUMAN || race == SENTRYBOT || race == NOTHING || race == SLIME - || race == SPELLBOT || race == AUTOMATON || race == GYROBOT || race == DUMMYBOT ) + || race == SPELLBOT || race == AUTOMATON || race == GYROBOT || race == DUMMYBOT || race == DUCK_SMALL ) + { + return false; + } + if ( race == MOTH_SMALL && (entity + && ((entity->sprite == 1822 || entity->sprite == 1822 + 1 || entity->sprite == 1822 + 2) + || (myStats && myStats->getAttribute("fire_sprite") != ""))) ) + { + return false; + } + if ( race == FLAME_ELEMENTAL || race == EARTH_ELEMENTAL ) { return false; } @@ -22614,15 +30812,40 @@ int monsterTinkeringConvertAppearanceToHP(Stat* myStats, int appearance) return 0; } -void Entity::handleQuiverThirdPersonModel(Stat& myStats) +void Entity::handleQuiverThirdPersonModel(Stat& myStats, int mySprite) { if ( multiplayer == CLIENT ) { return; } + + Monster monsterType = myStats.type; + if ( behavior == &actPlayer ) + { + if ( effectShapeshift != NOTHING ) + { + monsterType = static_cast(effectShapeshift); + } + else if ( myStats.playerRace > 0 || myStats.getEffectActive(EFF_POLYMORPH) || effectPolymorph != NOTHING ) + { + monsterType = getMonsterFromPlayerRace(myStats.playerRace); + if ( effectPolymorph != NOTHING ) + { + if ( effectPolymorph > NUMMONSTERS ) + { + monsterType = HUMAN; + } + else + { + monsterType = static_cast(effectPolymorph); + } + } + } + } + if ( !myStats.breastplate ) { - switch ( myStats.type ) + switch ( monsterType ) { case SKELETON: case AUTOMATON: @@ -22630,8 +30853,31 @@ void Entity::handleQuiverThirdPersonModel(Stat& myStats) break; case KOBOLD: case GNOME: + case GREMLIN: // no strap. break; + case DRYAD: + case MYCONID: + { + bool shortSprite = false; + if ( monsterType == DRYAD && (mySprite == 1514 || mySprite == 1515 || mySprite == 1992 || mySprite == 1993) ) + { + shortSprite = true; + } + if ( monsterType == MYCONID && (mySprite == 1520 || mySprite == 1998) ) + { + shortSprite = true; + } + if ( shortSprite ) + { + // no strap. + } + else + { + sprite += 1; // normal strap + } + break; + } default: sprite += 1; // normal strap break; @@ -22639,7 +30885,7 @@ void Entity::handleQuiverThirdPersonModel(Stat& myStats) } else { - switch ( myStats.type ) + switch ( monsterType ) { case SKELETON: case AUTOMATON: @@ -22647,8 +30893,31 @@ void Entity::handleQuiverThirdPersonModel(Stat& myStats) break; case KOBOLD: case GNOME: + case GREMLIN: // no strap. break; + case DRYAD: + case MYCONID: + { + bool shortSprite = false; + if ( monsterType == DRYAD && (mySprite == 1514 || mySprite == 1515 || mySprite == 1992 || mySprite == 1993) ) + { + shortSprite = true; + } + if ( monsterType == MYCONID && (mySprite == 1520 || mySprite == 1998) ) + { + shortSprite = true; + } + if ( shortSprite ) + { + // no strap. + } + else + { + sprite += 3; // shoulderpad-less. + } + break; + } default: sprite += 3; // shoulderpad-less. break; @@ -22676,11 +30945,49 @@ Sint32 Entity::playerInsectoidHungerValueOfManaPoint(Stat& myStats) return static_cast(1000 * manaPointPercentage); } -real_t Entity::getDamageTableMultiplier(Entity* my, Stat& myStats, DamageTableType damageType) +void Entity::playerInsectoidIncrementHungerToMP(int mpAmount) +{ + if ( mpAmount <= 0 ) + { + return; + } + if ( svFlags & SV_FLAG_HUNGER && multiplayer != CLIENT ) + { + if ( behavior == &actPlayer ) + { + if ( Stat* myStats = getStats() ) + { + if ( myStats->playerRace == RACE_INSECTOID && myStats->stat_appearance == 0 ) + { + Sint32 hungerPointPerMana = playerInsectoidHungerValueOfManaPoint(*myStats); + myStats->HUNGER += mpAmount * hungerPointPerMana; + myStats->HUNGER = std::min(999, myStats->HUNGER); + serverUpdateHunger(skill[2]); + } + } + } + } +} + +real_t Entity::getDamageTableEquipmentMod(Stat& myStats, Item& item, real_t base, real_t mod) +{ + real_t bonus = base; + if ( item.beatitude >= 0 || shouldInvertEquipmentBeatitude(&myStats) ) + { + bonus += mod * std::min(abs(item.beatitude), 3); + } + else + { + bonus = -base - mod * abs(item.beatitude); + } + + return bonus; +} + +real_t Entity::getDamageTableMultiplier(Entity* my, Stat& myStats, DamageTableType damageType, int* magicResistance, int* outNumSources) { real_t damageMultiplier = damagetables[myStats.type][damageType]; - real_t bonus = 0.0; - if ( myStats.EFFECTS[EFF_SHADOW_TAGGED] ) + if ( myStats.getEffectActive(EFF_SHADOW_TAGGED) ) { if ( myStats.type == LICH || myStats.type == LICH_FIRE || myStats.type == LICH_ICE || myStats.type == DEVIL ) @@ -22692,10 +30999,13 @@ real_t Entity::getDamageTableMultiplier(Entity* my, Stat& myStats, DamageTableTy return 1.0; } } + + std::vector allBonuses; + //messagePlayer(0, "%f", damageMultiplier); - if ( myStats.type == GOATMAN && myStats.EFFECTS[EFF_DRUNK] ) + if ( myStats.type == GOATMAN && myStats.getEffectActive(EFF_DRUNK) ) { - bonus = -.2; + allBonuses.push_back(-.2); } if ( damageType == DamageTableType::DAMAGE_TABLE_MAGIC && myStats.type == BUGBEAR && myStats.defending && myStats.shield ) @@ -22705,9 +31015,38 @@ real_t Entity::getDamageTableMultiplier(Entity* my, Stat& myStats, DamageTableTy int followerResist = my ? my->getFollowerBonusDamageResist() : 0; if ( followerResist != 0 ) { - bonus += -followerResist / 100.0; + allBonuses.push_back(-followerResist / 100.0); } + if ( damageType == DAMAGE_TABLE_MAGIC ) + { + if ( myStats.getEffectActive(EFF_RATION_HERBAL) ) + { + allBonuses.push_back(-0.2); + } + + if ( myStats.getEffectActive(EFF_GUARD_SPIRIT) || myStats.getEffectActive(EFF_DIVINE_GUARD) ) + { + real_t guardBonus = -0.1 * myStats.getEffectActive(EFF_GUARD_SPIRIT); + guardBonus = std::min(guardBonus, -0.025 * myStats.getEffectActive(EFF_DIVINE_GUARD)); + allBonuses.push_back(guardBonus); + } + } + + if ( damageType != DAMAGE_TABLE_MAGIC ) + { + if ( myStats.type == MYCONID ) + { + if ( !myStats.helmet && myStats.getEffectActive(EFF_GROWTH) > 1 ) + { + int bonus = std::min(3, myStats.getEffectActive(EFF_GROWTH) - 1); + allBonuses.push_back(-0.05 * bonus); + } + } + } + + allBonuses.push_back(-myStats.getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_TIER) / 100.0); + if ( myStats.cloak && myStats.cloak->type == CLOAK_GUARDIAN ) { real_t res = 0.25; @@ -22715,9 +31054,151 @@ real_t Entity::getDamageTableMultiplier(Entity* my, Stat& myStats, DamageTableTy { res = std::min(0.75, 0.25 + 0.25 * (abs(myStats.cloak->beatitude))); } - bonus -= res; + allBonuses.push_back(-res); + } + + if ( damageType == DAMAGE_TABLE_MAGIC ) + { + int resistance = magicResistance ? *magicResistance : Entity::getMagicResistance(&myStats); + for ( int i = 0; i < resistance; ++i, allBonuses.push_back(-Entity::magicResistancePerPoint) ) {} + } + + bool isPlayer = false; + bool shapeshifted = false; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( &myStats == stats[i] ) + { + if ( myStats.type == TROLL || myStats.type == RAT || myStats.type == SPIDER || myStats.type == CREATURE_IMP ) + { + shapeshifted = true; + } + isPlayer = true; + break; + } + } + + if ( !shapeshifted ) + { + if ( damageType == DAMAGE_TABLE_AXE || damageType == DAMAGE_TABLE_SWORD ) + { + if ( myStats.breastplate && myStats.breastplate->type == CHAIN_HAUBERK ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.breastplate, 0.2, 0.05)); + } + if ( myStats.gloves && myStats.gloves->type == CHAIN_GLOVES ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.gloves, 0.1, 0.05)); + } + if ( myStats.shoes && myStats.shoes->type == CHAIN_BOOTS ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.shoes, 0.1, 0.05)); + } + if ( myStats.helmet && myStats.helmet->type == CHAIN_COIF ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.helmet, 0.1, 0.05)); + } + } + if ( damageType == DAMAGE_TABLE_POLEARM || damageType == DAMAGE_TABLE_RANGED ) + { + if ( myStats.breastplate && myStats.breastplate->type == QUILTED_GAMBESON ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.breastplate, 0.2, 0.05)); + } + if ( myStats.gloves && myStats.gloves->type == QUILTED_GLOVES ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.gloves, 0.1, 0.05)); + } + if ( myStats.shoes && myStats.shoes->type == QUILTED_BOOTS ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.shoes, 0.1, 0.05)); + } + if ( myStats.helmet && myStats.helmet->type == QUILTED_CAP ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.helmet, 0.1, 0.05)); + } + } + if ( damageType == DAMAGE_TABLE_UNARMED || damageType == DAMAGE_TABLE_MACE ) + { + if ( myStats.breastplate && myStats.breastplate->type == BONE_BREASTPIECE ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.breastplate, 0.2, 0.05)); + } + if ( myStats.gloves && myStats.gloves->type == BONE_BRACERS ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.gloves, 0.1, 0.05)); + } + if ( myStats.shoes && myStats.shoes->type == BONE_BOOTS ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.shoes, 0.1, 0.05)); + } + if ( myStats.helmet && myStats.helmet->type == BONE_HELM ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.helmet, 0.1, 0.05)); + } + } + if ( damageType == DAMAGE_TABLE_MAGIC ) + { + if ( myStats.breastplate && myStats.breastplate->type == BANDIT_BREASTPIECE ) + { + allBonuses.push_back(-Entity::getDamageTableEquipmentMod(myStats, *myStats.breastplate, 0.15, 0.05)); + } + } + } + + //std::sort(allBonuses.begin(), allBonuses.end()); + + real_t multipliedBonuses = 1.0; + real_t summedExtraDamage = 0.0; + for ( auto val : allBonuses ) + { + if ( val > 0.01 ) // extra damage + { + summedExtraDamage += val; + } + else if ( val < -0.01 ) // less damage + { + multipliedBonuses *= (1 + val); + if ( outNumSources ) + { + *outNumSources += 1; + } + } + } + if ( isPlayer && damageType == DAMAGE_TABLE_MAGIC ) + { + Sint32 INT = std::min(90, statGetINT(&myStats, my)); + if ( INT > 0 ) + { + multipliedBonuses *= 1 - INT / 100.0; + } + } + if ( damageMultiplier > 1.0 ) + { + summedExtraDamage += std::max(0.0, damageMultiplier - 1.0); + } + else if ( damageMultiplier < 1.0 ) + { + multipliedBonuses *= 1 - std::max(1.0 - damageMultiplier, 0.0); + } + + real_t bonus = summedExtraDamage - (1.0 - floor(100.0 * multipliedBonuses) / 100.0); + + if ( myStats.getEffectActive(EFF_DIVINE_FIRE) ) + { + for ( int i = 0; i < (myStats.getEffectActive(EFF_DIVINE_FIRE) & 0xF); ++i ) + { + if ( bonus < 0.0 ) + { + bonus += 0.1; + } + else + { + bonus += 0.025; + } + } } - return std::max(0.1, damageMultiplier + bonus); + return std::max(0.1, 1.0 + bonus); } void Entity::createWorldUITooltip() @@ -22803,6 +31284,13 @@ bool Entity::bEntityHighlightedForPlayer(const int player) const { return true; } + if ( cast_animation[player].active && cast_animation[player].targetUid != 0 + && cast_animation[player].targetUid == getUID() + && players[player] && players[player]->hud.magicRangefinder + && !players[player]->hud.magicRangefinder->flags[INVISIBLE] ) + { + return true; + } if ( (behavior == &actMonster && !isInertMimic()) || behavior == &actPlayer ) { @@ -22943,11 +31431,15 @@ void Entity::alertAlliesOnBeingHit(Entity* attacker, std::unordered_set Stat* buddystats = entity->getStats(); if ( buddystats != nullptr ) { - if ( buddystats->type == SHOPKEEPER && hitstats->type != SHOPKEEPER ) + if ( buddystats->type == GYROBOT ) + { + continue; + } + if ( (buddystats->type == SHOPKEEPER || entity->monsterCanTradeWith(-1)) && hitstats->type != SHOPKEEPER ) { continue; // shopkeepers don't care about hitting humans/robots etc. } - if ( hitstats->type == SHOPKEEPER && entity->monsterAllyGetPlayerLeader() ) + if ( (hitstats->type == SHOPKEEPER || this->monsterCanTradeWith(-1)) && entity->monsterAllyGetPlayerLeader()) { continue; // hitting a shopkeeper, player followers won't retaliate against player } @@ -23053,7 +31545,7 @@ bool Entity::entityCanVomit() const } } - if ( myStats->type == SKELETON || myStats->type == AUTOMATON ) + if ( myStats->type == SKELETON || myStats->type == AUTOMATON || myStats->type == MYCONID ) { return false; } @@ -23063,7 +31555,7 @@ bool Entity::entityCanVomit() const return false; } - if ( myStats->EFFECTS[EFF_NAUSEA_PROTECTION] ) + if ( myStats->getEffectActive(EFF_NAUSEA_PROTECTION) ) { return false; } @@ -23109,53 +31601,98 @@ int Entity::getFollowerBonusDamageResist() return resist; } -int Entity::getHPRestoreOnLevelUp() +int Entity::getMPRestoreOnLevelUp(Entity* entity, Stat* myStats, int baseMP, bool statCheckOnly) { - int hpMod = HP_MOD; - - if ( Stat* myStats = getStats() ) + int mpMod = baseMP; + if ( !myStats ) + { + return mpMod; + } + if ( !entity || (entity && entity->behavior == &actPlayer) ) { - if ( myStats->helmet && myStats->helmet->type == HAT_CROWN ) + if ( statCheckOnly ) { - if ( myStats->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(myStats) ) - { - hpMod += (std::min(50, (20 + 10 * (abs(myStats->helmet->beatitude)))) / 100.0) * myStats->MAXHP; - } - else - { - hpMod = 0; - } + mpMod += std::max(0, myStats->CHR); } - if ( behavior == &actMonster ) + else { - Entity* leader = monsterAllyGetPlayerLeader(); - if ( !leader ) + mpMod += std::max(0, statGetCHR(myStats, entity)); + } + } + /*static int mpModCounter = 0; + if ( !statCheckOnly ) + { + mpModCounter += mpMod; + }*/ + return mpMod; +} + +int Entity::getHPRestoreOnLevelUp(Entity* entity, Stat* myStats, int baseHP, bool statCheckOnly) +{ + int hpMod = baseHP; + if ( !myStats ) + { + return hpMod; + } + + if ( myStats ) + { + if ( !statCheckOnly ) + { + if ( myStats->helmet && myStats->helmet->type == HAT_CROWN ) { - if ( myStats->leader_uid != 0 ) + if ( myStats->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(myStats) ) { - leader = uidToEntity(myStats->leader_uid); + hpMod += (std::min(50, (20 + 10 * (abs(myStats->helmet->beatitude)))) / 100.0) * myStats->MAXHP; + } + else + { + hpMod = 0; } } - if ( leader ) + if ( entity && entity->behavior == &actMonster ) { - if ( Stat* stat = leader->getStats() ) + Entity* leader = entity->monsterAllyGetPlayerLeader(); + if ( !leader ) { - if ( stat->helmet && - (stat->helmet->type == HAT_CROWN) ) + if ( myStats->leader_uid != 0 ) { - if ( stat->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(stat) ) - { - hpMod += 20 + (10 * (abs(stat->helmet->beatitude)) / 100.0) * myStats->MAXHP; - } - else + leader = uidToEntity(myStats->leader_uid); + } + } + if ( leader ) + { + if ( Stat* stat = leader->getStats() ) + { + if ( stat->helmet && + (stat->helmet->type == HAT_CROWN) ) { - hpMod = 0; + if ( stat->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(stat) ) + { + hpMod += 20 + (10 * (abs(stat->helmet->beatitude)) / 100.0) * myStats->MAXHP; + } + else + { + hpMod = 0; + } } } } } } + if ( !entity || (entity && entity->behavior == &actPlayer) ) + { + if ( statCheckOnly ) + { + hpMod += std::max(0, myStats->CHR); + } + else + { + hpMod += std::max(0, statGetCHR(myStats, entity)); + } + } } + return hpMod; } @@ -23194,7 +31731,33 @@ int Entity::getFollowerBonusHPRegen() } } } - return regen; + return regen; +} + +bool Entity::onEntityTrapHitSacredPath(Entity* trap) +{ + if ( Stat* myStats = getStats() ) + { + if ( Uint8 effectStrength = myStats->getEffectActive(EFF_SACRED_PATH) ) + { + int duration = myStats->EFFECTS_TIMERS[EFF_SACRED_PATH]; + if ( effectStrength == 1 ) + { + if ( myStats->EFFECTS_TIMERS[EFF_SACRED_PATH] > 0 ) + { + myStats->EFFECTS_TIMERS[EFF_SACRED_PATH] = 1; + } + } + else if ( effectStrength > 1 ) + { + --effectStrength; + myStats->setEffectValueUnsafe(EFF_SACRED_PATH, effectStrength); + setEffect(EFF_SACRED_PATH, effectStrength, myStats->EFFECTS_TIMERS[EFF_SACRED_PATH], false); + } + return true; + } + } + return false; } int Entity::getEntityBonusTrapResist() @@ -23242,6 +31805,10 @@ int Entity::getEntityBonusTrapResist() resist += 50; } } + if ( myStats->getEffectActive(EFF_SACRED_PATH) ) + { + resist = 100; + } } return std::min(100, std::max(-100, resist)); } @@ -23389,7 +31956,11 @@ bool Entity::doSilkenBowOnAttack(Entity* attacker) chance = 1; } - if ( roll < chance ) + if ( playerHit >= 0 ) + { + tryEffect = players[playerHit]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_SILKEN_BOW, chance); + } + else if ( roll < chance ) { tryEffect = true; } @@ -23403,9 +31974,7 @@ bool Entity::doSilkenBowOnAttack(Entity* attacker) chance -= difficulty * 30; // special cases: - if ( (attackerStats->type == VAMPIRE && MonsterData_t::nameMatchesSpecialNPCName(*attackerStats, "bram kindly")) - || (attackerStats->type == COCKATRICE && !strncmp(map.name, "Cockatrice Lair", 15)) - ) + if ( attacker->isBossMonster() ) { chance = 0; } @@ -23474,3 +32043,508 @@ bool Entity::doSilkenBowOnAttack(Entity* attacker) return false; } + +bool Entity::windEffectsEntity(Entity* entity) +{ + if ( !entity ) { return false; } + if ( multiplayer == CLIENT ) + { + if ( entity->behavior == &actPlayer && players[entity->skill[2]]->isLocalPlayer() ) + { + /*if ( stats[entity->skill[2]]->shoes && stats[entity->skill[2]]->shoes->type == CLEAT_BOOTS + && entity->effectShapeshift == NOTHING ) + { + return false; + }*/ + return true; + } + return false; + } + if ( entity->behavior == &actMonster ) + { + if ( entity->flags[PASSABLE] ) + { + return false; + } + if ( !entity->monsterIsTargetable() ) + { + return false; + } + if ( Stat* myStats = entity->getStats() ) + { + if ( myStats->type == LICH + || myStats->type == DEVIL + || myStats->type == LICH_FIRE + || myStats->type == LICH_ICE ) + { + return false; + } + /*if ( myStats->shoes && myStats->shoes->type == CLEAT_BOOTS ) + { + return false; + }*/ + } + if ( !entity->isUntargetableBat() && !entity->isInertMimic() ) + { + return true; + } + } + else if ( (entity->behavior == &actPlayer && players[entity->skill[2]]->isLocalPlayer()) + || (actWindEffectsProjectiles + && (entity->behavior == &actArrow + || entity->behavior == &actMagicMissile + || entity->behavior == &actThrown)) ) + { + /*if ( entity->behavior == &actPlayer ) + { + if ( stats[entity->skill[2]]->shoes && stats[entity->skill[2]]->shoes->type == CLEAT_BOOTS + && entity->effectShapeshift == NOTHING ) + { + return false; + } + }*/ + return true; + } + return false; +} + +void Entity::processEntityWind() +{ + static real_t redirectionStrength = 0.1; + if ( fabs(creatureWindVelocity) > 0.1 ) + { + if ( behavior == &actArrow ) + { + real_t dirx = cos(creatureWindDir - yaw); + real_t diry = sin(creatureWindDir - yaw); + real_t tangent = atan2(diry, dirx); + this->yaw += creatureWindVelocity * (tangent) * redirectionStrength; + } + else if ( behavior == &actMagicMissile ) + { + real_t dir = atan2(this->vel_y, this->vel_x); + real_t spd = sqrt(this->vel_x * this->vel_x + this->vel_y * this->vel_y); + real_t dirx = cos(creatureWindDir - dir); + real_t diry = sin(creatureWindDir - dir); + real_t tangent = atan2(diry, dirx); + dir += creatureWindVelocity * (tangent) * redirectionStrength; + this->vel_x = spd * cos(dir); + this->vel_y = spd * sin(dir); + this->yaw = dir; + } + else if ( behavior == &actThrown ) + { + real_t dir = atan2(this->vel_y, this->vel_x); + real_t spd = sqrt(this->vel_x * this->vel_x + this->vel_y * this->vel_y); + real_t dirx = cos(creatureWindDir - dir); + real_t diry = sin(creatureWindDir - dir); + real_t tangent = atan2(diry, dirx); + dir += creatureWindVelocity * (tangent) * redirectionStrength; + this->vel_x = spd * cos(dir); + this->vel_y = spd * sin(dir); + //this->yaw = dir; + } + else if ( behavior == &actMonster ) + { + int myDex = monsterGetDexterityForMovement(); + real_t weightratio = monsterGetWeightRatio(); + real_t maxVel = .045 * (myDex + 10) * weightratio; + + real_t velx = maxVel * 1.0 * creatureWindVelocity * cos(creatureWindDir); + real_t vely = maxVel * 1.0 * creatureWindVelocity * sin(creatureWindDir); + real_t dist = clipMove(&x, &y, velx, vely, this); + } + else + { + vel_x += creatureWindVelocity * cos(creatureWindDir); + vel_y += creatureWindVelocity * sin(creatureWindDir); + } + creatureWindVelocity *= 0.9; + } + else + { + creatureWindDir = 0.0f; + creatureWindVelocity = 0.0f; + } +} + +real_t Entity::monsterGetWeightRatio() +{ + Stat* myStats = getStats(); + if ( !myStats ) { return 1.0; } + Sint32 weight = 0; + if ( myStats->helmet != NULL ) + { + weight += myStats->helmet->getWeight(); + } + if ( myStats->breastplate != NULL ) + { + weight += myStats->breastplate->getWeight(); + } + if ( myStats->gloves != NULL ) + { + weight += myStats->gloves->getWeight(); + } + if ( myStats->shoes != NULL ) + { + weight += myStats->shoes->getWeight(); + } + if ( myStats->shield != NULL ) + { + weight += myStats->shield->getWeight(); + } + if ( myStats->weapon != NULL ) + { + weight += myStats->weapon->getWeight(); + } + if ( myStats->cloak != NULL ) + { + weight += myStats->cloak->getWeight(); + } + if ( myStats->amulet != NULL ) + { + weight += myStats->amulet->getWeight(); + } + if ( myStats->ring != NULL ) + { + weight += myStats->ring->getWeight(); + } + if ( myStats->mask != NULL ) + { + weight += myStats->mask->getWeight(); + } + weight += myStats->getGoldWeight(); + weight /= 2; // on monsters weight shouldn't matter so much + double weightratio = (1000 + std::max((Sint32)0, getSTR()) * 100 - weight) / (double)(1000 + std::max((Sint32)0, getSTR()) * 100); + weightratio = fmin(fmax(0, weightratio), 1); + return weightratio; +} + +void Entity::creatureHandleLiftZ() +{ + Monster type = getMonsterTypeFromSprite(); + Stat* myStats = getStats(); + real_t shiftMult = 1.0; + if ( myStats && (myStats->getEffectActive(EFF_LIFT)) ) + { + creatureHoverZ += 0.25; + } + else if ( myStats && (myStats->getEffectActive(EFF_STASIS)) ) + { + creatureHoverZ += 0.025; + } + else if ( myStats && type == SALAMANDER + && myStats->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && myStats->getEffectActive(EFF_SALAMANDER_HEART) <= 2 ) + { + creatureHoverZ += 0.025; + shiftMult = 0.5; + } + else + { + creatureHoverZ = 0.0; + } + + real_t height = 2.0 * sin(std::min(creatureHoverZ, PI / 2)); + if ( creatureHoverZ >= PI / 2 ) + { + height += 0.5 * cos(creatureHoverZ); + } + + real_t shift = 2 * height * shiftMult; + + if ( multiplayer == CLIENT && behavior == &actMonster ) + { + switch ( type ) + { + case RAT: + focalz = 0.0; + focalz -= shift; + break; + case SLIME: + focalz -= shift; + break; + case SCARAB: + z -= shift; + new_z = z; + break; + default: + break; + } + return; + } + + switch ( type ) + { + case RAT: + focalz = 0.0; + focalz -= shift; + break; + case SLIME: + focalz -= shift; + break; + case SCARAB: + z -= shift; + new_z = z; + break; + case SPIDER: + case SCORPION: + case GNOME: + case KOBOLD: + case MINIMIMIC: + z -= shift; + break; + case HUMAN: + case GOBLIN: + case TROLL: + case SKELETON: + case SUCCUBUS: + case SHOPKEEPER: + case INCUBUS: + case INSECTOID: + case VAMPIRE: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case GOATMAN: + case GHOUL: + case AUTOMATON: + case MIMIC: + case HOLOGRAM: + case SENTRYBOT: + case SPELLBOT: + case DUMMYBOT: + z -= shift * 0.75; + break; + case BAT_SMALL: + case REVENANT_SKULL: + case MONSTER_ADORCISED_WEAPON: + case FLAME_ELEMENTAL: + case MOTH_SMALL: + case EARTH_ELEMENTAL: + z -= shift / 2; + break; + case BUGBEAR: + case SHADOW: + case CRYSTALGOLEM: + case DEMON: + z -= shift / 2; + break; + case CREATURE_IMP: + case COCKATRICE: + case MINOTAUR: + z -= shift / 4; + break; + case LICH: + case DEVIL: + case LICH_ICE: + case LICH_FIRE: + break; + case GYROBOT: + break; + case DUCK_SMALL: + break; + case MONSTER_UNUSED_6: + case MONSTER_UNUSED_7: + case MONSTER_UNUSED_8: + default: + break; + } +} + +bool Entity::degradeAmuletProc(Stat* myStats, ItemType type) +{ + if ( !myStats || (myStats && !myStats->amulet) ) { return false; } + int player = -1; + if ( behavior == &actPlayer ) + { + player = this->skill[2]; + } + if ( myStats->amulet && myStats->amulet->type == type && (type == AMULET_POISONRESISTANCE || type == AMULET_BURNINGRESIST) ) + { + if ( myStats->amulet->status > BROKEN ) + { + if ( ::ticks - myStats->itemLastDegradeTick[myStats->amulet->type] < 3 * TICKS_PER_SECOND ) + { + return false; + } + myStats->itemLastDegradeTick[myStats->amulet->type] = ::ticks; + + int chance = 8 + 4 * (shouldInvertEquipmentBeatitude(myStats) ? abs(myStats->amulet->beatitude) : myStats->amulet->beatitude); + if ( chance > 0 && local_rng.rand() % std::max(chance, 1) == 0 && !this->spellEffectPreserveItem(myStats->amulet) ) + { + if ( player >= 0 && type == AMULET_BURNINGRESIST ) + { + messagePlayer(player, MESSAGE_EQUIPMENT | MESSAGE_HINT, Language::get(6867)); + } + + myStats->amulet->count = 1; + myStats->amulet->status = static_cast(std::max(static_cast(BROKEN), myStats->amulet->status - 1)); + if ( myStats->amulet->status != BROKEN ) + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(382)); + } + else + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(383)); + playSoundEntity(this, 76, 64); + if ( player >= 0 ) + { + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, myStats->amulet->type, 1); + } + } + + if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "ARMR"); + net_packet->data[4] = 7; // amulet + net_packet->data[5] = myStats->amulet->status; + SDLNet_Write16((int)myStats->amulet->type, &net_packet->data[6]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + return true; + } + } + } + + return false; +} + +bool Entity::myconidReboundOnHit(Entity* attacker) +{ + if ( !attacker ) + { + return false; + } + Stat* myStats = getStats(); + if ( !myStats ) { return false; } + if ( behavior == &actPlayer + && myStats->type == MYCONID + && !myStats->helmet + && myStats->sex == MALE + && myStats->getEffectActive(EFF_GROWTH) > 1 ) + { + if ( !isMobile() ) + { + return true; + } + real_t yawDiff = this->yawDifferenceFromEntity(attacker); + if ( yawDiff >= 0 && yawDiff < 4 * PI / 5 ) + { + return true; + } + } + + return false; +} + +bool Entity::modifyDamageMultipliersFromEffects(Entity* hitentity, Entity* attacker, + real_t& damageMultiplier, DamageTableType damageTableType, Entity* projectile, int spellID) +{ + if ( !hitentity ) { return false; } + + Stat* hitstats = hitentity->getStats(); + if ( !hitstats ) { return false; } + + Stat* attackerStats = attacker ? attacker->getStats() : nullptr; + bool result = false; + if ( hitstats->getEffectActive(EFF_BLOOD_WARD) ) + { + real_t reduction = std::min(getSpellDamageSecondaryFromID(SPELL_BLOOD_WARD, hitentity, nullptr, hitentity) / 100.0, std::max(0.0, getSpellDamageFromID(SPELL_BLOOD_WARD, hitentity, nullptr, hitentity) / 100.0)); + if ( attackerStats ) + { + if ( (attackerStats && attackerStats->type == SPIDER + || attackerStats->type == SCORPION + || attackerStats->type == INSECTOID + || attackerStats->type == MYCONID) + || spellID == SPELL_POISON + || spellID == SPELL_ACID_SPRAY + || spellID == SPELL_SLIME_ACID + || spellID == SPELL_SPORES + || spellID == SPELL_SPORE_BOMB + || spellID == SPELL_MYCELIUM_BOMB + || spellID == SPELL_MYCELIUM_SPORES + || spellID == SPELL_BLEED + || spellID == SPELL_MERCURY_BOLT + || spellID == SPELL_MUSHROOM + || (projectile && projectile->behavior == &actArrow && projectile->arrowQuiverType == QUIVER_HUNTING) + ) + { + damageMultiplier = std::max(0.1, damageMultiplier * (1.0 - reduction)); + if ( hitentity->behavior == &actPlayer ) + { + players[hitentity->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_BLOOD_WARD, 30.0, 1.0, attacker); + hitentity->safeConsumeMP(1); + } + result = true; + } + } + } + if ( hitstats->getEffectActive(EFF_SIGIL) ) + { + int caster = ((hitstats->getEffectActive(EFF_SIGIL) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( hitentity->behavior == &actMonster + && !hitentity->monsterAllyGetPlayerLeader() ) + { + damageMultiplier += 0.1 + (0.1 * (int)(hitstats->getEffectActive(EFF_SIGIL) & 0xF)); + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_SIGIL, 30.0, 1.0, hitentity); + } + result = true; + } + } + } + if ( hitstats->getEffectActive(EFF_SANCTUARY) ) + { + real_t reduction = std::min(0.8, std::max(0.0, 0.1 + (0.15 * (int)(hitstats->getEffectActive(EFF_SANCTUARY) & 0xF)))); + damageMultiplier = std::max(0.1, damageMultiplier * (1.0 - reduction)); + + int caster = ((hitstats->getEffectActive(EFF_SANCTUARY) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_SANCTUARY, 30.0, 1.0, hitentity); + players[caster]->entity->safeConsumeMP(1); + } + } + result = true; + } + + return result; +} + +real_t Entity::getHealingSpellPotionModifierFromEffects(bool processLevelup) +{ + real_t result = 1.0; + if ( Stat* myStats = getStats() ) + { + if ( myStats->getEffectActive(EFF_SIGIL) ) + { + int caster = ((myStats->getEffectActive(EFF_SIGIL) >> 4) & 0xF) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( (behavior == &actMonster + && monsterAllyGetPlayerLeader()) || behavior == &actPlayer ) + { + result += 0.1 + (0.1 * (int)(myStats->getEffectActive(EFF_SIGIL) & 0xF)); + if ( processLevelup ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_SIGIL, 30.0, 1.0, nullptr); + } + } + } + } + } + } + + return result; +} \ No newline at end of file diff --git a/src/entity.hpp b/src/entity.hpp index 4d3fcb99e..f6b4c2015 100644 --- a/src/entity.hpp +++ b/src/entity.hpp @@ -36,6 +36,8 @@ #define INVISIBLE_DITHER 16 #define NOCLIP_WALLS 17 #define NOCLIP_CREATURES 18 +#define ENTITY_SKIP_CULLING 19 +#define STASIS_DITHER 20 // number of entity skills and fskills static const int NUMENTITYSKILLS = 60; @@ -64,6 +66,8 @@ class Entity Sint32& orbHoverDirection; // animation, waiting/up/down floating state skill[7] Sint32& orbHoverWaitTimer; // animation, if waiting state, then wait this many ticks before moving to next state skill[8] + Sint32& entityShowOnMap; //skill[59] + //### Begin - Private Entity Constants for BURNING Status Effect static const Sint32 MIN_TICKS_ON_FIRE = TICKS_TO_PROCESS_FIRE * 4; // Minimum time an Entity can be on fire is 4 cycles (120 ticks) static const Sint32 MAX_TICKS_ON_FIRE = TICKS_TO_PROCESS_FIRE * 20; // Maximum time an Entity can be on fire is 20 cycles (600 ticks) @@ -93,6 +97,7 @@ class Entity ~Entity(); bool ditheringDisabled = false; + int ditheringOverride = -1; struct Dither { int value = 0; Uint32 lastUpdateTick = 0; @@ -123,6 +128,28 @@ class Entity #endif } + void setEntityString(const char* str) + { + if ( string ) + { + free(string); + string = nullptr; + } + if ( !str ) { return; } + size_t len = sizeof(char) * (strlen(str) + 1); + if ( string = (char*)malloc(len) ) + { + memset(string, 0, len); + stringCopy(string, str, len, strlen(str)); + } + } + + bool entityHasString(const char* str) + { + if ( !string ) { return false; } + return (!strcmp(string, str) ? true : false); + } + Uint32 getUID() const {return uid;} void setUID(Uint32 new_uid); Uint32 ticks; // duration of the entity's existence @@ -203,6 +230,7 @@ class Entity Sint32& chestLockpickHealth; // skill[12] Sint32& chestOldHealth; //skill[15] Sint32& chestMimicChance; //skill[16] + Sint32& chestVoidState = skill[17]; Sint32& char_gonnavomit; // skill[26] Sint32& char_heal; // skill[22] @@ -256,12 +284,14 @@ class Entity Sint32& monsterIllusionTauntingThisUid; //skill[55] Sint32& monsterLastDistractedByNoisemaker;//skill[55] shared with above as above only is for inner demons. Sint32& monsterExtraReflexTick; //skill[56] - Sint32& entityShowOnMap; //skill[59] real_t& monsterSentrybotLookDir; //fskill[10] real_t& monsterKnockbackTangentDir; //fskill[11] real_t& playerStrafeVelocity; //fskill[12] real_t& playerStrafeDir; //fskill[13] real_t& monsterSpecialAttackUnequipSafeguard; //fskill[14] + real_t& creatureWindDir; //fskill[15] + real_t& creatureWindVelocity; //fskill[16] + real_t& creatureHoverZ; //fskill[17] //--EFFECTS-- Sint32& effectPolymorph; // skill[50] @@ -273,6 +303,7 @@ class Entity real_t& highlightForUIGlow; //fskill[28] for highlighting animation real_t& grayscaleGLRender; //fskill[27] for grayscale rendering real_t& noColorChangeAllyLimb; // fskill[26] for ignoring recolor of follower limbs + real_t& mistformGLRender = fskill[22]; //--PUBLIC PLAYER SKILLS-- Sint32& playerLevelEntrySpeech; //skill[18] @@ -280,6 +311,7 @@ class Entity Sint32& playerVampireCurse; //skill[51] Sint32& playerAutomatonDeathCounter; //skill[15] - 0 if unused, > 0 if counting to death Sint32& playerCreatedDeathCam; //skill[16] - if we triggered actDeathCam already. + Sint32& playerCastTimeAnim = skill[17]; // how many ticks we're casting for in the current animation //--PUBLIC MONSTER ANIMATION SKILLS-- Sint32& monsterAnimationLimbDirection; //skill[20] @@ -336,6 +368,9 @@ class Entity Sint32& boulderTrapPreDelay; //skill[5] Sint32& boulderTrapRocksToSpawn; //skill[7] bitwise storage. + Sint32& boulderShatterEarthSpell = skill[16]; + Sint32& boulderShatterEarthDamage = skill[17]; + //--PUBLIC AMBIENT PARTICLE EFFECT SKILLS-- Sint32& particleDuration; //skill[0] Sint32& particleShrink; //skill[1] @@ -350,6 +385,9 @@ class Entity Sint32& particleTimerPreDelay; //skill[7] Sint32& particleTimerVariable1; //skill[8] Sint32& particleTimerVariable2; //skill[9] + Sint32& particleTimerEffectLifetime = skill[10]; + Sint32& particleTimerVariable3 = skill[11]; + Sint32& particleTimerVariable4 = skill[12]; //--PUBLIC DOOR SKILLS-- Sint32& doorDir; //skill[0] @@ -368,6 +406,7 @@ class Entity Sint32& doorDisableOpening; //skill[13] Sint32& doorLockpickHealth; //skill[14] Sint32& doorOldHealth; //skill[15] + Sint32& doorUnlockWhenPowered; //skill[16] //--PUBLIC PEDESTAL SKILLS-- Sint32& pedestalHasOrb; //skill[0] @@ -409,6 +448,10 @@ class Entity Sint32& teleporterY; //skill[1] Sint32& teleporterType; //skill[3] Sint32& teleporterAmbience; //skill[4] + Sint32& teleporterStartFrame = skill[5]; + Sint32& teleporterCurrentFrame = skill[6]; + Sint32& teleporterNumFrames = skill[7]; + Sint32& teleporterDuration = skill[8]; //--PUBLIC CEILING TILE SKILLS-- Sint32& ceilingTileModel; //skill[0] @@ -422,6 +465,8 @@ class Entity Sint32& floorDecorationHeightOffset; //skill[3] positive numbers will lift the model higher Sint32& floorDecorationXOffset; //skill[4] Sint32& floorDecorationYOffset; //skill[5] + Sint32& floorDecorationDestroyIfNoWall; //skill[6] + Sint32& floorDecorationDialogueProgress = skill[7]; // for players interacting with a dialogue bubble progress on clicking, unused Sint32& floorDecorationInteractText1; //skill[8] Sint32& floorDecorationInteractText2; //skill[9] Sint32& floorDecorationInteractText3; //skill[10] @@ -449,6 +494,17 @@ class Entity Sint32& colliderContainedEntity; //skill[15] Sint32& colliderHideMonster; //skill[16] Sint32& colliderKillerUid; //skill[17] + Sint32& colliderSpellEvent = skill[18]; + Sint32& colliderSpellEventCooldown = skill[19]; + Sint32& colliderCreatedParent = skill[20]; + Sint32& colliderSpellEventTrigger = skill[21]; + Sint32& colliderIsMapGenerated = skill[22]; + Sint32& colliderSpellTarget = skill[23]; + Sint32& colliderTelepathy = skill[24]; + Sint32& colliderDropVariable = skill[25]; // store germinate drop qtys + static void colliderAssignProperties(Entity* entity, bool mapGeneration, map_t* whichMap); + static Entity* createBreakableCollider(int colliderDamageType, real_t _x, real_t _y, Entity* parent); + void colliderSetServerSkillOnSpawned(); //--PUBLIC SPELL TRAP SKILLS-- Sint32& spellTrapType; //skill[0] @@ -519,7 +575,12 @@ class Entity Sint32& itemAutoSalvageByPlayer; //skill[26] Sint32& itemSplooshed; //skill[27] Sint32& itemContainer; //skill[29] + Sint32& itemFollowUID = skill[30]; + Sint32& itemReturnUID = skill[31]; + Sint32& itemGerminateResult = skill[32]; real_t& itemWaterBob; //fskill[2] + real_t& itemLevitate = fskill[3]; + real_t& itemLevitateStartZ = fskill[4]; //--PUBLIC ACTMAGIC SKILLS (Standard projectiles)-- Sint32& actmagicIsVertical; //skill[6] @@ -537,6 +598,9 @@ class Entity real_t& actmagicOrbitStationaryY; // fskill[5] real_t& actmagicOrbitStationaryCurrentDist; // fskill[6] real_t& actmagicSprayGravity; // fskill[7] + real_t& actmagicVelXStore; // fskill[8] + real_t& actmagicVelYStore; // fskill[9] + real_t& actmagicVelZStore; // fskill[10] Sint32& actmagicOrbitStationaryHitTarget; // skill[14] Sint32& actmagicOrbitHitTargetUID1; // skill[15] Sint32& actmagicOrbitHitTargetUID2; // skill[16] @@ -550,6 +614,30 @@ class Entity Sint32& actmagicFromSpellbook; // skill[26] Sint32& actmagicSpray; // skill[27] Sint32& actmagicEmitter; // skill[29] + Sint32& actmagicDelayMove; // skill[30] + Sint32& actmagicNoHitMessage; // skill[31] + Sint32& actmagicNoParticle; // skill[32] + Sint32& actmagicNoLight; // skill[33] + Sint32& actmagicUpdateOLDHPOnHit = skill[34]; + Sint32& actmagicAllowFriendlyFireHit = skill[35]; + Sint32& actmagicAdditionalDamage = skill[38]; // extra damage bonus from external sources like windgate + + Sint32& actfloorMagicType = skill[3]; + Sint32& actfloorMagicClientReceived = skill[4]; + + Sint32& actRadiusMagicID = skill[1]; + Sint32& actRadiusMagicInit = skill[3]; + Sint32& actRadiusMagicDist = skill[4]; + Sint32& actRadiusMagicFollowUID = skill[5]; + Sint32& actRadiusMagicDoPulseTick = skill[6]; + Sint32& actRadiusMagicAutoPulseTick = skill[7]; + Sint32& actRadiusMagicEffectPower = skill[8]; + + Sint32& actParticleWaveStartFrame = skill[4]; + Sint32& actParticleWaveLight = skill[7]; + Sint32& actParticleWaveMagicType = skill[9]; + Sint32& actParticleWaveClientReceived = skill[10]; + Sint32& actParticleWaveVariable1 = skill[11]; //--PUBLIC GOLD SKILLS-- Sint32& goldAmount; //skill[0] @@ -557,6 +645,9 @@ class Entity Sint32& goldSokoban; //skill[2] Sint32& goldBouncing; //skill[3] Sint32& goldInContainer; //skill[4] + Sint32& goldTelepathy = skill[5]; + Sint32& goldAmountBonus = skill[6]; + Sint32& goldDroppedByPlayer = skill[7]; //--PUBLIC SOUND SOURCE SKILLS-- Sint32& soundSourceFired; //skill[0] @@ -576,6 +667,7 @@ class Entity Sint32& lightSourceFlicker; //skill[5] Sint32& lightSourceDelay; //skill[6] Sint32& lightSourceDelayCounter;//skill[7] + Sint32& lightSourceRGB;//skill[11] //--PUBLIC TEXT SOURCE SKILLS-- Sint32& textSourceColorRGB; //skill[0] @@ -593,15 +685,34 @@ class Entity Sint32& signalGateANDPowerCount; //skill[9] Sint32& signalInvertOutput; //skill[10] + //--PUBLIC LOCK SKILLS-- + Sint32& wallLockState; //skill[0] + Sint32& wallLockInvertPower; //skill[1] + Sint32& wallLockTurnable; //skill[3] + Sint32& wallLockMaterial; //skill[4] + Sint32& wallLockDir; //skill[5] + Sint32& wallLockClientInteractDelay; //skill[6] + Sint32& wallLockPlayerInteracting; //skill[7] + Sint32& wallLockPower; //skill[8] + Sint32& wallLockInit; //skill[9] + Sint32& wallLockTimer; //skill[10] + Sint32& wallLockPickable; //skill[11] + Sint32& wallLockPickHealth; //skill[12] + Sint32& wallLockPickableSkeletonKey; //skill[13] + Sint32& wallLockPreventLockpickExploit; //skill[14] + Sint32& wallLockAutoGenKey; //skill[15] + //--THROWN PROJECTILE-- Sint32& thrownProjectilePower; //skill[19] Sint32& thrownProjectileCharge; //skill[20] + Sint32& thrownProjectileParticleTimerUID = skill[9]; //--PLAYER SPAWN POINT-- Sint32& playerStartDir; //skill[1] //--ACTTRAP/PERMANENT - Sint32 pressurePlateTriggerType; //skill[3] + Sint32& pressurePlateTriggerType; //skill[3] + enum PressurePlateTriggerTypes : int { PRESSURE_PLATE_DEFAULT_ALL, @@ -615,6 +726,58 @@ class Entity PRESSURE_PLATE_ENUM_END }; + enum WallLockStates + { + LOCK_NO_KEY, + LOCK_KEY_START, + LOCK_KEY_ENTER, + LOCK_KEY_ACTIVE_START, + LOCK_KEY_ACTIVE, + LOCK_KEY_INACTIVE_START, + LOCK_KEY_INACTIVE + }; + + enum EntityShowMapSource + { + SHOW_MAP_DEFAULT = 1, + SHOW_MAP_GYRO = 2, + SHOW_MAP_SCRY = 3, + SHOW_MAP_DONATION = 4, + SHOW_MAP_PINPOINT = 5, + SHOW_MAP_DETECT_MONSTER = 6 + }; + void setEntityShowOnMap(EntityShowMapSource source, int duration) + { + entityShowOnMap = 0; + entityShowOnMap |= ((int)source & 0xFF) << 24; + entityShowOnMap |= duration & 0xFFFFFF; + } + void entityShowOnMapTickDuration() + { + auto duration = getEntityShowOnMapDuration(); + auto source = getEntityShowOnMapSource(); + if ( duration > 0 ) + { + --duration; + } + if ( duration == 0 ) + { + entityShowOnMap = 0; + } + else + { + setEntityShowOnMap(source, duration); + } + } + int getEntityShowOnMapDuration() + { + return (EntityShowMapSource)(entityShowOnMap & 0xFFFFFF); + } + EntityShowMapSource getEntityShowOnMapSource() + { + return (EntityShowMapSource)((entityShowOnMap >> 24) & 0xFF); + } + //--WORLDTOOLTIP-- real_t& worldTooltipAlpha; //fskill[0] real_t& worldTooltipZ; //fskill[1] @@ -630,6 +793,33 @@ class Entity Sint32& statueDir; //skill[1] Sint32& statueId; //skill[3] + // new references, just set the skill here + + // actSprite + Sint32& actSpriteUseAlpha = skill[6]; + Sint32& actSpriteNoBillboard = skill[7]; + Sint32& actSpriteCheckParentExists = skill[8]; + //Sint32& actSpriteAlwaysDraw = skill[9]; + Sint32& actSpriteUseCustomSurface = skill[10]; + Sint32& actSpriteFollowUID = skill[11]; + Sint32& actSpriteHasLightInit = skill[12]; + Sint32& actSpriteVelXY = skill[13]; + real_t& actSpritePitchRotate = fskill[4]; + + // actGib + Sint32& actGibHitGroundEvent = skill[10]; + Sint32& actGibMagicParticle = skill[12]; // skill[11] is player hud denote + Sint32& actGibDisableDrawForLocalPlayer = skill[13]; // set to 1 + playernum, won't draw for that playernum + + // actWind + Sint32& actWindParticleEffect = skill[1]; + Sint32& actWindEffectsProjectiles = skill[3]; + Sint32& actWindLifetime = skill[4]; + real_t& actWindStrength = fskill[0]; + Sint32& actWindTileBonusLength = skill[5]; + + Sint32& actTrapSabotaged = skill[30]; + void pedestalOrbInit(); // init orb properties // a pointer to the entity's location in a list (ie the map list of entities) @@ -651,7 +841,7 @@ class Entity void setObituary(const char* obituary); - void killedByMonsterObituary(Entity* victim); + void killedByMonsterObituary(Entity* victim, bool fromSpell = false); Sint32 getSTR(); Sint32 getDEX(); @@ -677,17 +867,20 @@ class Entity int getHP(); void setMP(int amount, bool updateClients = true); - void modMP(int amount, bool updateClients = true); //Adds amount to MP. + int modMP(int amount, bool updateClients = true); //Adds amount to MP. int getMP(); void drainMP(int amount, bool notifyOverexpend = true); //Removes this much from MP. Anything over the entity's MP is subtracted from their health. Can be very dangerous. bool safeConsumeMP(int amount); //A function for the magic code. Attempts to remove mana without overdrawing the player. Returns true if success, returns false if didn't have enough mana. - static Sint32 getAttack(Entity* my, Stat* myStats, bool isPlayer = false); + static real_t PlayerAttackMeleeStatFactor; + static real_t PlayerAttackRangedStatFactor; + static real_t PlayerAttackThrownStatFactor; + static Sint32 getAttack(Entity* my, Stat* myStats, bool isPlayer, int chargeModifier = -1, int* returnWeaponAttackValue = nullptr); static real_t getACEffectiveness(Entity* my, Stat* myStats, bool isPlayer, Entity* attacker, Stat* attackerStats, int& outNumBlessings); static void setMeleeDamageSkillModifiers(Entity* my, Stat* myStats, int skill, real_t& baseSkillModifier, real_t& variance, ItemType* itemType); Sint32 getBonusAttackOnTarget(Stat& hitstats); - Sint32 getRangedAttack(); + Sint32 getRangedAttack(int atkFromQuivers); Sint32 getThrownAttack(); bool isBlind(); bool isWaterWalking() const; @@ -700,7 +893,7 @@ class Entity void attack(int pose, int charge, Entity* target); bool teleport(int x, int y); - bool teleportRandom(); + bool teleportRandom(int x1 = 0, int x2 = 0, int y1 = 0, int y2 = 0); // arbitrary map limits variables // teleport entity to a target, within a radius dist (range in whole tile lengths) bool teleportAroundEntity(Entity* target, int dist, int effectType = 0); // teleport entity to fixed position with appropriate sounds, for actTeleporter. @@ -734,7 +927,7 @@ class Entity Item** shouldMonsterEquipThisArmor(const Item& item) const; int shouldMonsterDefend(Stat& myStats, const Entity& target, const Stat& targetStats, int targetDist, bool hasrangedweapon); bool monsterConsumeFoodEntity(Entity* food, Stat* myStats); - Entity* monsterAllyGetPlayerLeader(); + Entity* monsterAllyGetPlayerLeader() const; bool monsterAllyEquipmentInClass(const Item& item) const; bool monsterIsTinkeringCreation(); void monsterHandleKnockbackVelocity(real_t monsterFacingTangent, real_t weightratio); @@ -758,23 +951,28 @@ class Entity void closeChest(); void closeChestServer(); //Close the chest serverside, silently. Called when the chest is closed somewhere else for that client, but the server end stuff needs to be tied up. Item* addItemToChest(Item* item, bool forceNewStack, Item* specificDestinationStack); //Adds an item to the chest. If server, notifies the client. If client, notifies the server. + static Item* addItemToVoidChest(int player, Item* item, bool forceNewStack, Item* specificDestinationStack); //Adds an item to the chest. If client, notifies the server. + static Item* addItemToVoidChestServer(int player, Item* item, bool forceNewStack, Item* specificDestinationStack); Item* getItemFromChest(Item* item, int amount, bool getInfoOnly = false); //Removes an item from the chest and returns a pointer to it. Item* addItemToChestFromInventory(int player, Item* item, int amount, bool forceNewStack, Item* specificDestinationStack); Item* addItemToChestServer(Item* item, bool forceNewStack, Item* specificDestinationStack); //Adds an item to the chest. Called when the server receives a notification from the client that an item was added to the chest. bool removeItemFromChestServer(Item* item, int count); //Called when the server learns that a client removed an item from the chest. + static bool removeItemFromVoidChestServer(int player, Item* item, int count); //Called when the server learns that a client removed an item from the chest. void unlockChest(); void lockChest(); - void chestHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster); + list_t* getChestInventoryList(); + void chestHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster, bool doSound = true); //Power Crystal functions. void powerCrystalCreateElectricityNodes(); //Door functions. - void doorHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster); - void colliderHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster); + void doorHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster, bool messages = true, bool doSound = true); + void colliderHandleDamageMagic(int damage, Entity &magicProjectile, Entity *caster, bool messages = true, bool doSound = true); bool checkEnemy(Entity* your); bool checkFriend(Entity* your); + bool friendlyFireProtection(Entity* your); void alertAlliesOnBeingHit(Entity* attacker, std::unordered_set* skipEntitiesToAlert = nullptr); //Act functions. @@ -794,6 +992,7 @@ class Entity bool magicFallingCollision(); bool magicOrbitingCollision(); void actFurniture(); + void furnitureHandleDamageMagic(int damage, Entity& magicProjectile, Entity* caster, bool messages = true, bool doSound = true); void actPistonCam(); void actStalagCeiling(); void actStalagFloor(); @@ -804,6 +1003,10 @@ class Entity void actTextSource(); void actSignalTimer(); void actSignalGateAND(); + void actWallLock(); + void actWallButton(); + void actIronDoor(); + void actWind(); Monster getRace() const { @@ -839,6 +1042,7 @@ class Entity static Monster getMonsterTypeFromSprite(const int sprite); //--monster limb offsets void setHelmetLimbOffset(Entity* helm); + void setTorsoLimbOffset(Entity* torso); void setHumanoidLimbOffset(Entity* limb, Monster race, int limbType); void actMonsterLimb(bool processLight = false); @@ -851,7 +1055,7 @@ class Entity // monster attack pose, return the animation to use based on weapon. int getAttackPose() const; // if monster holding ranged weapon. - bool hasRangedWeapon() const; + bool hasRangedWeapon(bool ignoreMonsterNPCType = false) const; // weapon arm animation attacks void handleWeaponArmAttack(Entity* weaponarm); // handle walking movement for arms and legs @@ -861,9 +1065,10 @@ class Entity // handle humanoid weapon arm animation/sprite offsets void handleHumanoidWeaponLimb(Entity* weaponLimb, Entity* weaponArmLimb); void handleHumanoidShieldLimb(Entity* shieldLimb, Entity* shieldArmLimb); - void handleQuiverThirdPersonModel(Stat& myStats); + void handleQuiverThirdPersonModel(Stat& myStats, int mySprite = -1); // server only function to set boot sprites on monsters. - bool setBootSprite(Entity* leg, int spriteOffset); + bool setBootSprite(Entity* leg, int spriteOffset, bool forceShort = false); + static bool isBootSpriteShortArmor(Entity* leg); // monster special attack handler, returns true if monster should attack after calling this function. bool handleMonsterSpecialAttack(Stat* myStats, Entity* target, double dist, bool forceDeinit); // monster attack handler @@ -889,7 +1094,7 @@ class Entity // check for nearby items to add to monster's inventory, returns true if picked up item bool monsterAddNearbyItemToInventory(Stat* myStats, int rangeToFind, int maxInventoryItems, Entity* forcePickupItem = nullptr); // degrade chosen armor piece by 1 on entity, update clients. - void degradeArmor(Stat& hitstats, Item& armor, int armornum); + bool degradeArmor(Stat& hitstats, Item& armor, int armornum); // check stats if monster should "retreat" in actMonster bool shouldRetreat(Stat& myStats); // check if monster should retreat or stand still when less than given distance @@ -897,11 +1102,11 @@ class Entity // calc time required for a mana regen tick, uses equipped gear as modifiers. static int getManaringFromEquipment(Entity* my, Stat& myStats, bool isPlayer); static int getManaringFromEffects(Entity* my, Stat& myStats); - static int getManaRegenInterval(Entity* my, Stat& myStats, bool isPlayer); + static int getManaRegenInterval(Entity* my, Stat& myStats, bool isPlayer, bool excludeItemsEffectsBonus = false); // calc time required for a hp regen tick, uses equipped gear as modifiers. static int getHealringFromEquipment(Entity* my, Stat& myStats, bool isPlayer); static int getHealringFromEffects(Entity* my, Stat& myStats); - static int getHealthRegenInterval(Entity* my, Stat& myStats, bool isPlayer); + static int getHealthRegenInterval(Entity* my, Stat& myStats, bool isPlayer, bool excludeItemsEffectsBonus = false); // calc damage/effects for ranged weapons. void setRangedProjectileAttack(Entity& marksman, Stat& myStats, int optionalOverrideForArrowType = 0); bool setArrowProjectileProperties(int weaponType); @@ -911,8 +1116,8 @@ class Entity /* * 1 in @chance chance in spawning a particle with the given sprite and duration. */ - void spawnAmbientParticles(int chance, int particleSprite, int duration, double particleScale, bool shrink); - void spawnAmbientParticles2(int chance, int particleSprite, int duration, double particleScale, bool shrink); + Entity* spawnAmbientParticles(int chance, int particleSprite, int duration, double particleScale, bool shrink); + Entity* spawnAmbientParticles2(int chance, int particleSprite, int duration, double particleScale, bool shrink); //Updates the EFFECTS variable for all clients for this entity. void serverUpdateEffectsForEntity(bool guarantee); @@ -922,13 +1127,14 @@ class Entity * @param guarantee: Causes serverUpdateEffectsForEntity() to use sendPacketSafe() rather than just sendPacket(). * Returns true on successfully setting value. */ - bool setEffect(int effect, bool value, int duration, bool updateClients, bool guarantee = true); + bool setEffect(int effect, std::variant value, int duration, bool updateClients, bool guarantee = true, bool overrideEffectStrength = false, bool overrideDuration = true); /* * @param state: required to let the entity know if it should enter MONSTER_STATE_PATH, MONSTER_STATE_ATTACK, etc. * @param monsterWasHit: monster is retaliating to an attack as opposed to finding an enemy. to set reaction time accordingly in hardcore */ void monsterAcquireAttackTarget(const Entity& target, Sint32 state, bool monsterWasHit = false); + bool monsterAlertBeforeHit(Entity* attacker); /* * Attempts to set the target to 0. @@ -946,7 +1152,7 @@ class Entity return; } - if ( myStats->EFFECTS[EFF_FEAR] ) + if ( myStats->getEffectActive(EFF_FEAR) ) { return; // don't change weapons while feared. } @@ -974,9 +1180,21 @@ class Entity case SLIME: slimeChooseWeapon(target, dist); break; + case MOTH_SMALL: + mothChooseWeapon(target, dist); + break; case BUGBEAR: bugbearChooseWeapon(target, dist); break; + case DRYAD: + monsterDChooseWeapon(target, dist); + break; + case MYCONID: + monsterMChooseWeapon(target, dist); + break; + case GREMLIN: + monsterGChooseWeapon(target, dist); + break; case SHOPKEEPER: if ( target ) { @@ -1001,7 +1219,11 @@ class Entity void shadowChooseWeapon(const Entity* target, double dist); void succubusChooseWeapon(const Entity* target, double dist); void slimeChooseWeapon(const Entity* target, double dist); + void mothChooseWeapon(const Entity* target, double dist); void bugbearChooseWeapon(const Entity* target, double dist); + void monsterDChooseWeapon(const Entity* target, double dist); + void monsterMChooseWeapon(const Entity* target, double dist); + void monsterGChooseWeapon(const Entity* target, double dist); void skeletonSummonSetEquipment(Stat* myStats, int rank); static void tinkerBotSetStats(Stat* myStats, int rank); static void mimicSetStats(Stat* myStats); @@ -1037,7 +1259,7 @@ class Entity * Entities with Stats will have their fire time (char_fire) and chance to stop being on fire (chanceToPutOutFire) reduced by their CON * Calculations for reductions is outlined in this function */ - void SetEntityOnFire(Entity* sourceOfFire = nullptr); + bool SetEntityOnFire(Entity* sourceOfFire); void addToCreatureList(list_t* list); void addToWorldUIList(list_t *list); @@ -1057,32 +1279,39 @@ class Entity int getEntityInspirationFromAllies(); int getFollowerBonusDamageResist(); int getEntityBonusTrapResist(); + bool onEntityTrapHitSacredPath(Entity* trap); int getFollowerBonusHPRegen(); - int getHPRestoreOnLevelUp(); + static int getHPRestoreOnLevelUp(Entity* entity, Stat* myStats, int baseHP, bool statCheckOnly = false); + static int getMPRestoreOnLevelUp(Entity* entity, Stat* myStats, int baseMP, bool statCheckOnly = false); void monsterMoveBackwardsAndPath(bool trySidesFirst = false); // monster tries to move backwards in a cross shaped area if stuck against an entity. bool monsterHasLeader(); // return true if monsterstats->leader_uid is not 0. void monsterAllySendCommand(int command, int destX, int destY, Uint32 uid = 0); // update the behavior of allied NPCs. bool monsterAllySetInteract(); // set interact flags for allied NPCs. bool isInteractWithMonster(); // is a monster interacting with me? check interact flags for allied NPCs. void clearMonsterInteract(); // tidy up flags after interaction. - bool monsterSetPathToLocation(int destX, int destY, int adjacentTilesToCheck, int pathingType, bool tryRandomSpot = false); // monster create path to destination, search adjacent tiles if specified target is inaccessible. + bool monsterSetPathToLocation(int destX, int destY, int adjacentTilesToCheck, int pathingType, bool tryRandomSpot = false, bool shortByShortest = true); // monster create path to destination, search adjacent tiles if specified target is inaccessible. bool gyrobotSetPathToReturnLocation(int destX, int destY, int adjacentTilesToCheck, bool tryRandomSpot = false); // gyrobot create path to destination to land safely. static int getMagicResistance(Stat* myStats); // returns the value of magic resistance of a monster. + static real_t magicResistancePerPoint; void playerLevelEntrySpeechSecond(); // handle secondary voice lines for post-herx content bool isPlayerHeadSprite() const; // determines if model of entity is a human head. static bool isPlayerHeadSprite(const int sprite); - void setDefaultPlayerModel(int playernum, Monster playerRace, int limbType); // sets correct base color/model of limbs for player characters. + void setDefaultPlayerModel(int playernum, Monster playerRace, int limbType, int headSprite); // sets correct base color/model of limbs for player characters. Monster getMonsterFromPlayerRace(int playerRace); // convert playerRace into the relevant monster type void setHardcoreStats(Stat& stats); // set monster stats for hardcore mode. void handleNPCInteractDialogue(Stat& myStats, AllyNPCChatter event); // monster text for interactions. void playerStatIncrease(int playerClass, int chosenStats[3]); bool isBossMonster(); // return true if boss map (hell boss, boss etc or shopkeeper/shadow/other boss + bool isSmiteWeakMonster(); void handleKnockbackDamage(Stat& myStats, Entity* knockedInto); // handle knockback damage from getting hit into other things. void setHelmetLimbOffsetWithMask(Entity* helm, Entity* mask); bool entityCheckIfTriggeredBomb(bool triggerBomb); + bool entityCheckIfTriggeredWallButton(); Sint32 playerInsectoidExpectedManaFromHunger(Stat& myStats); Sint32 playerInsectoidHungerValueOfManaPoint(Stat& myStats); - static real_t getDamageTableMultiplier(Entity* my, Stat& myStats, DamageTableType damageType); + void playerInsectoidIncrementHungerToMP(int mpAmount); + static real_t getDamageTableMultiplier(Entity* my, Stat& myStats, DamageTableType damageType, int* magicResistance = nullptr, int* outNumSources = nullptr); + static real_t getDamageTableEquipmentMod(Stat& myStats, Item& item, real_t base, real_t mod); bool isBoulderSprite(); void createWorldUITooltip(); bool bEntityTooltipRequiresButtonHeld() const; @@ -1095,12 +1324,14 @@ class Entity bool isColliderWeakToBoulders() const; bool isColliderShownAsWallOnMinimap() const; bool isColliderDamageableByMagic() const; + bool isColliderPathableMonster(Monster type) const; bool isColliderAttachableToBombs() const; bool isColliderWall() const; bool isColliderBreakableContainer() const; void colliderOnDestroy(); int getColliderOnHitLangEntry() const; int getColliderOnBreakLangEntry() const; + int getColliderOnJumpLangEntry() const; int getColliderSfxOnHit() const; int getColliderSfxOnBreak() const; int getColliderLangName() const; @@ -1112,6 +1343,23 @@ class Entity bool entityCanVomit() const; bool doSilkenBowOnAttack(Entity* attacker); void setBugbearStrafeDir(bool forceDirection); + void processEntityWind(); + bool windEffectsEntity(Entity* entity); + real_t monsterGetWeightRatio(); + bool spellEffectPreserveItem(Item* item); + bool mistFormDodge(bool checkEffectActiveOnly, Entity* attacker); + bool defyFleshProc(Entity* attacker); + bool pinpointDamageProc(Entity* attacker, int damage); + static bool modifyDamageMultipliersFromEffects(Entity* hitentity, Entity* attacker, + real_t& damageMultiplier, DamageTableType damageTableType, Entity* projectile = nullptr, int spellID = -1); + real_t getHealingSpellPotionModifierFromEffects(bool processLevelup); + void attractItem(Entity& itemEntity); + void creatureHandleLiftZ(); + bool monsterIsTargetable(bool targetInertMimics = false) const; + bool monsterCanTradeWith(int player) const; + bool degradeAmuletProc(Stat* myStats, ItemType type); + bool myconidReboundOnHit(Entity* attacker); + void playerShakeGrowthHelmet(); }; Monster getMonsterFromPlayerRace(int playerRace); // convert playerRace into the relevant monster type @@ -1121,7 +1369,6 @@ Sint32 statGetCON(Stat* entitystats, Entity* my); Sint32 statGetINT(Stat* entitystats, Entity* my); Sint32 statGetPER(Stat* entitystats, Entity* my); Sint32 statGetCHR(Stat* entitystats, Entity* my); -extern list_t entitiesToDelete[MAXPLAYERS]; extern Uint32 entity_uids, lastEntityUIDs; //extern Entity *players[4]; extern Uint32 nummonsters; @@ -1143,12 +1390,14 @@ list_t* checkTileForEntity(int x, int y); //Don't forget to free the list return void getItemsOnTile(int x, int y, list_t** list); // get mana regen from stats and proficiencies only. -int getBaseManaRegen(Entity* my, Stat& myStats); +int getBaseManaRegen(Entity* my, Stat& myStats, bool excludeItemsEffectsBonus = false); //--- Entity act* functions --- void actMonster(Entity* my); -int playerHeadSprite(Monster race, sex_t sex, int appearance, int frame = 0); +int playerHeadSprite(Monster race, sex_t sex, int appearance, int frame = 0, int player = -1); void actPlayer(Entity* my); +void actPlayerXP(Entity* my); +void spawnPlayerXP(real_t x, real_t y, int player, int xpAmount); void playerAnimateRat(Entity* my); void playerAnimateSpider(Entity* my); @@ -1183,6 +1432,7 @@ void actArrowTrap(Entity* my); void actTrap(Entity* my); void actTrapPermanent(Entity* my); void actSwitchWithTimer(Entity* my); +void actIronDoor(Entity* my); /* * Note: Circuits and mechanisms use skill[28] to signify powered state. @@ -1230,9 +1480,8 @@ void actTextSource(Entity* my); //checks if a sprite falls in certain sprite ranges -static const int NUM_ITEM_STRINGS = 336; -static const int NUM_ITEM_STRINGS_BY_TYPE = 129; -static const int NUM_EDITOR_SPRITES = 202; +static const int NUM_ITEM_STRINGS = ITEM_ENUM_MAX + 3; +static const int NUM_ITEM_STRINGS_BY_TYPE = 236; static const int NUM_EDITOR_TILES = 350; // furniture types. @@ -1244,9 +1493,9 @@ static const int FURNITURE_PODIUM = 4; int checkSpriteType(Sint32 sprite); Monster editorSpriteTypeToMonster(Sint32 sprite); -extern char spriteEditorNameStrings[NUM_EDITOR_SPRITES][64]; +extern std::vectorspriteEditorNameStrings; extern char tileEditorNameStrings[NUM_EDITOR_TILES][44]; -extern char monsterEditorNameStrings[NUMMONSTERS][16]; +extern char monsterEditorNameStrings[NUMMONSTERS][32]; extern char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32]; extern char itemNameStrings[NUM_ITEM_STRINGS][32]; int canWearEquip(Entity* entity, int category); @@ -1265,7 +1514,7 @@ static const int SPRITE_BOOT_LEFT_OFFSET = 2; int setGloveSprite(Stat * myStats, Entity* ent, int spriteOffset); bool isLevitating(Stat * myStats); -int getWeaponSkill(Item* weapon); +int getWeaponSkill(const Item* weapon); int getStatForProficiency(int skill); void setSpriteAttributes(Entity* entityToSet, Entity* entityToCopy, Entity* entityStatToCopy); bool monsterIsImmobileTurret(Entity* my, Stat* myStats); @@ -1296,6 +1545,8 @@ void boulderLavaOrArcaneOnDestroy(Entity* my, int sprite, Entity* boulderHitEnti int playerEntityMatchesUid(Uint32 uid); // Returns >= 0 if player uid matches uid. bool monsterNameIsGeneric(Stat& monsterStats); // returns true if a monster's name is a generic decription rather than a miniboss. +bool shieldSpriteAllowedImpForm(int sprite); +bool weaponSpriteAllowedImpForm(int sprite); bool playerRequiresBloodToSustain(int player); // vampire type or accursed class void spawnBloodVialOnMonsterDeath(Entity* entity, Stat* hitstats, Entity* killer); @@ -1373,7 +1624,27 @@ class TextSourceScript TO_SPELLBOT, TO_GYROBOT, TO_DUMMYBOT, - TO_BUGBEAR + TO_BUGBEAR, + TO_MONSTER_D, + TO_MONSTER_M, + TO_MONSTER_S, + TO_MONSTER_G, + TO_REVENANT_SKULL, + TO_MINIMIMIC, + TO_ADORCISED_WEAPON, + TO_FLAME_ELEMENTAL, + TO_HOLOGRAM, + TO_MOTH_SMALL, + TO_EARTH_ELEMENTAL, + TO_DUCK_SMALL, + TO_MONSTER_UNUSED_6, + TO_MONSTER_UNUSED_7, + TO_MONSTER_UNUSED_8, + TO_MONSTER_MAX, + TO_BREAKABLE, + TO_COLLIDER, + TO_GOLD, + TO_BELL }; enum ScriptType : int { @@ -1390,7 +1661,8 @@ class TextSourceScript TRIGGER_ATTACHED_INVIS, TRIGGER_ATTACHED_VISIBLE, TRIGGER_ATTACHED_ALWAYS, - TRIGGER_ON_VARIABLE + TRIGGER_ON_VARIABLE, + TRIGGER_ATTACHED_INTERACTED }; /*enum TagAvailableToEntity : int { @@ -1424,6 +1696,8 @@ class TextSourceScript void playerClearInventory(bool clearStats); std::string getScriptFromEntity(Entity& src); void parseScriptInMapGeneration(Entity& src); + Entity* createScriptEntityInMapGen(int x, int y, const char* text); + void addScriptToTextSource(Entity& src, const char* text); void handleTextSourceScript(Entity& src, std::string input); int textSourceProcessScriptTag(std::string& input, std::string findTag, Entity& src); bool hasClearedInventory = false; @@ -1433,11 +1707,11 @@ class TextSourceScript } int getAttachedToEntityType(Sint32 skill) { - return ((skill & 0xF0) >> 4); + return ((skill & 0xFF0) >> 4); } int getTriggerType(Sint32 skill) { - return ((skill & 0xF00) >> 8); + return ((skill & 0xF000) >> 12); } void setScriptType(Sint32& skill, int setValue) { @@ -1446,13 +1720,13 @@ class TextSourceScript } void setAttachedToEntityType(Sint32& skill, int setValue) { - skill &= 0xFFFFFF0F; - skill |= ((setValue << 4) & 0xF0); + skill &= 0xFFFFF00F; + skill |= ((setValue << 4) & 0xFF0); } void setTriggerType(Sint32& skill, int setValue) { - skill &= 0xFFFFF0FF; - skill |= ((setValue << 8) & 0xF00); + skill &= 0xFFFF0FFF; + skill |= ((setValue << 12) & 0xF000); } std::vector getScriptAttachedEntities(Entity& script) { diff --git a/src/entity_editor.cpp b/src/entity_editor.cpp index 53d9df963..97d760dd2 100644 --- a/src/entity_editor.cpp +++ b/src/entity_editor.cpp @@ -11,451 +11,477 @@ -------------------------------------------------------------------------------*/ #include "entity.hpp" - - -Entity::Entity(Sint32 in_sprite, Uint32 pos, list_t* entlist, list_t* creaturelist) : - chanceToPutOutFire(skill[37]), - circuit_status(skill[28]), - switch_power(skill[0]), - chestInit(skill[0]), - chestStatus(skill[1]), - chestHealth(skill[3]), - chestLocked(skill[4]), - chestOpener(skill[5]), - chestLidClicked(skill[6]), - chestAmbience(skill[7]), - chestMaxHealth(skill[8]), - chestType(skill[9]), - chestPreventLockpickCapstoneExploit(skill[10]), - chestHasVampireBook(skill[11]), - chestLockpickHealth(skill[12]), - chestOldHealth(skill[15]), - chestMimicChance(skill[16]), - char_gonnavomit(skill[26]), - char_heal(skill[22]), - char_energize(skill[23]), - char_drunk(skill[24]), - char_torchtime(skill[25]), - char_poison(skill[21]), - char_fire(skill[36]), - monsterState(skill[0]), - monsterTarget(skill[1]), - monsterTargetX(fskill[2]), - monsterTargetY(fskill[3]), - crystalInitialised(skill[1]), - crystalTurning(skill[3]), - crystalTurnStartDir(skill[4]), - crystalGeneratedElectricityNodes(skill[5]), - crystalNumElectricityNodes(skill[6]), - crystalHoverDirection(skill[7]), - crystalHoverWaitTimer(skill[8]), - crystalTurnReverse(skill[9]), - crystalSpellToActivate(skill[10]), - crystalStartZ(fskill[0]), - crystalMaxZVelocity(fskill[1]), - crystalMinZVelocity(fskill[2]), - crystalTurnVelocity(fskill[3]), - monsterAnimationLimbDirection(skill[20]), - monsterAnimationLimbOvershoot(skill[30]), - monsterSpecialTimer(skill[29]), - monsterSpecialState(skill[33]), - monsterSpellAnimation(skill[31]), - monsterFootstepType(skill[32]), - monsterLookTime(skill[4]), - monsterMoveTime(skill[6]), - monsterLookDir(fskill[4]), - monsterEntityRenderAsTelepath(skill[41]), - playerLevelEntrySpeech(skill[18]), - playerAliveTime(skill[12]), - playerVampireCurse(skill[51]), - playerAutomatonDeathCounter(skill[15]), - playerCreatedDeathCam(skill[16]), - monsterAttack(skill[8]), - monsterAttackTime(skill[9]), - monsterArmbended(skill[10]), - monsterWeaponYaw(fskill[5]), - monsterShadowInitialMimic(skill[34]), - monsterShadowDontChangeName(skill[35]), - monsterSlimeLastAttack(skill[34]), - monsterLichFireMeleeSeq(skill[34]), - monsterLichFireMeleePrev(skill[35]), - monsterLichIceCastSeq(skill[34]), - monsterLichIceCastPrev(skill[35]), - monsterLichMagicCastCount(skill[37]), - monsterLichMeleeSwingCount(skill[38]), - monsterLichBattleState(skill[27]), - monsterLichTeleportTimer(skill[40]), - monsterLichAllyStatus(skill[18]), - monsterLichAllyUID(skill[17]), - monsterPathBoundaryXStart(skill[14]), - monsterPathBoundaryYStart(skill[15]), - monsterPathBoundaryXEnd(skill[16]), - monsterPathBoundaryYEnd(skill[17]), - monsterStoreType(skill[18]), - monsterDevilNumSummons(skill[18]), - monsterStrafeDirection(skill[39]), - monsterPathCount(skill[38]), - monsterAllyIndex(skill[42]), - monsterAllyState(skill[43]), - monsterAllyPickupItems(skill[44]), - monsterAllyInteractTarget(skill[45]), - monsterAllyClass(skill[46]), - monsterDefend(skill[47]), - monsterAllySpecial(skill[48]), - monsterAllySpecialCooldown(skill[49]), - monsterAllySummonRank(skill[50]), - monsterKnockbackVelocity(fskill[9]), - monsterKnockbackUID(skill[51]), - creatureWebbedSlowCount(skill[52]), - monsterFearfulOfUid(skill[53]), - creatureShadowTaggedThisUid(skill[54]), - monsterIllusionTauntingThisUid(skill[55]), - monsterLastDistractedByNoisemaker(skill[55]), - monsterExtraReflexTick(skill[56]), - monsterSentrybotLookDir(fskill[10]), - monsterKnockbackTangentDir(fskill[11]), - playerStrafeVelocity(fskill[12]), - playerStrafeDir(fskill[13]), - monsterSpecialAttackUnequipSafeguard(fskill[14]), - entityShowOnMap(skill[59]), - effectPolymorph(skill[50]), - effectShapeshift(skill[53]), - particleDuration(skill[0]), - particleShrink(skill[1]), - monsterHitTime(skill[7]), - itemNotMoving(skill[18]), - itemNotMovingClient(skill[19]), - itemSokobanReward(skill[20]), - itemOriginalOwner(skill[21]), - itemStolen(skill[22]), - itemShowOnMap(skill[23]), - itemDelayMonsterPickingUp(skill[24]), - itemReceivedDetailsFromServer(skill[25]), - itemAutoSalvageByPlayer(skill[26]), - itemSplooshed(skill[27]), - itemContainer(skill[29]), - itemWaterBob(fskill[2]), - gateInit(skill[1]), - gateStatus(skill[3]), - gateRattle(skill[4]), - gateStartHeight(fskill[0]), - gateVelZ(vel_z), - gateInverted(skill[5]), - gateDisableOpening(skill[6]), - leverStatus(skill[1]), - leverTimerTicks(skill[3]), - boulderTrapRefireAmount(skill[1]), - boulderTrapRefireDelay(skill[3]), - boulderTrapAmbience(skill[6]), - boulderTrapFired(skill[0]), - boulderTrapRefireCounter(skill[4]), - boulderTrapPreDelay(skill[5]), - boulderTrapRocksToSpawn(skill[7]), - doorDir(skill[0]), - doorInit(skill[1]), - doorStatus(skill[3]), - doorHealth(skill[4]), - doorLocked(skill[5]), - doorSmacked(skill[6]), - doorTimer(skill[7]), - doorOldStatus(skill[8]), - doorMaxHealth(skill[9]), - doorStartAng(fskill[0]), - doorPreventLockpickExploit(skill[10]), - doorForceLockedUnlocked(skill[11]), - doorDisableLockpicks(skill[12]), - doorDisableOpening(skill[13]), - doorLockpickHealth(skill[14]), - doorOldHealth(skill[15]), - particleTimerDuration(skill[0]), - particleTimerEndAction(skill[1]), - particleTimerEndSprite(skill[3]), - particleTimerCountdownAction(skill[4]), - particleTimerCountdownSprite(skill[5]), - particleTimerTarget(skill[6]), - particleTimerPreDelay(skill[7]), - particleTimerVariable1(skill[8]), - particleTimerVariable2(skill[9]), - pedestalHasOrb(skill[0]), - pedestalOrbType(skill[1]), - pedestalInvertedPower(skill[3]), - pedestalInGround(skill[4]), - pedestalInit(skill[5]), - pedestalAmbience(skill[6]), - pedestalLockOrb(skill[7]), - pedestalPowerStatus(skill[8]), - orbInitialised(skill[1]), - orbHoverDirection(skill[7]), - orbHoverWaitTimer(skill[8]), - orbStartZ(fskill[0]), - orbMaxZVelocity(fskill[1]), - orbMinZVelocity(fskill[2]), - orbTurnVelocity(fskill[3]), - portalAmbience(skill[0]), - portalInit(skill[1]), - portalNotSecret(skill[3]), - portalVictoryType(skill[4]), - portalFireAnimation(skill[5]), - portalCustomLevelsToJump(skill[6]), - portalCustomRequiresPower(skill[7]), - portalCustomSprite(skill[8]), - portalCustomSpriteAnimationFrames(skill[9]), - portalCustomZOffset(skill[10]), - portalCustomLevelText1(skill[11]), - portalCustomLevelText2(skill[12]), - portalCustomLevelText3(skill[13]), - portalCustomLevelText4(skill[14]), - portalCustomLevelText5(skill[15]), - portalCustomLevelText6(skill[16]), - portalCustomLevelText7(skill[17]), - portalCustomLevelText8(skill[18]), - teleporterX(skill[0]), - teleporterY(skill[1]), - teleporterType(skill[3]), - teleporterAmbience(skill[4]), - spellTrapType(skill[0]), - spellTrapRefire(skill[1]), - spellTrapLatchPower(skill[3]), - spellTrapFloorTile(skill[4]), - spellTrapRefireRate(skill[5]), - spellTrapAmbience(skill[6]), - spellTrapInit(skill[7]), - spellTrapCounter(skill[8]), - spellTrapReset(skill[9]), - shrineSpellEffect(skill[0]), - shrineRefire1(skill[1]), - shrineRefire2(skill[3]), - shrineDir(skill[4]), - shrineAmbience(skill[5]), - shrineInit(skill[6]), - shrineActivateDelay(skill[7]), - shrineZ(skill[8]), - shrineDestXOffset(skill[9]), - shrineDestYOffset(skill[10]), - shrineDaedalusState(skill[11]), - ceilingTileModel(skill[0]), - ceilingTileDir(skill[1]), - ceilingTileAllowTrap(skill[3]), - ceilingTileBreakable(skill[4]), - floorDecorationModel(skill[0]), - floorDecorationRotation(skill[1]), - floorDecorationHeightOffset(skill[3]), - floorDecorationXOffset(skill[4]), - floorDecorationYOffset(skill[5]), - floorDecorationInteractText1(skill[8]), - floorDecorationInteractText2(skill[9]), - floorDecorationInteractText3(skill[10]), - floorDecorationInteractText4(skill[11]), - floorDecorationInteractText5(skill[12]), - floorDecorationInteractText6(skill[13]), - floorDecorationInteractText7(skill[14]), - floorDecorationInteractText8(skill[15]), - colliderDecorationModel(skill[0]), - colliderDecorationRotation(skill[1]), - colliderDecorationHeightOffset(skill[3]), - colliderDecorationXOffset(skill[4]), - colliderDecorationYOffset(skill[5]), - colliderHasCollision(skill[6]), - colliderSizeX(skill[7]), - colliderSizeY(skill[8]), - colliderMaxHP(skill[9]), - colliderDiggable(skill[10]), - colliderDamageTypes(skill[11]), - colliderCurrentHP(skill[12]), - colliderOldHP(skill[13]), - colliderInit(skill[14]), - colliderContainedEntity(skill[15]), - colliderHideMonster(skill[16]), - colliderKillerUid(skill[17]), - furnitureType(skill[0]), - furnitureInit(skill[1]), - furnitureDir(skill[3]), - furnitureHealth(skill[4]), - furnitureMaxHealth(skill[9]), - furnitureTableRandomItemChance(skill[10]), - furnitureTableSpawnChairs(skill[11]), - furnitureOldHealth(skill[15]), - pistonCamDir(skill[0]), - pistonCamTimer(skill[1]), - pistonCamRotateSpeed(fskill[0]), - arrowPower(skill[3]), - arrowPoisonTime(skill[4]), - arrowArmorPierce(skill[5]), - arrowSpeed(fskill[4]), - arrowFallSpeed(fskill[5]), - arrowBoltDropOffRange(skill[6]), - arrowShotByWeapon(skill[7]), - arrowQuiverType(skill[8]), - arrowShotByParent(skill[9]), - arrowDropOffEquipmentModifier(skill[14]), - actmagicIsVertical(skill[6]), - actmagicIsOrbiting(skill[7]), - actmagicOrbitDist(skill[8]), - actmagicOrbitVerticalDirection(skill[9]), - actmagicOrbitLifetime(skill[10]), - actmagicMirrorReflected(skill[24]), - actmagicMirrorReflectedCaster(skill[12]), - actmagicCastByMagicstaff(skill[13]), - actmagicOrbitVerticalSpeed(fskill[2]), - actmagicOrbitStartZ(fskill[3]), - actmagicOrbitStationaryX(fskill[4]), - actmagicOrbitStationaryY(fskill[5]), - actmagicOrbitStationaryCurrentDist(fskill[6]), - actmagicSprayGravity(fskill[7]), - actmagicOrbitStationaryHitTarget(skill[14]), - actmagicOrbitHitTargetUID1(skill[15]), - actmagicOrbitHitTargetUID2(skill[16]), - actmagicOrbitHitTargetUID3(skill[17]), - actmagicOrbitHitTargetUID4(skill[18]), - actmagicProjectileArc(skill[19]), - actmagicOrbitCastFromSpell(skill[20]), - actmagicSpellbookBonus(skill[21]), - actmagicCastByTinkerTrap(skill[22]), - actmagicTinkerTrapFriendlyFire(skill[23]), - actmagicReflectionCount(skill[25]), - actmagicFromSpellbook(skill[26]), - actmagicSpray(skill[27]), - actmagicEmitter(skill[29]), - goldAmount(skill[0]), - goldAmbience(skill[1]), - goldSokoban(skill[2]), - goldBouncing(skill[3]), - goldInContainer(skill[4]), - interactedByMonster(skill[47]), - highlightForUI(fskill[29]), - highlightForUIGlow(fskill[28]), - grayscaleGLRender(fskill[27]), - noColorChangeAllyLimb(fskill[26]), - soundSourceFired(skill[0]), - soundSourceToPlay(skill[1]), - soundSourceVolume(skill[2]), - soundSourceLatchOn(skill[3]), - soundSourceDelay(skill[4]), - soundSourceDelayCounter(skill[5]), - soundSourceOrigin(skill[6]), - lightSourceBrightness(skill[0]), - lightSourceAlwaysOn(skill[1]), - lightSourceInvertPower(skill[2]), - lightSourceLatchOn(skill[3]), - lightSourceRadius(skill[4]), - lightSourceFlicker(skill[5]), - lightSourceDelay(skill[6]), - lightSourceDelayCounter(skill[7]), - textSourceColorRGB(skill[0]), - textSourceVariables4W(skill[1]), - textSourceDelay(skill[2]), - textSourceIsScript(skill[3]), - textSourceBegin(skill[4]), - signalActivateDelay(skill[1]), - signalTimerInterval(skill[2]), - signalTimerRepeatCount(skill[3]), - signalTimerLatchInput(skill[4]), - signalInputDirection(skill[5]), - signalGateANDPowerCount(skill[9]), - signalInvertOutput(skill[10]), - thrownProjectilePower(skill[19]), - thrownProjectileCharge(skill[20]), - playerStartDir(skill[1]), - pressurePlateTriggerType(skill[3]), - worldTooltipAlpha(fskill[0]), - worldTooltipZ(fskill[1]), - worldTooltipActive(skill[0]), - worldTooltipPlayer(skill[1]), - worldTooltipInit(skill[3]), - worldTooltipFadeDelay(skill[4]), - worldTooltipIgnoreDrawing(skill[5]), - worldTooltipRequiresButtonHeld(skill[6]), - statueInit(skill[0]), - statueDir(skill[1]), - statueId(skill[3]) -{ - int c; - // add the entity to the entity list - if (!pos) - { - mynode = list_AddNodeFirst(entlist); - } - else - { - mynode = list_AddNodeLast(entlist); - } - mynode->element = this; - mynode->deconstructor = &entityDeconstructor; - mynode->size = sizeof(Entity); - - myCreatureListNode = nullptr; - - // now reset all of my data elements - lastupdate = 0; - lastupdateserver = 0; - ticks = 0; - x = 0; - y = 0; - z = 0; - new_x = 0; - new_y = 0; - new_z = 0; - focalx = 0; - focaly = 0; - focalz = 0; - scalex = 1; - scaley = 1; - scalez = 1; - vel_x = 0; - vel_y = 0; - vel_z = 0; - sizex = 0; - sizey = 0; - yaw = 0; - pitch = 0; - roll = 0; - new_yaw = 0; - new_pitch = 0; - new_roll = 0; - sprite = in_sprite; - light = nullptr; - string = nullptr; - children.first = nullptr; - children.last = nullptr; - //this->magic_effects = (list_t *) malloc(sizeof(list_t)); - //this->magic_effects->first = NULL; this->magic_effects->last = NULL; - for ( c = 0; c < NUMENTITYSKILLS; ++c ) - { - skill[c] = 0; - } - for (c = 0; c < NUMENTITYFSKILLS; ++c) - { - fskill[c] = 0; - } - skill[2] = -1; - for (c = 0; c < 24; ++c) - { - flags[c] = false; - } - if (entlist == map.entities) - { - if (multiplayer != CLIENT || loading) - { - uid = entity_uids; - entity_uids++; - } - else - { - uid = -2; - } - } - else - { - uid = -2; - } - behavior = nullptr; - ranbehavior = false; - parent = 0; - path = nullptr; - - clientStats = nullptr; - clientsHaveItsStats = false; -} +//Entity::Entity(Sint32 in_sprite, Uint32 pos, list_t* entlist, list_t* creaturelist) : +// chanceToPutOutFire(skill[37]), +// circuit_status(skill[28]), +// switch_power(skill[0]), +// chestInit(skill[0]), +// chestStatus(skill[1]), +// chestHealth(skill[3]), +// chestLocked(skill[4]), +// chestOpener(skill[5]), +// chestLidClicked(skill[6]), +// chestAmbience(skill[7]), +// chestMaxHealth(skill[8]), +// chestType(skill[9]), +// chestPreventLockpickCapstoneExploit(skill[10]), +// chestHasVampireBook(skill[11]), +// chestLockpickHealth(skill[12]), +// chestOldHealth(skill[15]), +// chestMimicChance(skill[16]), +// char_gonnavomit(skill[26]), +// char_heal(skill[22]), +// char_energize(skill[23]), +// char_drunk(skill[24]), +// char_torchtime(skill[25]), +// char_poison(skill[21]), +// char_fire(skill[36]), +// monsterState(skill[0]), +// monsterTarget(skill[1]), +// monsterTargetX(fskill[2]), +// monsterTargetY(fskill[3]), +// crystalInitialised(skill[1]), +// crystalTurning(skill[3]), +// crystalTurnStartDir(skill[4]), +// crystalGeneratedElectricityNodes(skill[5]), +// crystalNumElectricityNodes(skill[6]), +// crystalHoverDirection(skill[7]), +// crystalHoverWaitTimer(skill[8]), +// crystalTurnReverse(skill[9]), +// crystalSpellToActivate(skill[10]), +// crystalStartZ(fskill[0]), +// crystalMaxZVelocity(fskill[1]), +// crystalMinZVelocity(fskill[2]), +// crystalTurnVelocity(fskill[3]), +// monsterAnimationLimbDirection(skill[20]), +// monsterAnimationLimbOvershoot(skill[30]), +// monsterSpecialTimer(skill[29]), +// monsterSpecialState(skill[33]), +// monsterSpellAnimation(skill[31]), +// monsterFootstepType(skill[32]), +// monsterLookTime(skill[4]), +// monsterMoveTime(skill[6]), +// monsterLookDir(fskill[4]), +// monsterEntityRenderAsTelepath(skill[41]), +// playerLevelEntrySpeech(skill[18]), +// playerAliveTime(skill[12]), +// playerVampireCurse(skill[51]), +// playerAutomatonDeathCounter(skill[15]), +// playerCreatedDeathCam(skill[16]), +// monsterAttack(skill[8]), +// monsterAttackTime(skill[9]), +// monsterArmbended(skill[10]), +// monsterWeaponYaw(fskill[5]), +// monsterShadowInitialMimic(skill[34]), +// monsterShadowDontChangeName(skill[35]), +// monsterSlimeLastAttack(skill[34]), +// monsterLichFireMeleeSeq(skill[34]), +// monsterLichFireMeleePrev(skill[35]), +// monsterLichIceCastSeq(skill[34]), +// monsterLichIceCastPrev(skill[35]), +// monsterLichMagicCastCount(skill[37]), +// monsterLichMeleeSwingCount(skill[38]), +// monsterLichBattleState(skill[27]), +// monsterLichTeleportTimer(skill[40]), +// monsterLichAllyStatus(skill[18]), +// monsterLichAllyUID(skill[17]), +// monsterPathBoundaryXStart(skill[14]), +// monsterPathBoundaryYStart(skill[15]), +// monsterPathBoundaryXEnd(skill[16]), +// monsterPathBoundaryYEnd(skill[17]), +// monsterStoreType(skill[18]), +// monsterDevilNumSummons(skill[18]), +// monsterStrafeDirection(skill[39]), +// monsterPathCount(skill[38]), +// monsterAllyIndex(skill[42]), +// monsterAllyState(skill[43]), +// monsterAllyPickupItems(skill[44]), +// monsterAllyInteractTarget(skill[45]), +// monsterAllyClass(skill[46]), +// monsterDefend(skill[47]), +// monsterAllySpecial(skill[48]), +// monsterAllySpecialCooldown(skill[49]), +// monsterAllySummonRank(skill[50]), +// monsterKnockbackVelocity(fskill[9]), +// monsterKnockbackUID(skill[51]), +// creatureWebbedSlowCount(skill[52]), +// monsterFearfulOfUid(skill[53]), +// creatureShadowTaggedThisUid(skill[54]), +// monsterIllusionTauntingThisUid(skill[55]), +// monsterLastDistractedByNoisemaker(skill[55]), +// monsterExtraReflexTick(skill[56]), +// monsterSentrybotLookDir(fskill[10]), +// monsterKnockbackTangentDir(fskill[11]), +// playerStrafeVelocity(fskill[12]), +// playerStrafeDir(fskill[13]), +// monsterSpecialAttackUnequipSafeguard(fskill[14]), +// creatureWindDir(fskill[15]), +// creatureWindVelocity(fskill[16]), +// creatureHoverZ(fskill[17]), +// entityShowOnMap(skill[59]), +// effectPolymorph(skill[50]), +// effectShapeshift(skill[53]), +// particleDuration(skill[0]), +// particleShrink(skill[1]), +// monsterHitTime(skill[7]), +// itemNotMoving(skill[18]), +// itemNotMovingClient(skill[19]), +// itemSokobanReward(skill[20]), +// itemOriginalOwner(skill[21]), +// itemStolen(skill[22]), +// itemShowOnMap(skill[23]), +// itemDelayMonsterPickingUp(skill[24]), +// itemReceivedDetailsFromServer(skill[25]), +// itemAutoSalvageByPlayer(skill[26]), +// itemSplooshed(skill[27]), +// itemContainer(skill[29]), +// itemWaterBob(fskill[2]), +// gateInit(skill[1]), +// gateStatus(skill[3]), +// gateRattle(skill[4]), +// gateStartHeight(fskill[0]), +// gateVelZ(vel_z), +// gateInverted(skill[5]), +// gateDisableOpening(skill[6]), +// leverStatus(skill[1]), +// leverTimerTicks(skill[3]), +// boulderTrapRefireAmount(skill[1]), +// boulderTrapRefireDelay(skill[3]), +// boulderTrapAmbience(skill[6]), +// boulderTrapFired(skill[0]), +// boulderTrapRefireCounter(skill[4]), +// boulderTrapPreDelay(skill[5]), +// boulderTrapRocksToSpawn(skill[7]), +// doorDir(skill[0]), +// doorInit(skill[1]), +// doorStatus(skill[3]), +// doorHealth(skill[4]), +// doorLocked(skill[5]), +// doorSmacked(skill[6]), +// doorTimer(skill[7]), +// doorOldStatus(skill[8]), +// doorMaxHealth(skill[9]), +// doorStartAng(fskill[0]), +// doorPreventLockpickExploit(skill[10]), +// doorForceLockedUnlocked(skill[11]), +// doorDisableLockpicks(skill[12]), +// doorDisableOpening(skill[13]), +// doorLockpickHealth(skill[14]), +// doorOldHealth(skill[15]), +// doorUnlockWhenPowered(skill[16]), +// particleTimerDuration(skill[0]), +// particleTimerEndAction(skill[1]), +// particleTimerEndSprite(skill[3]), +// particleTimerCountdownAction(skill[4]), +// particleTimerCountdownSprite(skill[5]), +// particleTimerTarget(skill[6]), +// particleTimerPreDelay(skill[7]), +// particleTimerVariable1(skill[8]), +// particleTimerVariable2(skill[9]), +// pedestalHasOrb(skill[0]), +// pedestalOrbType(skill[1]), +// pedestalInvertedPower(skill[3]), +// pedestalInGround(skill[4]), +// pedestalInit(skill[5]), +// pedestalAmbience(skill[6]), +// pedestalLockOrb(skill[7]), +// pedestalPowerStatus(skill[8]), +// orbInitialised(skill[1]), +// orbHoverDirection(skill[7]), +// orbHoverWaitTimer(skill[8]), +// orbStartZ(fskill[0]), +// orbMaxZVelocity(fskill[1]), +// orbMinZVelocity(fskill[2]), +// orbTurnVelocity(fskill[3]), +// portalAmbience(skill[0]), +// portalInit(skill[1]), +// portalNotSecret(skill[3]), +// portalVictoryType(skill[4]), +// portalFireAnimation(skill[5]), +// portalCustomLevelsToJump(skill[6]), +// portalCustomRequiresPower(skill[7]), +// portalCustomSprite(skill[8]), +// portalCustomSpriteAnimationFrames(skill[9]), +// portalCustomZOffset(skill[10]), +// portalCustomLevelText1(skill[11]), +// portalCustomLevelText2(skill[12]), +// portalCustomLevelText3(skill[13]), +// portalCustomLevelText4(skill[14]), +// portalCustomLevelText5(skill[15]), +// portalCustomLevelText6(skill[16]), +// portalCustomLevelText7(skill[17]), +// portalCustomLevelText8(skill[18]), +// teleporterX(skill[0]), +// teleporterY(skill[1]), +// teleporterType(skill[3]), +// teleporterAmbience(skill[4]), +// spellTrapType(skill[0]), +// spellTrapRefire(skill[1]), +// spellTrapLatchPower(skill[3]), +// spellTrapFloorTile(skill[4]), +// spellTrapRefireRate(skill[5]), +// spellTrapAmbience(skill[6]), +// spellTrapInit(skill[7]), +// spellTrapCounter(skill[8]), +// spellTrapReset(skill[9]), +// shrineSpellEffect(skill[0]), +// shrineRefire1(skill[1]), +// shrineRefire2(skill[3]), +// shrineDir(skill[4]), +// shrineAmbience(skill[5]), +// shrineInit(skill[6]), +// shrineActivateDelay(skill[7]), +// shrineZ(skill[8]), +// shrineDestXOffset(skill[9]), +// shrineDestYOffset(skill[10]), +// shrineDaedalusState(skill[11]), +// ceilingTileModel(skill[0]), +// ceilingTileDir(skill[1]), +// ceilingTileAllowTrap(skill[3]), +// ceilingTileBreakable(skill[4]), +// floorDecorationModel(skill[0]), +// floorDecorationRotation(skill[1]), +// floorDecorationHeightOffset(skill[3]), +// floorDecorationXOffset(skill[4]), +// floorDecorationYOffset(skill[5]), +// floorDecorationDestroyIfNoWall(skill[6]), +// floorDecorationInteractText1(skill[8]), +// floorDecorationInteractText2(skill[9]), +// floorDecorationInteractText3(skill[10]), +// floorDecorationInteractText4(skill[11]), +// floorDecorationInteractText5(skill[12]), +// floorDecorationInteractText6(skill[13]), +// floorDecorationInteractText7(skill[14]), +// floorDecorationInteractText8(skill[15]), +// colliderDecorationModel(skill[0]), +// colliderDecorationRotation(skill[1]), +// colliderDecorationHeightOffset(skill[3]), +// colliderDecorationXOffset(skill[4]), +// colliderDecorationYOffset(skill[5]), +// colliderHasCollision(skill[6]), +// colliderSizeX(skill[7]), +// colliderSizeY(skill[8]), +// colliderMaxHP(skill[9]), +// colliderDiggable(skill[10]), +// colliderDamageTypes(skill[11]), +// colliderCurrentHP(skill[12]), +// colliderOldHP(skill[13]), +// colliderInit(skill[14]), +// colliderContainedEntity(skill[15]), +// colliderHideMonster(skill[16]), +// colliderKillerUid(skill[17]), +// furnitureType(skill[0]), +// furnitureInit(skill[1]), +// furnitureDir(skill[3]), +// furnitureHealth(skill[4]), +// furnitureMaxHealth(skill[9]), +// furnitureTableRandomItemChance(skill[10]), +// furnitureTableSpawnChairs(skill[11]), +// furnitureOldHealth(skill[15]), +// pistonCamDir(skill[0]), +// pistonCamTimer(skill[1]), +// pistonCamRotateSpeed(fskill[0]), +// arrowPower(skill[3]), +// arrowPoisonTime(skill[4]), +// arrowArmorPierce(skill[5]), +// arrowSpeed(fskill[4]), +// arrowFallSpeed(fskill[5]), +// arrowBoltDropOffRange(skill[6]), +// arrowShotByWeapon(skill[7]), +// arrowQuiverType(skill[8]), +// arrowShotByParent(skill[9]), +// arrowDropOffEquipmentModifier(skill[14]), +// actmagicIsVertical(skill[6]), +// actmagicIsOrbiting(skill[7]), +// actmagicOrbitDist(skill[8]), +// actmagicOrbitVerticalDirection(skill[9]), +// actmagicOrbitLifetime(skill[10]), +// actmagicMirrorReflected(skill[24]), +// actmagicMirrorReflectedCaster(skill[12]), +// actmagicCastByMagicstaff(skill[13]), +// actmagicOrbitVerticalSpeed(fskill[2]), +// actmagicOrbitStartZ(fskill[3]), +// actmagicOrbitStationaryX(fskill[4]), +// actmagicOrbitStationaryY(fskill[5]), +// actmagicOrbitStationaryCurrentDist(fskill[6]), +// actmagicSprayGravity(fskill[7]), +// actmagicVelXStore(fskill[8]), +// actmagicVelYStore(fskill[9]), +// actmagicVelZStore(fskill[10]), +// actmagicOrbitStationaryHitTarget(skill[14]), +// actmagicOrbitHitTargetUID1(skill[15]), +// actmagicOrbitHitTargetUID2(skill[16]), +// actmagicOrbitHitTargetUID3(skill[17]), +// actmagicOrbitHitTargetUID4(skill[18]), +// actmagicProjectileArc(skill[19]), +// actmagicOrbitCastFromSpell(skill[20]), +// actmagicSpellbookBonus(skill[21]), +// actmagicCastByTinkerTrap(skill[22]), +// actmagicTinkerTrapFriendlyFire(skill[23]), +// actmagicReflectionCount(skill[25]), +// actmagicFromSpellbook(skill[26]), +// actmagicSpray(skill[27]), +// actmagicEmitter(skill[29]), +// actmagicDelayMove(skill[30]), +// actmagicNoHitMessage(skill[31]), +// actmagicNoParticle(skill[32]), +// actmagicNoLight(skill[33]), +// goldAmount(skill[0]), +// goldAmbience(skill[1]), +// goldSokoban(skill[2]), +// goldBouncing(skill[3]), +// goldInContainer(skill[4]), +// interactedByMonster(skill[47]), +// highlightForUI(fskill[29]), +// highlightForUIGlow(fskill[28]), +// grayscaleGLRender(fskill[27]), +// noColorChangeAllyLimb(fskill[26]), +// soundSourceFired(skill[0]), +// soundSourceToPlay(skill[1]), +// soundSourceVolume(skill[2]), +// soundSourceLatchOn(skill[3]), +// soundSourceDelay(skill[4]), +// soundSourceDelayCounter(skill[5]), +// soundSourceOrigin(skill[6]), +// lightSourceBrightness(skill[0]), +// lightSourceAlwaysOn(skill[1]), +// lightSourceInvertPower(skill[2]), +// lightSourceLatchOn(skill[3]), +// lightSourceRadius(skill[4]), +// lightSourceFlicker(skill[5]), +// lightSourceDelay(skill[6]), +// lightSourceDelayCounter(skill[7]), +// lightSourceRGB(skill[11]), +// textSourceColorRGB(skill[0]), +// textSourceVariables4W(skill[1]), +// textSourceDelay(skill[2]), +// textSourceIsScript(skill[3]), +// textSourceBegin(skill[4]), +// signalActivateDelay(skill[1]), +// signalTimerInterval(skill[2]), +// signalTimerRepeatCount(skill[3]), +// signalTimerLatchInput(skill[4]), +// signalInputDirection(skill[5]), +// signalGateANDPowerCount(skill[9]), +// signalInvertOutput(skill[10]), +// wallLockState(skill[0]), +// wallLockInvertPower(skill[1]), +// wallLockTurnable(skill[3]), +// wallLockMaterial(skill[4]), +// wallLockDir(skill[5]), +// wallLockClientInteractDelay(skill[6]), +// wallLockPlayerInteracting(skill[7]), +// wallLockPower(skill[8]), +// wallLockInit(skill[9]), +// wallLockTimer(skill[10]), +// wallLockPickable(skill[11]), +// wallLockPickHealth(skill[12]), +// wallLockPickableSkeletonKey(skill[13]), +// wallLockPreventLockpickExploit(skill[14]), +// wallLockAutoGenKey(skill[15]), +// thrownProjectilePower(skill[19]), +// thrownProjectileCharge(skill[20]), +// playerStartDir(skill[1]), +// pressurePlateTriggerType(skill[3]), +// worldTooltipAlpha(fskill[0]), +// worldTooltipZ(fskill[1]), +// worldTooltipActive(skill[0]), +// worldTooltipPlayer(skill[1]), +// worldTooltipInit(skill[3]), +// worldTooltipFadeDelay(skill[4]), +// worldTooltipIgnoreDrawing(skill[5]), +// worldTooltipRequiresButtonHeld(skill[6]), +// statueInit(skill[0]), +// statueDir(skill[1]), +// statueId(skill[3]) +//{ +// int c; +// // add the entity to the entity list +// if (!pos) +// { +// mynode = list_AddNodeFirst(entlist); +// } +// else +// { +// mynode = list_AddNodeLast(entlist); +// } +// mynode->element = this; +// mynode->deconstructor = &entityDeconstructor; +// mynode->size = sizeof(Entity); +// +// myCreatureListNode = nullptr; +// +// // now reset all of my data elements +// lastupdate = 0; +// lastupdateserver = 0; +// ticks = 0; +// x = 0; +// y = 0; +// z = 0; +// new_x = 0; +// new_y = 0; +// new_z = 0; +// focalx = 0; +// focaly = 0; +// focalz = 0; +// scalex = 1; +// scaley = 1; +// scalez = 1; +// vel_x = 0; +// vel_y = 0; +// vel_z = 0; +// sizex = 0; +// sizey = 0; +// yaw = 0; +// pitch = 0; +// roll = 0; +// new_yaw = 0; +// new_pitch = 0; +// new_roll = 0; +// sprite = in_sprite; +// light = nullptr; +// string = nullptr; +// children.first = nullptr; +// children.last = nullptr; +// //this->magic_effects = (list_t *) malloc(sizeof(list_t)); +// //this->magic_effects->first = NULL; this->magic_effects->last = NULL; +// for ( c = 0; c < NUMENTITYSKILLS; ++c ) +// { +// skill[c] = 0; +// } +// for (c = 0; c < NUMENTITYFSKILLS; ++c) +// { +// fskill[c] = 0; +// } +// skill[2] = -1; +// for (c = 0; c < 24; ++c) +// { +// flags[c] = false; +// } +// if (entlist == map.entities) +// { +// if (multiplayer != CLIENT || loading) +// { +// uid = entity_uids; +// entity_uids++; +// } +// else +// { +// uid = -2; +// } +// } +// else +// { +// uid = -2; +// } +// behavior = nullptr; +// ranbehavior = false; +// parent = 0; +// path = nullptr; +// +// clientStats = nullptr; +// clientsHaveItsStats = false; +//} Entity::~Entity() { @@ -463,6 +489,11 @@ Entity::~Entity() { delete clientStats; } + if ( string ) + { + free(string); + string = nullptr; + } } Stat* Entity::getStats() const diff --git a/src/entity_shared.cpp b/src/entity_shared.cpp index 4c26c4a80..4eb0a2ce9 100644 --- a/src/entity_shared.cpp +++ b/src/entity_shared.cpp @@ -11,6 +11,509 @@ See LICENSE for details. #include "entity.hpp" +Entity::Entity(Sint32 in_sprite, Uint32 pos, list_t* entlist, list_t* creaturelist) : + lightBonus(0.f), + chanceToPutOutFire(skill[37]), + circuit_status(skill[28]), + switch_power(skill[0]), + chestInit(skill[0]), + chestStatus(skill[1]), + chestHealth(skill[3]), + chestLocked(skill[4]), + chestOpener(skill[5]), + chestLidClicked(skill[6]), + chestAmbience(skill[7]), + chestMaxHealth(skill[8]), + chestType(skill[9]), + chestPreventLockpickCapstoneExploit(skill[10]), + chestHasVampireBook(skill[11]), + chestLockpickHealth(skill[12]), + chestOldHealth(skill[15]), + chestMimicChance(skill[16]), + char_gonnavomit(skill[26]), + char_heal(skill[22]), + char_energize(skill[23]), + char_drunk(skill[24]), + char_torchtime(skill[25]), + char_poison(skill[21]), + char_fire(skill[36]), + monsterState(skill[0]), + monsterTarget(skill[1]), + monsterTargetX(fskill[2]), + monsterTargetY(fskill[3]), + crystalInitialised(skill[1]), + crystalTurning(skill[3]), + crystalTurnStartDir(skill[4]), + crystalGeneratedElectricityNodes(skill[5]), + crystalNumElectricityNodes(skill[6]), + crystalHoverDirection(skill[7]), + crystalHoverWaitTimer(skill[8]), + crystalTurnReverse(skill[9]), + crystalSpellToActivate(skill[10]), + crystalStartZ(fskill[0]), + crystalMaxZVelocity(fskill[1]), + crystalMinZVelocity(fskill[2]), + crystalTurnVelocity(fskill[3]), + monsterAnimationLimbDirection(skill[20]), + monsterAnimationLimbOvershoot(skill[30]), + monsterSpecialTimer(skill[29]), + monsterSpecialState(skill[33]), + monsterSpellAnimation(skill[31]), + monsterFootstepType(skill[32]), + monsterLookTime(skill[4]), + monsterMoveTime(skill[6]), + monsterLookDir(fskill[4]), + monsterEntityRenderAsTelepath(skill[41]), + playerLevelEntrySpeech(skill[18]), + playerAliveTime(skill[12]), + playerVampireCurse(skill[51]), + playerAutomatonDeathCounter(skill[15]), + playerCreatedDeathCam(skill[16]), + monsterAttack(skill[8]), + monsterAttackTime(skill[9]), + monsterArmbended(skill[10]), + monsterWeaponYaw(fskill[5]), + monsterShadowInitialMimic(skill[34]), + monsterShadowDontChangeName(skill[35]), + monsterSlimeLastAttack(skill[34]), + monsterLichFireMeleeSeq(skill[34]), + monsterLichFireMeleePrev(skill[35]), + monsterLichIceCastSeq(skill[34]), + monsterLichIceCastPrev(skill[35]), + monsterLichMagicCastCount(skill[37]), + monsterLichMeleeSwingCount(skill[38]), + monsterLichBattleState(skill[27]), + monsterLichTeleportTimer(skill[40]), + monsterLichAllyStatus(skill[18]), + monsterLichAllyUID(skill[17]), + monsterPathBoundaryXStart(skill[14]), + monsterPathBoundaryYStart(skill[15]), + monsterPathBoundaryXEnd(skill[16]), + monsterPathBoundaryYEnd(skill[17]), + monsterStoreType(skill[18]), + monsterDevilNumSummons(skill[18]), + monsterStrafeDirection(skill[39]), + monsterPathCount(skill[38]), + monsterAllyIndex(skill[42]), + monsterAllyState(skill[43]), + monsterAllyPickupItems(skill[44]), + monsterAllyInteractTarget(skill[45]), + monsterAllyClass(skill[46]), + monsterDefend(skill[47]), + monsterAllySpecial(skill[48]), + monsterAllySpecialCooldown(skill[49]), + monsterAllySummonRank(skill[50]), + monsterKnockbackVelocity(fskill[9]), + monsterKnockbackUID(skill[51]), + creatureWebbedSlowCount(skill[52]), + monsterFearfulOfUid(skill[53]), + creatureShadowTaggedThisUid(skill[54]), + monsterIllusionTauntingThisUid(skill[55]), + monsterLastDistractedByNoisemaker(skill[55]), // shares with above as above only applies to inner demons. + monsterExtraReflexTick(skill[56]), + monsterSentrybotLookDir(fskill[10]), + monsterKnockbackTangentDir(fskill[11]), + playerStrafeVelocity(fskill[12]), + playerStrafeDir(fskill[13]), + monsterSpecialAttackUnequipSafeguard(fskill[14]), + creatureWindDir(fskill[15]), + creatureWindVelocity(fskill[16]), + creatureHoverZ(fskill[17]), + particleDuration(skill[0]), + particleShrink(skill[1]), + monsterHitTime(skill[7]), + itemNotMoving(skill[18]), + itemNotMovingClient(skill[19]), + itemSokobanReward(skill[20]), + itemOriginalOwner(skill[21]), + itemStolen(skill[22]), + itemShowOnMap(skill[23]), + itemDelayMonsterPickingUp(skill[24]), + itemReceivedDetailsFromServer(skill[25]), + itemAutoSalvageByPlayer(skill[26]), + itemSplooshed(skill[27]), + itemContainer(skill[29]), + itemWaterBob(fskill[2]), + gateInit(skill[1]), + gateStatus(skill[3]), + gateRattle(skill[4]), + gateStartHeight(fskill[0]), + gateVelZ(vel_z), + gateInverted(skill[5]), + gateDisableOpening(skill[6]), + leverStatus(skill[1]), + leverTimerTicks(skill[3]), + boulderTrapRefireAmount(skill[1]), + boulderTrapRefireDelay(skill[3]), + boulderTrapAmbience(skill[6]), + boulderTrapFired(skill[0]), + boulderTrapRefireCounter(skill[4]), + boulderTrapPreDelay(skill[5]), + boulderTrapRocksToSpawn(skill[7]), + doorDir(skill[0]), + doorInit(skill[1]), + doorStatus(skill[3]), + doorHealth(skill[4]), + doorLocked(skill[5]), + doorSmacked(skill[6]), + doorTimer(skill[7]), + doorOldStatus(skill[8]), + doorMaxHealth(skill[9]), + doorStartAng(fskill[0]), + doorPreventLockpickExploit(skill[10]), + doorForceLockedUnlocked(skill[11]), + doorDisableLockpicks(skill[12]), + doorDisableOpening(skill[13]), + doorLockpickHealth(skill[14]), + doorOldHealth(skill[15]), + doorUnlockWhenPowered(skill[16]), + particleTimerDuration(skill[0]), + particleTimerEndAction(skill[1]), + particleTimerEndSprite(skill[3]), + particleTimerCountdownAction(skill[4]), + particleTimerCountdownSprite(skill[5]), + particleTimerTarget(skill[6]), + particleTimerPreDelay(skill[7]), + particleTimerVariable1(skill[8]), + particleTimerVariable2(skill[9]), + pedestalHasOrb(skill[0]), + pedestalOrbType(skill[1]), + pedestalInvertedPower(skill[3]), + pedestalInGround(skill[4]), + pedestalInit(skill[5]), + pedestalAmbience(skill[6]), + pedestalLockOrb(skill[7]), + pedestalPowerStatus(skill[8]), + orbInitialised(skill[1]), + orbHoverDirection(skill[7]), + orbHoverWaitTimer(skill[8]), + orbStartZ(fskill[0]), + orbMaxZVelocity(fskill[1]), + orbMinZVelocity(fskill[2]), + orbTurnVelocity(fskill[3]), + portalAmbience(skill[0]), + portalInit(skill[1]), + portalNotSecret(skill[3]), + portalVictoryType(skill[4]), + portalFireAnimation(skill[5]), + portalCustomLevelsToJump(skill[6]), + portalCustomRequiresPower(skill[7]), + portalCustomSprite(skill[8]), + portalCustomSpriteAnimationFrames(skill[9]), + portalCustomZOffset(skill[10]), + portalCustomLevelText1(skill[11]), + portalCustomLevelText2(skill[12]), + portalCustomLevelText3(skill[13]), + portalCustomLevelText4(skill[14]), + portalCustomLevelText5(skill[15]), + portalCustomLevelText6(skill[16]), + portalCustomLevelText7(skill[17]), + portalCustomLevelText8(skill[18]), + teleporterX(skill[0]), + teleporterY(skill[1]), + teleporterType(skill[3]), + teleporterAmbience(skill[4]), + spellTrapType(skill[0]), + spellTrapRefire(skill[1]), + spellTrapLatchPower(skill[3]), + spellTrapFloorTile(skill[4]), + spellTrapRefireRate(skill[5]), + spellTrapAmbience(skill[6]), + spellTrapInit(skill[7]), + spellTrapCounter(skill[8]), + spellTrapReset(skill[9]), + shrineSpellEffect(skill[0]), + shrineRefire1(skill[1]), + shrineRefire2(skill[3]), + shrineDir(skill[4]), + shrineAmbience(skill[5]), + shrineInit(skill[6]), + shrineActivateDelay(skill[7]), + shrineZ(skill[8]), + shrineDestXOffset(skill[9]), + shrineDestYOffset(skill[10]), + shrineDaedalusState(skill[11]), + ceilingTileModel(skill[0]), + ceilingTileDir(skill[1]), + ceilingTileAllowTrap(skill[3]), + ceilingTileBreakable(skill[4]), + floorDecorationModel(skill[0]), + floorDecorationRotation(skill[1]), + floorDecorationHeightOffset(skill[3]), + floorDecorationXOffset(skill[4]), + floorDecorationYOffset(skill[5]), + floorDecorationDestroyIfNoWall(skill[6]), + floorDecorationInteractText1(skill[8]), + floorDecorationInteractText2(skill[9]), + floorDecorationInteractText3(skill[10]), + floorDecorationInteractText4(skill[11]), + floorDecorationInteractText5(skill[12]), + floorDecorationInteractText6(skill[13]), + floorDecorationInteractText7(skill[14]), + floorDecorationInteractText8(skill[15]), + colliderDecorationModel(skill[0]), + colliderDecorationRotation(skill[1]), + colliderDecorationHeightOffset(skill[3]), + colliderDecorationXOffset(skill[4]), + colliderDecorationYOffset(skill[5]), + colliderHasCollision(skill[6]), + colliderSizeX(skill[7]), + colliderSizeY(skill[8]), + colliderMaxHP(skill[9]), + colliderDiggable(skill[10]), + colliderDamageTypes(skill[11]), + colliderCurrentHP(skill[12]), + colliderOldHP(skill[13]), + colliderInit(skill[14]), + colliderContainedEntity(skill[15]), + colliderHideMonster(skill[16]), + colliderKillerUid(skill[17]), + furnitureType(skill[0]), + furnitureInit(skill[1]), + furnitureDir(skill[3]), + furnitureHealth(skill[4]), + furnitureMaxHealth(skill[9]), + furnitureTableRandomItemChance(skill[10]), + furnitureTableSpawnChairs(skill[11]), + furnitureOldHealth(skill[15]), + pistonCamDir(skill[0]), + pistonCamTimer(skill[1]), + pistonCamRotateSpeed(fskill[0]), + arrowPower(skill[3]), + arrowPoisonTime(skill[4]), + arrowArmorPierce(skill[5]), + arrowSpeed(fskill[4]), + arrowFallSpeed(fskill[5]), + arrowBoltDropOffRange(skill[6]), + arrowShotByWeapon(skill[7]), + arrowQuiverType(skill[8]), + arrowShotByParent(skill[9]), + arrowDropOffEquipmentModifier(skill[14]), + actmagicIsVertical(skill[6]), + actmagicIsOrbiting(skill[7]), + actmagicOrbitDist(skill[8]), + actmagicOrbitVerticalDirection(skill[9]), + actmagicOrbitLifetime(skill[10]), + actmagicMirrorReflected(skill[24]), + actmagicMirrorReflectedCaster(skill[12]), + actmagicCastByMagicstaff(skill[13]), + actmagicOrbitVerticalSpeed(fskill[2]), + actmagicOrbitStartZ(fskill[3]), + actmagicOrbitStationaryX(fskill[4]), + actmagicOrbitStationaryY(fskill[5]), + actmagicOrbitStationaryCurrentDist(fskill[6]), + actmagicSprayGravity(fskill[7]), + actmagicVelXStore(fskill[8]), + actmagicVelYStore(fskill[9]), + actmagicVelZStore(fskill[10]), + actmagicOrbitStationaryHitTarget(skill[14]), + actmagicOrbitHitTargetUID1(skill[15]), + actmagicOrbitHitTargetUID2(skill[16]), + actmagicOrbitHitTargetUID3(skill[17]), + actmagicOrbitHitTargetUID4(skill[18]), + actmagicProjectileArc(skill[19]), + actmagicOrbitCastFromSpell(skill[20]), + actmagicSpellbookBonus(skill[21]), + actmagicCastByTinkerTrap(skill[22]), + actmagicTinkerTrapFriendlyFire(skill[23]), + actmagicReflectionCount(skill[25]), + actmagicFromSpellbook(skill[26]), + actmagicSpray(skill[27]), + actmagicEmitter(skill[29]), + actmagicDelayMove(skill[30]), + actmagicNoHitMessage(skill[31]), + actmagicNoParticle(skill[32]), + actmagicNoLight(skill[33]), + goldAmount(skill[0]), + goldAmbience(skill[1]), + goldSokoban(skill[2]), + goldBouncing(skill[3]), + goldInContainer(skill[4]), + interactedByMonster(skill[47]), + highlightForUI(fskill[29]), + highlightForUIGlow(fskill[28]), + grayscaleGLRender(fskill[27]), + noColorChangeAllyLimb(fskill[26]), + soundSourceFired(skill[0]), + soundSourceToPlay(skill[1]), + soundSourceVolume(skill[2]), + soundSourceLatchOn(skill[3]), + soundSourceDelay(skill[4]), + soundSourceDelayCounter(skill[5]), + soundSourceOrigin(skill[6]), + lightSourceBrightness(skill[0]), + lightSourceAlwaysOn(skill[1]), + lightSourceInvertPower(skill[2]), + lightSourceLatchOn(skill[3]), + lightSourceRadius(skill[4]), + lightSourceFlicker(skill[5]), + lightSourceDelay(skill[6]), + lightSourceDelayCounter(skill[7]), + lightSourceRGB(skill[11]), + textSourceColorRGB(skill[0]), + textSourceVariables4W(skill[1]), + textSourceDelay(skill[2]), + textSourceIsScript(skill[3]), + textSourceBegin(skill[4]), + signalActivateDelay(skill[1]), + signalTimerInterval(skill[2]), + signalTimerRepeatCount(skill[3]), + signalTimerLatchInput(skill[4]), + signalInputDirection(skill[5]), + signalGateANDPowerCount(skill[9]), + signalInvertOutput(skill[10]), + wallLockState(skill[0]), + wallLockInvertPower(skill[1]), + wallLockTurnable(skill[3]), + wallLockMaterial(skill[4]), + wallLockDir(skill[5]), + wallLockClientInteractDelay(skill[6]), + wallLockPlayerInteracting(skill[7]), + wallLockPower(skill[8]), + wallLockInit(skill[9]), + wallLockTimer(skill[10]), + wallLockPickable(skill[11]), + wallLockPickHealth(skill[12]), + wallLockPickableSkeletonKey(skill[13]), + wallLockPreventLockpickExploit(skill[14]), + wallLockAutoGenKey(skill[15]), + effectPolymorph(skill[50]), + effectShapeshift(skill[53]), + entityShowOnMap(skill[59]), + thrownProjectilePower(skill[19]), + thrownProjectileCharge(skill[20]), + playerStartDir(skill[1]), + pressurePlateTriggerType(skill[3]), + worldTooltipAlpha(fskill[0]), + worldTooltipZ(fskill[1]), + worldTooltipActive(skill[0]), + worldTooltipPlayer(skill[1]), + worldTooltipInit(skill[3]), + worldTooltipFadeDelay(skill[4]), + worldTooltipIgnoreDrawing(skill[5]), + worldTooltipRequiresButtonHeld(skill[6]), + statueInit(skill[0]), + statueDir(skill[1]), + statueId(skill[3]) +{ + int c; + // add the entity to the entity list + if ( !pos ) + { + mynode = list_AddNodeFirst(entlist); + } + else + { + mynode = list_AddNodeLast(entlist); + } + + if ( mynode ) + { + mynode->element = this; + mynode->deconstructor = &entityDeconstructor; + mynode->size = sizeof(Entity); + } + + myCreatureListNode = nullptr; + if ( creaturelist ) + { + addToCreatureList(creaturelist); + } + myWorldUIListNode = nullptr; + myTileListNode = nullptr; + + // now reset all of my data elements + lastupdate = 0; + lastupdateserver = 0; + ticks = 0; + x = 0; + y = 0; + z = 0; + new_x = 0; + new_y = 0; + new_z = 0; + focalx = 0; + focaly = 0; + focalz = 0; + scalex = 1; + scaley = 1; + scalez = 1; + vel_x = 0; + vel_y = 0; + vel_z = 0; + sizex = 0; + sizey = 0; + yaw = 0; + pitch = 0; + roll = 0; + new_yaw = 0; + new_pitch = 0; + new_roll = 0; +#ifndef EDITOR + lerpCurrentState.resetMovement(); + lerpCurrentState.resetPosition(); + lerpPreviousState.resetMovement(); + lerpPreviousState.resetPosition(); + lerpRenderState.resetMovement(); + lerpRenderState.resetPosition(); +#endif + bNeedsRenderPositionInit = true; + bUseRenderInterpolation = false; + mapGenerationRoomX = 0; + mapGenerationRoomY = 0; + lerp_ox = 0.0; + lerp_oy = 0.0; + sprite = in_sprite; + light = nullptr; + string = nullptr; + children.first = nullptr; + children.last = nullptr; + //this->magic_effects = (list_t *) malloc(sizeof(list_t)); + //this->magic_effects->first = NULL; this->magic_effects->last = NULL; + for ( c = 0; c < NUMENTITYSKILLS; ++c ) + { + skill[c] = 0; + } + for ( c = 0; c < NUMENTITYFSKILLS; ++c ) + { + fskill[c] = 0; + } + skill[2] = -1; + for ( c = 0; c < 24; c++ ) + { + flags[c] = false; + } + if ( entlist != nullptr && entlist == map.entities ) + { + if ( multiplayer != CLIENT || loading ) + { + uid = entity_uids; + entity_uids++; + map.entities_map.insert({ uid, mynode }); + } + else + { + uid = -2; + } + } + else + { + uid = -2; + } + behavior = nullptr; + ranbehavior = false; + parent = 0; + path = nullptr; + monsterAllyIndex = -1; // set to -1 to not reference player indices 0-3. + /*if ( checkSpriteType(this->sprite) > 1 ) + { + setSpriteAttributes(this, nullptr, nullptr); + }*/ + + clientStats = nullptr; + clientsHaveItsStats = false; +} + Monster editorSpriteTypeToMonster(Sint32 sprite) { Monster monsterType = NOTHING; @@ -56,6 +559,12 @@ Monster editorSpriteTypeToMonster(Sint32 sprite) case 166: monsterType = GYROBOT; break; case 188: monsterType = BAT_SMALL; break; case 189: monsterType = BUGBEAR; break; + case 204: monsterType = DRYAD; break; + case 205: monsterType = MYCONID; break; + case 206: monsterType = SALAMANDER; break; + case 207: monsterType = GREMLIN; break; + case 246: monsterType = REVENANT_SKULL; break; + case 247: monsterType = MONSTER_ADORCISED_WEAPON; break; default: break; } @@ -109,6 +618,12 @@ int checkSpriteType(Sint32 sprite) case 195: case 196: case 197: + case 204: + case 205: + case 206: + case 207: + case 246: + case 247: //monsters return 1; break; @@ -218,6 +733,25 @@ int checkSpriteType(Sint32 sprite) case 34: // act trap return 29; + case 208: + case 209: + case 210: + case 211: + // wall locks + return 30; + case 212: + case 213: + case 214: + case 215: + // wall buttons + return 31; + case 217: + case 218: + // iron doors + return 32; + case 220: + // wind + return 33; default: return 0; break; @@ -561,7 +1095,199 @@ char itemNameStrings[NUM_ITEM_STRINGS][32] = "hat_hood_whispers", "ring_resolve", "cloak_guardian", - "mask_marigold" + "mask_marigold", + "key_stone", + "key_bone", + "key_bronze", + "key_iron", + "key_silver", + "key_gold", + "key_crystal", + "key_machine", + "tool_foci_fire", + "instrument_flute", + "instrument_lyre", + "instrument_drum", + "instrument_lute", + "instrument_horn", + "rapier", + "amulet_burningresist", + "grease_ball", + "branch_staff", + "branch_bow", + "branch_bow_infected", + "dust_ball", + "bolas", + "steel_flail", + "food_ration", + "food_ration_spicy", + "food_ration_sour", + "food_ration_bitter", + "food_ration_hearty", + "food_ration_herbal", + "food_ration_sweet", + "slop_ball", + "tool_frying_pan", + "cleat_boots", + "bandit_breastpiece", + "tunic_blouse", + "bone_breastpiece", + "blackiron_breastpiece", + "silver_breastpiece", + "iron_pauldrons", + "quilted_gambeson", + "robe_cultist", + "robe_healer", + "robe_monk", + "robe_wizard", + "shawl", + "chain_hauberk", + "bone_bracers", + "blackiron_gauntlets", + "silver_gauntlets", + "quilted_gloves", + "chain_gloves", + "bone_boots", + "blackiron_boots", + "silver_boots", + "quilted_boots", + "loafers", + "chain_boots", + "scutum", + "bone_shield", + "blackiron_shield", + "silver_shield", + "cloak_dendrite", + "bone_helm", + "blackiron_helm", + "silver_helm", + "hat_felt", + "quilted_cap", + "hood_teal", + "chain_coif", + "food_shroom", + "food_nut", + "tool_foci_snow", + "tool_foci_needles", + "tool_foci_arcs", + "tool_foci_sand", + "tool_foci_dark_life", + "tool_foci_dark_rift", + "tool_foci_dark_silence", + "tool_foci_dark_vengeance", + "tool_foci_dark_suppress", + "tool_foci_light_peace", + "tool_foci_light_justice", + "tool_foci_light_providence", + "tool_foci_light_purity", + "tool_foci_light_sanctuary", + "magicstaff_scepter", + "tome_sorcery", + "tome_mysticism", + "tome_thaumaturgy", + "hat_circlet_sorcery", + "hat_circlet_thaumaturgy", + "tool_duck", + "shillelagh_mace", + "claymore_sword", + "anelace_sword", + "lance_spear", + "steel_falshion", + "steel_greataxe", + "blackiron_axe", + "blackiron_crossbow", + "blackiron_dart", + "blackiron_mace", + "blackiron_sword", + "blackiron_trident", + "bone_axe", + "bone_mace", + "bone_shortbow", + "bone_spear", + "bone_sword", + "bone_throwing", + "silver_axe", + "silver_glaive", + "silver_mace", + "silver_plumbata", + "silver_sword", + "quiver_bone", + "quiver_blackiron", + "gem_jewel", + "spellbook_meteor", + "spellbook_ice_wave", + "spellbook_guard_body", + "spellbook_guard_spirit", + "spellbook_divine_guard", + "spellbook_prof_nimbleness", + "spellbook_prof_greater_might", + "spellbook_prof_counsel", + "spellbook_prof_sturdiness", + "spellbook_bless_food", + "spellbook_pinpoint", + "spellbook_donation", + "spellbook_scry_allies", + "spellbook_scry_traps", + "spellbook_scry_treasures", + "spellbook_detect_enemy", + "spellbook_turn_undead", + "spellbook_heal_other", + "spellbook_blood_ward", + "spellbook_divine_zeal", + "spellbook_maximise", + "spellbook_minimise", + "spellbook_incoherence", + "spellbook_overcharge", + "spellbook_envenom_weapon", + "spellbook_psychic_spear", + "spellbook_defy_flesh", + "spellbook_grease_spray", + "spellbook_blood_waves", + "spellbook_command", + "spellbook_metallurgy", + "spellbook_forge_key", + "spellbook_reshape_weapon", + "spellbook_alter_arrow", + "spellbook_void_chest", + "spellbook_lead_bolt", + "spellbook_numbing_bolt", + "spellbook_curse_flesh", + "spellbook_cowardice", + "spellbook_seek_ally", + "spellbook_deep_shade", + "spellbook_spirit_weapon", + "spellbook_spores", + "spellbook_windgate", + "spellbook_telekinesis", + "spellbook_disarm", + "spellbook_abundance", + "spellbook_preserve", + "spellbook_sabotage", + "spellbook_mist_form", + "spellbook_force_shield", + "spellbook_splinter_gear", + "spellbook_attract_items", + "spellbook_absorb_magic", + "spellbook_tunnel", + "spellbook_null_area", + "spellbook_fire_sprite", + "spellbook_spin", + "spellbook_cleanse_food", + "spellbook_flame_cloak", + "spellbook_lightning_bolt", + "spellbook_disrupt_earth", + "spellbook_fire_wall", + "spellbook_slam", + "spellbook_ignite", + "spellbook_shatter_objects", + "spellbook_kinetic_field", + "spellbook_thorns", + "spellbook_magicians_armor", + "spellbook_heal_minor", + "spellbook_sigil", + "spellbook_sanctuary", + "spellbook_holy_beam", + "spellbook_dominate", "" }; @@ -608,6 +1334,15 @@ char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32] = "hat_hood_apprentice", "hat_hood_assassin", "hat_hood_whispers", + "bone_helm", + "blackiron_helm", + "silver_helm", + "hat_felt", + "quilted_cap", + "hood_teal", + "chain_coif", + "hat_circlet_sorcery", + "hat_circlet_thaumaturgy", "" }, { @@ -739,6 +1474,113 @@ char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32] = "artifact_orb_red", "artifact_orb_purple", "artifact_orb_green", + "rapier", + "grease_ball", + "branch_staff", + "branch_bow", + "branch_bow_infected", + "dust_ball", + "bolas", + "steel_flail", + "slop_ball", + "magicstaff_scepter", + "shillelagh_mace", + "claymore_sword", + "anelace_sword", + "lance_spear", + "steel_falshion", + "steel_greataxe", + "blackiron_axe", + "blackiron_crossbow", + "blackiron_dart", + "blackiron_mace", + "blackiron_sword", + "blackiron_trident", + "bone_axe", + "bone_mace", + "bone_shortbow", + "bone_spear", + "bone_sword", + "bone_throwing", + "silver_axe", + "silver_glaive", + "silver_mace", + "silver_plumbata", + "silver_sword", + "spellbook_meteor", + "spellbook_ice_wave", + "spellbook_guard_body", + "spellbook_guard_spirit", + "spellbook_divine_guard", + "spellbook_prof_nimbleness", + "spellbook_prof_greater_might", + "spellbook_prof_counsel", + "spellbook_prof_sturdiness", + "spellbook_bless_food", + "spellbook_pinpoint", + "spellbook_donation", + "spellbook_scry_allies", + "spellbook_scry_traps", + "spellbook_scry_treasures", + "spellbook_detect_enemy", + "spellbook_turn_undead", + "spellbook_heal_other", + "spellbook_blood_ward", + "spellbook_divine_zeal", + "spellbook_maximise", + "spellbook_minimise", + "spellbook_incoherence", + "spellbook_overcharge", + "spellbook_envenom_weapon", + "spellbook_psychic_spear", + "spellbook_defy_flesh", + "spellbook_grease_spray", + "spellbook_blood_waves", + "spellbook_command", + "spellbook_metallurgy", + "spellbook_forge_key", + "spellbook_reshape_weapon", + "spellbook_alter_arrow", + "spellbook_void_chest", + "spellbook_lead_bolt", + "spellbook_numbing_bolt", + "spellbook_curse_flesh", + "spellbook_cowardice", + "spellbook_seek_ally", + "spellbook_deep_shade", + "spellbook_spirit_weapon", + "spellbook_spores", + "spellbook_windgate", + "spellbook_telekinesis", + "spellbook_disarm", + "spellbook_abundance", + "spellbook_preserve", + "spellbook_sabotage", + "spellbook_mist_form", + "spellbook_force_shield", + "spellbook_splinter_gear", + "spellbook_attract_items", + "spellbook_absorb_magic", + "spellbook_tunnel", + "spellbook_null_area", + "spellbook_fire_sprite", + "spellbook_spin", + "spellbook_cleanse_food", + "spellbook_flame_cloak", + "spellbook_lightning_bolt", + "spellbook_disrupt_earth", + "spellbook_fire_wall", + "spellbook_slam", + "spellbook_ignite", + "spellbook_shatter_objects", + "spellbook_kinetic_field", + "spellbook_thorns", + "spellbook_magicians_armor", + "spellbook_heal_minor", + "spellbook_sigil", + "spellbook_sanctuary", + "spellbook_holy_beam", + "spellbook_dominate", "" }, { @@ -761,6 +1603,33 @@ char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32] = "quiver_heavy", "quiver_crystal", "quiver_hunting", + "tool_foci_fire", + "instrument_flute", + "instrument_lyre", + "instrument_drum", + "instrument_lute", + "instrument_horn", + "tool_frying_pan", + "scutum", + "bone_shield", + "blackiron_shield", + "silver_shield", + "tool_foci_snow", + "tool_foci_needles", + "tool_foci_arcs", + "tool_foci_sand", + "tool_foci_dark_life", + "tool_foci_dark_rift", + "tool_foci_dark_silence", + "tool_foci_dark_vengeance", + "tool_foci_dark_suppress", + "tool_foci_light_peace", + "tool_foci_light_justice", + "tool_foci_light_providence", + "tool_foci_light_purity", + "tool_foci_light_sanctuary", + "quiver_bone", + "quiver_blackiron", "" }, { @@ -777,6 +1646,19 @@ char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32] = "tunic", "silver_doublet", "machinist_apron", + "bandit_breastpiece", + "tunic_blouse", + "bone_breastpiece", + "blackiron_breastpiece", + "silver_breastpiece", + "iron_pauldrons", + "quilted_gambeson", + "robe_cultist", + "robe_healer", + "robe_monk", + "robe_wizard", + "shawl", + "chain_hauberk", "" }, { @@ -792,6 +1674,13 @@ char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32] = "crystal_boots", "artifact_boots", "suede_boots", + "cleat_boots", + "bone_boots", + "blackiron_boots", + "silver_boots", + "quilted_boots", + "loafers", + "chain_boots", "" }, { @@ -821,6 +1710,7 @@ char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32] = "amulet_magicreflection", "amulet_strangulation", "amulet_poisonresistance", + "amulet_burningresist", "" }, { @@ -835,6 +1725,7 @@ char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32] = "artifact_cloak", "cloak_backpack", "cloak_guardian", + "cloak_dendrite", "" }, { @@ -879,12 +1770,17 @@ char itemStringsByType[10][NUM_ITEM_STRINGS_BY_TYPE][32] = "iron_knuckles", "spiked_gauntlets", "suede_gloves", + "bone_bracers", + "blackiron_gauntlets", + "silver_gauntlets", + "quilted_gloves", + "chain_gloves", "" } }; -char spriteEditorNameStrings[NUM_EDITOR_SPRITES][64] = +std::vector spriteEditorNameStrings = { "NULL", "PLAYER START", @@ -1087,10 +1983,56 @@ char spriteEditorNameStrings[NUM_EDITOR_SPRITES][64] = "NOT USED", "NOT USED", "NOT USED", - "ASSIST SHRINE" + "ASSIST SHRINE", + "NOT USED", + "NOT USED", + "DRYAD", + "MYCONID", + "SALAMANDER", + "GREMLIN", + "WALL LOCK (East)", + "WALL LOCK (South)", + "WALL LOCK (West)", + "WALL LOCK (North)", + "WALL BUTTON (East)", + "WALL BUTTON (South)", + "WALL BUTTON (West)", + "WALL BUTTON (North)", + "NO DIG TILE", + "IRON DOOR (North-South)", + "IRON DOOR (East-West)", + "SLIPPERY TILE", + "WIND TILE", + "SLOW TILE", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "NOT USED", + "REVENANT_SKULL", + "ADORCISED_WEAPON" }; -char monsterEditorNameStrings[NUMMONSTERS][16] = +char monsterEditorNameStrings[NUMMONSTERS][32] = { "nothing", "human", @@ -1129,7 +2071,22 @@ char monsterEditorNameStrings[NUMMONSTERS][16] = "spellbot", "gyrobot", "dummybot", - "bugbear" + "bugbear", + "dryad", + "myconid", + "salamander", + "gremlin", + "revenant_skull", + "minimimic", + "monster_adorcised_weapon", + "flame_elemental", + "hologram", + "moth", + "earth_elemental", + "duck_small", + "monster_unused_6", + "monster_unused_7", + "monster_unused_8" }; char tileEditorNameStrings[NUM_EDITOR_TILES][44] = @@ -1421,6 +2378,12 @@ int canWearEquip(Entity* entity, int category) case CRYSTALGOLEM: case COCKATRICE: case MIMIC: + case MINIMIMIC: + case REVENANT_SKULL: + case FLAME_ELEMENTAL: + case EARTH_ELEMENTAL: + case HOLOGRAM: + case MOTH_SMALL: equipType = 0; break; @@ -1428,6 +2391,7 @@ int canWearEquip(Entity* entity, int category) case LICH: case CREATURE_IMP: case DEMON: + case MONSTER_ADORCISED_WEAPON: equipType = 1; break; @@ -1452,6 +2416,10 @@ int canWearEquip(Entity* entity, int category) case GOATMAN: case KOBOLD: case INSECTOID: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: equipType = 3; break; @@ -1778,6 +2746,7 @@ void setSpriteAttributes(Entity* entityNew, Entity* entityToCopy, Entity* entity entityNew->floorDecorationHeightOffset = entityToCopy->floorDecorationHeightOffset; entityNew->floorDecorationXOffset = entityToCopy->floorDecorationXOffset; entityNew->floorDecorationYOffset = entityToCopy->floorDecorationYOffset; + entityNew->floorDecorationDestroyIfNoWall = entityToCopy->floorDecorationDestroyIfNoWall; for ( int i = 8; i < 60; ++i ) { entityNew->skill[i] = entityToCopy->skill[i]; @@ -1791,6 +2760,7 @@ void setSpriteAttributes(Entity* entityNew, Entity* entityToCopy, Entity* entity entityNew->floorDecorationHeightOffset = 0; entityNew->floorDecorationXOffset = 0; entityNew->floorDecorationYOffset = 0; + entityNew->floorDecorationDestroyIfNoWall = -1; for ( int i = 8; i < 60; ++i ) { entityNew->skill[i] = 0; @@ -1830,6 +2800,7 @@ void setSpriteAttributes(Entity* entityNew, Entity* entityToCopy, Entity* entity entityNew->lightSourceRadius = entityToCopy->lightSourceRadius; entityNew->lightSourceFlicker = entityToCopy->lightSourceFlicker; entityNew->lightSourceDelay = entityToCopy->lightSourceDelay; + entityNew->lightSourceRGB = entityToCopy->lightSourceRGB; } else { @@ -1841,6 +2812,10 @@ void setSpriteAttributes(Entity* entityNew, Entity* entityToCopy, Entity* entity entityNew->lightSourceRadius = 5; entityNew->lightSourceFlicker = 0; entityNew->lightSourceDelay = 0; + entityNew->lightSourceRGB = 0; + entityNew->lightSourceRGB |= 255; + entityNew->lightSourceRGB |= (255 << 8); + entityNew->lightSourceRGB |= (255 << 16); } } else if ( spriteType == 16 ) @@ -2006,6 +2981,25 @@ void setSpriteAttributes(Entity* entityNew, Entity* entityToCopy, Entity* entity entityNew->doorDisableOpening = 0; } } + else if ( spriteType == 32 ) // iron doors + { + if ( entityToCopy != nullptr ) + { + // copy old entity attributes to newly created. + entityNew->doorUnlockWhenPowered = entityToCopy->doorUnlockWhenPowered; + entityNew->doorDisableLockpicks = entityToCopy->doorDisableLockpicks; + entityNew->doorForceLockedUnlocked = entityToCopy->doorForceLockedUnlocked; + entityNew->doorDisableOpening = entityToCopy->doorDisableOpening; + } + else + { + // set default new entity attributes. + entityNew->doorUnlockWhenPowered = 1; + entityNew->doorDisableLockpicks = 1; + entityNew->doorDisableOpening = 1; + entityNew->doorForceLockedUnlocked = 1; + } + } else if ( spriteType == 22 ) // gates { if ( entityToCopy != nullptr ) @@ -2131,6 +3125,57 @@ void setSpriteAttributes(Entity* entityNew, Entity* entityToCopy, Entity* entity entityNew->pressurePlateTriggerType = 0; } } + else if ( spriteType == 30 ) // wall locks + { + if ( entityToCopy != nullptr ) + { + // copy old entity attributes to newly created. + entityNew->wallLockMaterial = entityToCopy->wallLockMaterial; + entityNew->wallLockInvertPower = entityToCopy->wallLockInvertPower; + entityNew->wallLockTurnable = entityToCopy->wallLockTurnable; + entityNew->wallLockPickable = entityToCopy->wallLockPickable; + entityNew->wallLockPickableSkeletonKey = entityToCopy->wallLockPickableSkeletonKey; + entityNew->wallLockAutoGenKey = entityToCopy->wallLockAutoGenKey; + } + else + { + // set default new entity attributes. + entityNew->wallLockMaterial = 0; + entityNew->wallLockInvertPower = 0; + entityNew->wallLockTurnable = 0; + entityNew->wallLockPickable = -1; + entityNew->wallLockPickableSkeletonKey = 0; + entityNew->wallLockAutoGenKey = 0; + } + } + else if ( spriteType == 31 ) // wall buttons + { + if ( entityToCopy != nullptr ) + { + // copy old entity attributes to newly created. + entityNew->wallLockInvertPower = entityToCopy->wallLockInvertPower; + entityNew->wallLockTimer = entityToCopy->wallLockTimer; + } + else + { + // set default new entity attributes. + entityNew->wallLockInvertPower = 0; + entityNew->wallLockTimer = 0; + } + } + else if ( spriteType == 33 ) // wind + { + if ( entityToCopy != nullptr ) + { + // copy old entity attributes to newly created. + entityNew->skill[0] = entityToCopy->skill[0]; + } + else + { + // set default new entity attributes. + entityNew->skill[0] = 0; + } + } if ( entityToCopy != nullptr ) { diff --git a/src/eos.cpp b/src/eos.cpp index 71fcae6d0..4233e2c15 100644 --- a/src/eos.cpp +++ b/src/eos.cpp @@ -3905,18 +3905,31 @@ void EOSFuncs::queryDLCOwnership() { EcomHandle = EOS_Platform_GetEcomInterface(PlatformHandle); - EOS_Ecom_QueryEntitlementsOptions options{}; - options.ApiVersion = EOS_ECOM_QUERYENTITLEMENTS_API_LATEST; - options.bIncludeRedeemed = true; - std::vector entitlements; - entitlements.push_back("fced51d547714291869b8847fdd770e8"); - entitlements.push_back("7ea3754f8bfa4069938fd0bee3e7197b"); - - options.EntitlementNames = entitlements.data(); - options.EntitlementNameCount = entitlements.size(); - options.LocalUserId = EOSFuncs::Helpers_t::epicIdFromString(CurrentUserInfo.epicAccountId.c_str()); - - EOS_Ecom_QueryEntitlements(EcomHandle, &options, nullptr, OnEcomQueryEntitlementsCallback); + // Deprecated in newest SDK + //EOS_Ecom_QueryEntitlementsOptions options{}; + //options.ApiVersion = EOS_ECOM_QUERYENTITLEMENTS_API_LATEST; + //options.bIncludeRedeemed = true; + //std::vector entitlements; + //entitlements.push_back("fced51d547714291869b8847fdd770e8"); + //entitlements.push_back("7ea3754f8bfa4069938fd0bee3e7197b"); + //entitlements.push_back("8f68cbe981e346afaaeddebcd9447e9b"); + // + //options.EntitlementNames = entitlements.data(); + //options.EntitlementNameCount = entitlements.size(); + //options.LocalUserId = EOSFuncs::Helpers_t::epicIdFromString(CurrentUserInfo.epicAccountId.c_str()); + // + //EOS_Ecom_QueryEntitlements(EcomHandle, &options, nullptr, OnEcomQueryEntitlementsCallback); + + EOS_Ecom_QueryOwnershipOptions ownershipOptions{}; + ownershipOptions.ApiVersion = EOS_ECOM_QUERYOWNERSHIP_API_LATEST; + ownershipOptions.LocalUserId = EOSFuncs::Helpers_t::epicIdFromString(CurrentUserInfo.epicAccountId.c_str()); + std::vector catalogItems; + catalogItems.push_back("fced51d547714291869b8847fdd770e8"); + catalogItems.push_back("7ea3754f8bfa4069938fd0bee3e7197b"); + catalogItems.push_back("8f68cbe981e346afaaeddebcd9447e9b"); + ownershipOptions.CatalogItemIds = catalogItems.data(); + ownershipOptions.CatalogItemIdCount = catalogItems.size(); + EOS_Ecom_QueryOwnership(EcomHandle, &ownershipOptions, nullptr, OnEcomQueryOwnershipCallback); } void EOS_CALL EOSFuncs::OnEcomQueryEntitlementsCallback(const EOS_Ecom_QueryEntitlementsCallbackInfo* data) @@ -3960,6 +3973,11 @@ void EOS_CALL EOSFuncs::OnEcomQueryEntitlementsCallback(const EOS_Ecom_QueryEnti enabledDLCPack2 = true; EOSFuncs::logInfo("Legends & Pariahs DLC Enabled"); } + else if ( id.compare("8f68cbe981e346afaaeddebcd9447e9b") == 0 ) + { + enabledDLCPack3 = true; + EOSFuncs::logInfo("Deserters & Disciples DLC Enabled"); + } //EOSFuncs::logInfo("Index: %d | Id %s: | Entitlement Name: %s | CatalogItemId: %s | Redeemed: %d", i, e->EntitlementId, e->EntitlementName, e->CatalogItemId, (e->bRedeemed == EOS_TRUE) ? 1 : 0); } EOS_Ecom_Entitlement_Release(e); @@ -3983,7 +4001,25 @@ void EOS_CALL EOSFuncs::OnEcomQueryOwnershipCallback(const EOS_Ecom_QueryOwnersh { for (int i = 0; i < data->ItemOwnershipCount; ++i) { - EOSFuncs::logInfo("OnEcomQueryOwnershipCallback: Ownership status: %d, %d", static_cast(data->ItemOwnership[i].OwnershipStatus), data->ItemOwnershipCount); + if ( data->ItemOwnership[i].OwnershipStatus == EOS_EOwnershipStatus::EOS_OS_Owned ) + { + std::string itemName = data->ItemOwnership[i].Id; + if ( itemName.compare("8f68cbe981e346afaaeddebcd9447e9b") == 0 ) + { + enabledDLCPack3 = true; + EOSFuncs::logInfo("Deserters & Disciples DLC Enabled"); + } + if ( itemName.compare("fced51d547714291869b8847fdd770e8") == 0 ) + { + enabledDLCPack1 = true; + EOSFuncs::logInfo("Myths & Outcasts DLC Enabled"); + } + else if ( itemName.compare("7ea3754f8bfa4069938fd0bee3e7197b") == 0 ) + { + enabledDLCPack2 = true; + EOSFuncs::logInfo("Legends & Pariahs DLC Enabled"); + } + } } } else diff --git a/src/files.cpp b/src/files.cpp index 85839b230..b67dfbe4f 100644 --- a/src/files.cpp +++ b/src/files.cpp @@ -9,6 +9,8 @@ -------------------------------------------------------------------------------*/ +#include "main.hpp" + #include #include #include @@ -19,7 +21,6 @@ #include #include -#include "main.hpp" #include "files.hpp" #include "engine/audio/sound.hpp" #include "entity.hpp" @@ -82,746 +83,1178 @@ bool isCurrentHoliday(bool force) { } std::unordered_map mapHashes = { - { "boss.lmp", 2376307 }, - { "bramscastle.lmp", 2944609 }, - { "caves.lmp", 777612 }, - { "caves00.lmp", 70935 }, - { "caves01.lmp", 13350 }, - { "caves01a.lmp", 5 }, - { "caves01b.lmp", 898 }, - { "caves01c.lmp", 1238 }, - { "caves01d.lmp", 709 }, - { "caves01e.lmp", 5 }, - { "caves01f.lmp", 955 }, - { "caves02.lmp", 6995 }, - { "caves03.lmp", 11883 }, - { "caves04.lmp", 15294 }, - { "caves05.lmp", 10359 }, - { "caves06.lmp", 8376 }, - { "caves07.lmp", 9198 }, - { "caves08.lmp", 6873 }, - { "caves09.lmp", 102539 }, - { "caves09a.lmp", 2836 }, - { "caves09b.lmp", 6610 }, - { "caves09c.lmp", 4989 }, - { "caves09d.lmp", 6168 }, - { "caves09e.lmp", 3999 }, - { "caves10.lmp", 28899 }, - { "caves11.lmp", 35066 }, - { "caves12.lmp", 39802 }, - { "caves13.lmp", 45478 }, - { "caves13a.lmp", 1855 }, - { "caves13b.lmp", 1678 }, - { "caves13c.lmp", 6637 }, - { "caves13d.lmp", 3386 }, - { "caves13e.lmp", 2892 }, - { "caves14.lmp", 37757 }, - { "caves15.lmp", 26569 }, - { "caves16.lmp", 142126 }, - { "caves17.lmp", 31734 }, - { "caves18.lmp", 36806 }, - { "caves19.lmp", 25878 }, - { "caves20.lmp", 33315 }, - { "caves21.lmp", 16169 }, - { "caves22.lmp", 26212 }, - { "caves23.lmp", 35367 }, - { "caves24.lmp", 82144 }, - { "caves24a.lmp", 175812 }, - { "caves24b.lmp", 293908 }, - { "caves24c.lmp", 266809 }, - { "caves24d.lmp", 70970 }, - { "caves25.lmp", 19691 }, - { "caves26.lmp", 41694 }, - { "caves27.lmp", 10622 }, - { "caves28.lmp", 8712 }, - { "caveslair.lmp", 4869964 }, - { "cavessecret.lmp", 198959 }, - { "cavestocitadel.lmp", 276221 }, - { "citadel.lmp", 729069 }, - { "citadel00.lmp", 23997 }, - { "citadel01.lmp", 30094 }, - { "citadel01a.lmp", 2025 }, - { "citadel01b.lmp", 2224 }, - { "citadel01c.lmp", 2144 }, - { "citadel01d.lmp", 2377 }, - { "citadel01e.lmp", 582 }, - { "citadel01f.lmp", 204 }, - { "citadel01g.lmp", 6 }, - { "citadel02.lmp", 22718 }, - { "citadel02a.lmp", 4 }, - { "citadel02b.lmp", 4 }, - { "citadel02c.lmp", 4 }, - { "citadel02d.lmp", 4 }, - { "citadel02e.lmp", 4 }, - { "citadel02f.lmp", 4 }, - { "citadel02g.lmp", 4 }, - { "citadel03.lmp", 25112 }, - { "citadel04.lmp", 28361 }, - { "citadel05.lmp", 29081 }, - { "citadel06.lmp", 26711 }, - { "citadel07.lmp", 28380 }, - { "citadel08.lmp", 26768 }, - { "citadel09.lmp", 31320 }, - { "citadel09a.lmp", 5545 }, - { "citadel09b.lmp", 7174 }, - { "citadel09c.lmp", 6969 }, - { "citadel10.lmp", 31323 }, - { "citadel10a.lmp", 5557 }, - { "citadel10b.lmp", 10195 }, - { "citadel10c.lmp", 6072 }, - { "citadel11.lmp", 31330 }, - { "citadel11a.lmp", 4930 }, - { "citadel11b.lmp", 7722 }, - { "citadel11c.lmp", 8961 }, - { "citadel11d.lmp", 4179 }, - { "citadel12.lmp", 31330 }, - { "citadel12a.lmp", 5545 }, - { "citadel12b.lmp", 6497 }, - { "citadel12c.lmp", 7446 }, - { "citadel13.lmp", 62357 }, - { "citadel13a.lmp", 37133 }, - { "citadel13b.lmp", 12526 }, - { "citadel14.lmp", 121487 }, - { "citadel14a.lmp", 282262 }, - { "citadel14b.lmp", 51979 }, - { "citadel14c.lmp", 67253 }, - { "citadel14d.lmp", 43379 }, - { "citadel15.lmp", 62368 }, - { "citadel15a.lmp", 12141 }, - { "citadel15b.lmp", 24927 }, - { "citadel16.lmp", 55383 }, - { "citadel16a.lmp", 11376 }, - { "citadel16b.lmp", 14784 }, - { "citadel17.lmp", 112499 }, - { "citadel17a.lmp", 7279 }, - { "citadel17b.lmp", 1729 }, - { "citadel17c.lmp", 6786 }, - { "citadel17d.lmp", 10055 }, - { "citadel18.lmp", 31650 }, - { "citadel19.lmp", 13068 }, - { "citadel20.lmp", 14119 }, - { "citadel21.lmp", 72586 }, - { "citadelsecret.lmp", 104229 }, - { "gnomishmines.lmp", 603055 }, - { "greatcastle.lmp", 9978472 }, - { "hamlet.lmp", 8390565 }, - { "hell.lmp", 3511919 }, - { "hell00.lmp", 28651 }, - { "hell01.lmp", 22399 }, - { "hell01a.lmp", 12949 }, - { "hell01b.lmp", 18166 }, - { "hell01c.lmp", 19627 }, - { "hell01d.lmp", 16459 }, - { "hell01e.lmp", 36334 }, - { "hell01f.lmp", 18020 }, - { "hell02.lmp", 22399 }, - { "hell02a.lmp", 3763 }, - { "hell02b.lmp", 1670 }, - { "hell02c.lmp", 1910 }, - { "hell02d.lmp", 4375 }, - { "hell02e.lmp", 14071 }, - { "hell03.lmp", 50240 }, - { "hell03a.lmp", 12761 }, - { "hell03b.lmp", 40402 }, - { "hell03c.lmp", 40234 }, - { "hell03d.lmp", 49944 }, - { "hell03e.lmp", 15617 }, - { "hell04.lmp", 22972 }, - { "hell04a.lmp", 10679 }, - { "hell04b.lmp", 8986 }, - { "hell04c.lmp", 5239 }, - { "hell04d.lmp", 31329 }, - { "hell04e.lmp", 4952 }, - { "hell04f.lmp", 9569 }, - { "hell05.lmp", 25407 }, - { "hell05a.lmp", 7189 }, - { "hell05b.lmp", 3665 }, - { "hell05c.lmp", 32373 }, - { "hell05d.lmp", 22036 }, - { "hell05e.lmp", 10065 }, - { "hell06.lmp", 22609 }, - { "hell06a.lmp", 8241 }, - { "hell06b.lmp", 10302 }, - { "hell06c.lmp", 8056 }, - { "hell06d.lmp", 2877 }, - { "hell06e.lmp", 7147 }, - { "hell07.lmp", 36892 }, - { "hell07a.lmp", 17548 }, - { "hell07b.lmp", 28580 }, - { "hell07c.lmp", 32585 }, - { "hell07d.lmp", 13911 }, - { "hell07e.lmp", 12105 }, - { "hell08.lmp", 22399 }, - { "hell08a.lmp", 10072 }, - { "hell08b.lmp", 6364 }, - { "hell08c.lmp", 9463 }, - { "hell08d.lmp", 25955 }, - { "hell08e.lmp", 6529 }, - { "hell09.lmp", 120552 }, - { "hell09a.lmp", 42748 }, - { "hell09b.lmp", 42531 }, - { "hell09c.lmp", 34090 }, - { "hell09d.lmp", 39055 }, - { "hell09e.lmp", 41828 }, - { "hell09f.lmp", 31239 }, - { "hell10.lmp", 105532 }, - { "hell10a.lmp", 71620 }, - { "hell10b.lmp", 73741 }, - { "hell10c.lmp", 73427 }, - { "hell10d.lmp", 84123 }, - { "hell10e.lmp", 75177 }, - { "hell11.lmp", 154132 }, - { "hell11a.lmp", 39188 }, - { "hell11b.lmp", 960950 }, - { "hell11c.lmp", 297188 }, - { "hell11d.lmp", 311336 }, - { "hell11e.lmp", 45064 }, - { "hell12.lmp", 102711 }, - { "hell12a.lmp", 372802 }, - { "hell12b.lmp", 202404 }, - { "hell12c.lmp", 98709 }, - { "hell12d.lmp", 49665 }, - { "hell12e.lmp", 118405 }, - { "hell13.lmp", 89151 }, - { "hell13a.lmp", 8688 }, - { "hell13b.lmp", 79713 }, - { "hell13c.lmp", 82953 }, - { "hell13d.lmp", 58989 }, - { "hell13e.lmp", 156881 }, - { "hell14.lmp", 159494 }, - { "hell14a.lmp", 185994 }, - { "hell14b.lmp", 111381 }, - { "hell14c.lmp", 1156179 }, - { "hell14d.lmp", 1080797 }, - { "hell14e.lmp", 43268 }, - { "hell15.lmp", 186638 }, - { "hell15a.lmp", 277620 }, - { "hell15b.lmp", 16905 }, - { "hell15c.lmp", 110319 }, - { "hell15d.lmp", 24877 }, - { "hell15e.lmp", 39287 }, - { "hell16.lmp", 102356 }, - { "hell16a.lmp", 20283 }, - { "hell16b.lmp", 69443 }, - { "hell16c.lmp", 49121 }, - { "hell16d.lmp", 34319 }, - { "hell16e.lmp", 340840 }, - { "hell17.lmp", 102551 }, - { "hell17a.lmp", 47111 }, - { "hell17b.lmp", 62511 }, - { "hell17c.lmp", 696123 }, - { "hell17d.lmp", 65619 }, - { "hell17e.lmp", 54446 }, - { "hell18.lmp", 107665 }, - { "hell18a.lmp", 34227 }, - { "hell18b.lmp", 35552 }, - { "hell18c.lmp", 30816 }, - { "hell18d.lmp", 66616 }, - { "hell18e.lmp", 99723 }, - { "hell18f.lmp", 44282 }, - { "hell19.lmp", 46447 }, - { "hell19a.lmp", 13345 }, - { "hell19b.lmp", 6927 }, - { "hell19c.lmp", 13221 }, - { "hell19d.lmp", 19694 }, - { "hell19e.lmp", 27110 }, - { "hell20.lmp", 118897 }, - { "hell20a.lmp", 40697 }, - { "hell20b.lmp", 39985 }, - { "hell20c.lmp", 67615 }, - { "hell20d.lmp", 23685 }, - { "hell20e.lmp", 53263 }, - { "hell21.lmp", 90546 }, - { "hell21a.lmp", 30584 }, - { "hell21b.lmp", 139850 }, - { "hell21c.lmp", 58279 }, - { "hell21d.lmp", 47900 }, - { "hell21e.lmp", 57606 }, - { "hell22.lmp", 157265 }, - { "hell22a.lmp", 481602 }, - { "hell22b.lmp", 955291 }, - { "hell22c.lmp", 232845 }, - { "hell22d.lmp", 89909 }, - { "hell22e.lmp", 122584 }, - { "hell23.lmp", 21689 }, - { "hell23a.lmp", 9093 }, - { "hell23b.lmp", 33348 }, - { "hell23c.lmp", 27068 }, - { "hell23d.lmp", 25263 }, - { "hell23e.lmp", 28828 }, - { "hell24.lmp", 51865 }, - { "hell24a.lmp", 79994 }, - { "hell24b.lmp", 68952 }, - { "hell24c.lmp", 186384 }, - { "hell24d.lmp", 72132 }, - { "hell24e.lmp", 142110 }, - { "hell25.lmp", 50764 }, - { "hell25a.lmp", 89577 }, - { "hell25b.lmp", 28086 }, - { "hell25c.lmp", 23221 }, - { "hell25d.lmp", 33416 }, - { "hell25e.lmp", 13553 }, - { "hell26.lmp", 102672 }, - { "hell26a.lmp", 27154 }, - { "hell26b.lmp", 37682 }, - { "hell26c.lmp", 97908 }, - { "hell26d.lmp", 94776 }, - { "hell26e.lmp", 138670 }, - { "hell27.lmp", 17187 }, - { "hell27a.lmp", 3756 }, - { "hell27b.lmp", 3838 }, - { "hell27c.lmp", 4407 }, - { "hell27d.lmp", 2879 }, - { "hell27e.lmp", 6 }, - { "hell28.lmp", 25405 }, - { "hell28a.lmp", 11985 }, - { "hell28b.lmp", 12416 }, - { "hell28c.lmp", 11255 }, - { "hell28d.lmp", 11550 }, - { "hell28e.lmp", 14389 }, - { "hell29.lmp", 102673 }, - { "hell29a.lmp", 45392 }, - { "hell29b.lmp", 92088 }, - { "hell29c.lmp", 124389 }, - { "hell29d.lmp", 144983 }, - { "hell29e.lmp", 48684 }, - { "hell29f.lmp", 167923 }, - { "hellboss.lmp", 4459727 }, { "baphoexit.lmp", 4309647 }, - { "labyrinth.lmp", 397402 }, - { "labyrinth00.lmp", 119759 }, - { "labyrinth01.lmp", 32319 }, - { "labyrinth01a.lmp", 64189 }, - { "labyrinth01b.lmp", 65337 }, - { "labyrinth01c.lmp", 43746 }, - { "labyrinth01d.lmp", 26536 }, - { "labyrinth01e.lmp", 19636 }, - { "labyrinth02.lmp", 40567 }, - { "labyrinth02a.lmp", 420 }, - { "labyrinth02b.lmp", 38920 }, - { "labyrinth02c.lmp", 16008 }, - { "labyrinth02d.lmp", 24253 }, - { "labyrinth02e.lmp", 63921 }, - { "labyrinth02f.lmp", 126844 }, - { "labyrinth03.lmp", 149405 }, - { "labyrinth03a.lmp", 16472 }, - { "labyrinth03b.lmp", 111509 }, - { "labyrinth03c.lmp", 50370 }, - { "labyrinth03d.lmp", 236316 }, - { "labyrinth03e.lmp", 82737 }, - { "labyrinth03f.lmp", 191286 }, - { "labyrinth04.lmp", 109784 }, - { "labyrinth04a.lmp", 66827 }, - { "labyrinth04b.lmp", 80896 }, - { "labyrinth04c.lmp", 107797 }, - { "labyrinth04d.lmp", 89364 }, - { "labyrinth04e.lmp", 91803 }, - { "labyrinth04f.lmp", 67426 }, - { "labyrinth05.lmp", 110121 }, - { "labyrinth05a.lmp", 53007 }, - { "labyrinth05b.lmp", 47308 }, - { "labyrinth05c.lmp", 66118 }, - { "labyrinth05d.lmp", 53922 }, - { "labyrinth05e.lmp", 66820 }, - { "labyrinth06.lmp", 39633 }, - { "labyrinth06a.lmp", 16213 }, - { "labyrinth06b.lmp", 41773 }, - { "labyrinth06c.lmp", 52793 }, - { "labyrinth06d.lmp", 33467 }, - { "labyrinth06e.lmp", 35252 }, - { "labyrinth07.lmp", 39633 }, - { "labyrinth07a.lmp", 16244 }, - { "labyrinth07b.lmp", 64137 }, - { "labyrinth07c.lmp", 43007 }, - { "labyrinth07d.lmp", 49006 }, - { "labyrinth07e.lmp", 48286 }, - { "labyrinth08.lmp", 91975 }, - { "labyrinth08a.lmp", 126344 }, - { "labyrinth08b.lmp", 261965 }, - { "labyrinth08c.lmp", 116805 }, - { "labyrinth08d.lmp", 93423 }, - { "labyrinth08e.lmp", 166877 }, - { "labyrinth09.lmp", 70907 }, - { "labyrinth09a.lmp", 42123 }, - { "labyrinth09b.lmp", 123680 }, - { "labyrinth09c.lmp", 158047 }, - { "labyrinth09d.lmp", 41246 }, - { "labyrinth09e.lmp", 53113 }, - { "labyrinth10.lmp", 70911 }, - { "labyrinth10a.lmp", 37919 }, - { "labyrinth10b.lmp", 52942 }, - { "labyrinth10c.lmp", 104651 }, - { "labyrinth10d.lmp", 41875 }, - { "labyrinth10e.lmp", 77554 }, - { "labyrinth11.lmp", 102011 }, - { "labyrinth11a.lmp", 124995 }, - { "labyrinth11b.lmp", 44855 }, - { "labyrinth11c.lmp", 121035 }, - { "labyrinth11d.lmp", 245955 }, - { "labyrinth11e.lmp", 92172 }, - { "labyrinth12.lmp", 108662 }, - { "labyrinth12a.lmp", 248043 }, - { "labyrinth12b.lmp", 75322 }, - { "labyrinth12c.lmp", 251882 }, - { "labyrinth12d.lmp", 285746 }, - { "labyrinth12e.lmp", 93600 }, - { "labyrinth13.lmp", 42829 }, - { "labyrinth13a.lmp", 30982 }, - { "labyrinth13b.lmp", 21962 }, - { "labyrinth13c.lmp", 31714 }, - { "labyrinth13d.lmp", 9987 }, - { "labyrinth13e.lmp", 20182 }, - { "labyrinth13f.lmp", 12578 }, - { "labyrinth14.lmp", 67079 }, - { "labyrinth14a.lmp", 129808 }, - { "labyrinth14b.lmp", 45415 }, - { "labyrinth14c.lmp", 44746 }, - { "labyrinth14d.lmp", 100542 }, - { "labyrinth14e.lmp", 31548 }, - { "labyrinth15.lmp", 287900 }, - { "labyrinth15a.lmp", 22984 }, - { "labyrinth15b.lmp", 104822 }, - { "labyrinth15c.lmp", 114256 }, - { "labyrinth15d.lmp", 104887 }, - { "labyrinth15e.lmp", 64426 }, - { "labyrinth16.lmp", 92449 }, - { "labyrinth16a.lmp", 12211 }, - { "labyrinth16b.lmp", 62760 }, - { "labyrinth16c.lmp", 77205 }, - { "labyrinth16d.lmp", 39180 }, - { "labyrinth16e.lmp", 30724 }, - { "labyrinth17.lmp", 358170 }, - { "labyrinth17a.lmp", 27749 }, - { "labyrinth17b.lmp", 43026 }, - { "labyrinth17c.lmp", 33891 }, - { "labyrinth17d.lmp", 70771 }, - { "labyrinth17e.lmp", 123853 }, - { "labyrinth18.lmp", 142769 }, - { "labyrinth18a.lmp", 108928 }, - { "labyrinth18b.lmp", 103153 }, - { "labyrinth18c.lmp", 69372 }, - { "labyrinth18d.lmp", 68465 }, - { "labyrinth18e.lmp", 99163 }, - { "labyrinth19.lmp", 56735 }, - { "labyrinth19a.lmp", 3275 }, - { "labyrinth19b.lmp", 15120 }, - { "labyrinth19c.lmp", 37342 }, - { "labyrinth19d.lmp", 30150 }, - { "labyrinth19e.lmp", 63796 }, - { "labyrinth20.lmp", 68928 }, - { "labyrinth20a.lmp", 9243 }, - { "labyrinth20b.lmp", 68327 }, - { "labyrinth20c.lmp", 59151 }, - { "labyrinth20d.lmp", 42530 }, - { "labyrinth20e.lmp", 76531 }, - { "labyrinth21.lmp", 53319 }, - { "labyrinth21a.lmp", 3161 }, - { "labyrinth21b.lmp", 38170 }, - { "labyrinth21c.lmp", 12316 }, - { "labyrinth21d.lmp", 25530 }, - { "labyrinth21e.lmp", 60942 }, - { "labyrinth22.lmp", 35351 }, - { "labyrinth22a.lmp", 10229 }, - { "labyrinth22b.lmp", 12801 }, - { "labyrinth22c.lmp", 12265 }, - { "labyrinth22d.lmp", 10832 }, - { "labyrinth22e.lmp", 15763 }, - { "labyrinth23.lmp", 84812 }, - { "labyrinth23a.lmp", 60989 }, - { "labyrinth23b.lmp", 46318 }, - { "labyrinth23c.lmp", 38896 }, - { "labyrinth23d.lmp", 233512 }, - { "labyrinth23e.lmp", 81611 }, - { "labyrinth24.lmp", 55073 }, - { "labyrinth24a.lmp", 188853 }, - { "labyrinth24b.lmp", 74991 }, - { "labyrinth24c.lmp", 39266 }, - { "labyrinth24d.lmp", 17045 }, - { "labyrinth24e.lmp", 23087 }, - { "labyrinth25.lmp", 60492 }, - { "labyrinth25a.lmp", 11150 }, - { "labyrinth25b.lmp", 6878 }, - { "labyrinth25c.lmp", 11057 }, - { "labyrinth25d.lmp", 7201 }, - { "labyrinth25e.lmp", 4705 }, - { "labyrinth26.lmp", 10430 }, - { "labyrinth26a.lmp", 4453 }, - { "labyrinth26b.lmp", 5457 }, - { "labyrinth26c.lmp", 8932 }, - { "labyrinth26d.lmp", 3908 }, - { "labyrinth26e.lmp", 4013 }, - { "labyrinth27.lmp", 10433 }, - { "labyrinth27a.lmp", 4453 }, - { "labyrinth27b.lmp", 8752 }, - { "labyrinth27c.lmp", 4702 }, - { "labyrinth27d.lmp", 3343 }, - { "labyrinth27e.lmp", 4163 }, - { "labyrinth28.lmp", 20971 }, - { "labyrinth28a.lmp", 5511 }, - { "labyrinth28b.lmp", 4837 }, - { "labyrinth28c.lmp", 8471 }, - { "labyrinth28d.lmp", 5907 }, - { "labyrinth28e.lmp", 5629 }, - { "labyrinth29.lmp", 20516 }, - { "labyrinth29a.lmp", 2709 }, - { "labyrinth29b.lmp", 14281 }, - { "labyrinth29c.lmp", 12255 }, - { "labyrinth29d.lmp", 13714 }, - { "labyrinth29e.lmp", 27632 }, - { "labyrinth30.lmp", 67775 }, - { "labyrinth30a.lmp", 250787 }, - { "labyrinth30b.lmp", 76890 }, - { "labyrinth30c.lmp", 42344 }, - { "labyrinth30d.lmp", 122312 }, - { "labyrinth30e.lmp", 32283 }, - { "labyrinth31.lmp", 65567 }, - { "labyrinth31a.lmp", 18532 }, - { "labyrinth31b.lmp", 26136 }, - { "labyrinth31c.lmp", 44440 }, - { "labyrinth31d.lmp", 23865 }, - { "labyrinth31e.lmp", 21255 }, - { "labyrinth32.lmp", 49841 }, - { "labyrinth32a.lmp", 30907 }, - { "labyrinth32b.lmp", 35643 }, - { "labyrinth32c.lmp", 25911 }, - { "labyrinth32d.lmp", 5279 }, - { "labyrinth32e.lmp", 26968 }, - { "labyrinthsecret.lmp", 73135 }, - { "labyrinthtoruins.lmp", 137530 }, - { "mainmenu1.lmp", -1 }, - { "mainmenu2.lmp", -1 }, - { "mainmenu3.lmp", -1 }, - { "mainmenu4.lmp", -1 }, - { "mainmenu5.lmp", -1 }, - { "mainmenu6.lmp", -1 }, - { "mainmenu7.lmp", -1 }, - { "mainmenu8.lmp", -1 }, - { "mainmenu9.lmp", -1 }, - { "mine.lmp", 80741 }, - { "mine00.lmp", 12890 }, - { "mine01.lmp", 52889 }, - { "mine01a.lmp", 4094 }, - { "mine01b.lmp", 5156 }, - { "mine01c.lmp", 5524 }, - { "mine01d.lmp", 2780 }, - { "mine01e.lmp", 33508 }, - { "mine01f.lmp", 2436 }, - { "mine01g.lmp", 15844 }, - { "mine02.lmp", 16102 }, - { "mine02a.lmp", 1964 }, - { "mine02b.lmp", 2050 }, - { "mine02c.lmp", 3796 }, - { "mine02d.lmp", 19759 }, - { "mine02e.lmp", 2052 }, - { "mine02f.lmp", 375 }, - { "mine02g.lmp", 862 }, - { "mine03.lmp", 17657 }, - { "mine03a.lmp", 3440 }, - { "mine03b.lmp", 49719 }, - { "mine03c.lmp", 3274 }, - { "mine03d.lmp", 1451 }, - { "mine03e.lmp", 1516 }, - { "mine03f.lmp", 6794 }, - { "mine03g.lmp", 1331 }, - { "mine04.lmp", 21711 }, - { "mine04a.lmp", 2264 }, - { "mine04b.lmp", 6415 }, - { "mine04c.lmp", 4200 }, - { "mine04d.lmp", 2925 }, - { "mine04e.lmp", 914 }, - { "mine04f.lmp", 1042 }, - { "mine04g.lmp", 2415 }, - { "mine05.lmp", 33835 }, - { "mine05a.lmp", 4057 }, - { "mine05b.lmp", 8869 }, - { "mine05c.lmp", 3943 }, - { "mine05d.lmp", 6466 }, - { "mine05e.lmp", 3486 }, - { "mine06.lmp", 79441 }, - { "mine06a.lmp", 12895 }, - { "mine06b.lmp", 58604 }, - { "mine06c.lmp", 28704 }, - { "mine06d.lmp", 81643 }, - { "mine06e.lmp", 25051 }, - { "mine07.lmp", 107931 }, - { "mine07a.lmp", 13341 }, - { "mine07b.lmp", 24443 }, - { "mine07c.lmp", 49306 }, - { "mine07d.lmp", 17195 }, - { "mine07e.lmp", 13843 }, - { "mine08.lmp", 50089 }, - { "mine08a.lmp", 2824 }, - { "mine08b.lmp", 6387 }, - { "mine08c.lmp", 3666 }, - { "mine08d.lmp", 27312 }, - { "mine08e.lmp", 1614 }, - { "mine09.lmp", 57257 }, - { "mine09a.lmp", 10581 }, - { "mine09b.lmp", 10445 }, - { "mine09c.lmp", 46588 }, - { "mine09d.lmp", 30484 }, - { "mine09e.lmp", 10147 }, - { "mine10.lmp", 117885 }, - { "mine10a.lmp", 178759 }, - { "mine10b.lmp", 70918 }, - { "mine10c.lmp", 21147 }, - { "mine10d.lmp", 65233 }, - { "mine10e.lmp", 61077 }, - { "mine11.lmp", 74234 }, - { "mine11a.lmp", 7475 }, - { "mine11b.lmp", 7909 }, - { "mine11c.lmp", 4787 }, - { "mine11d.lmp", 18375 }, - { "mine11e.lmp", 55599 }, - { "mine12.lmp", 30195 }, - { "mine12a.lmp", 2408 }, - { "mine12b.lmp", 1064 }, - { "mine12c.lmp", 1064 }, - { "mine12d.lmp", 1064 }, - { "mine12e.lmp", 1066 }, - { "mine12f.lmp", 1123 }, - { "mine12g.lmp", 3173 }, - { "mine12h.lmp", 51714 }, - { "mine12i.lmp", 29186 }, - { "mine13.lmp", 43438 }, - { "mine13a.lmp", 6070 }, - { "mine13b.lmp", 6864 }, - { "mine13c.lmp", 94568 }, - { "mine13d.lmp", 14907 }, - { "mine13e.lmp", 14220 }, - { "mine14.lmp", 54278 }, - { "mine14a.lmp", 8371 }, - { "mine14b.lmp", 10552 }, - { "mine14c.lmp", 32460 }, - { "mine14d.lmp", 12894 }, - { "mine14e.lmp", 6706 }, - { "mine15.lmp", 44160 }, - { "mine15a.lmp", 14464 }, - { "mine15b.lmp", 16268 }, - { "mine15c.lmp", 15070 }, - { "mine15d.lmp", 11959 }, - { "mine15e.lmp", 6270 }, - { "mine16.lmp", 47683 }, - { "mine16a.lmp", 5567 }, - { "mine16b.lmp", 4516 }, - { "mine16c.lmp", 11200 }, - { "mine16d.lmp", 15776 }, - { "mine16e.lmp", 22921 }, - { "mine16f.lmp", 34277 }, - { "mine17.lmp", 18741 }, - { "mine17a.lmp", 4756 }, - { "mine17b.lmp", 6251 }, - { "mine17c.lmp", 6627 }, - { "mine17d.lmp", 5883 }, - { "mine17e.lmp", 7393 }, - { "mine17f.lmp", 7827 }, - { "mine18.lmp", 48006 }, - { "mine18a.lmp", 10791 }, - { "mine18b.lmp", 11978 }, - { "mine18c.lmp", 26618 }, - { "mine18d.lmp", 32406 }, - { "mine18e.lmp", 11426 }, - { "mine19.lmp", 21099 }, - { "mine19a.lmp", 2014 }, - { "mine19b.lmp", 2706 }, - { "mine19c.lmp", 2448 }, - { "mine19d.lmp", 2500 }, - { "mine19e.lmp", 4188 }, - { "mine20.lmp", 16836 }, - { "mine20a.lmp", 968 }, - { "mine20b.lmp", 1473 }, - { "mine20c.lmp", 2087 }, - { "mine20d.lmp", 1077 }, - { "mine20e.lmp", 10418 }, - { "mine21.lmp", 26317 }, - { "mine21a.lmp", 4635 }, - { "mine21b.lmp", 4623 }, - { "mine21c.lmp", 4872 }, - { "mine21d.lmp", 5830 }, - { "mine21e.lmp", 5158 }, - { "mine22.lmp", 20095 }, - { "mine22a.lmp", 937 }, - { "mine22b.lmp", 1229 }, - { "mine22c.lmp", 1229 }, - { "mine22d.lmp", 1353 }, - { "mine22e.lmp", 2587 }, - { "mine22f.lmp", 28494 }, - { "mine23.lmp", 20477 }, - { "mine23a.lmp", 2453 }, - { "mine23b.lmp", 2528 }, - { "mine23c.lmp", 3226 }, - { "mine23d.lmp", 1998 }, - { "mine23e.lmp", 14561 }, - { "mine23f.lmp", 3490 }, - { "mine24.lmp", 18353 }, - { "mine24a.lmp", 2206 }, - { "mine24b.lmp", 2573 }, - { "mine24c.lmp", 1544 }, - { "mine24d.lmp", 868 }, - { "mine24e.lmp", 1541 }, - { "mine25.lmp", 11992 }, - { "mine25a.lmp", 1512 }, - { "mine25b.lmp", 1099 }, - { "mine25c.lmp", 10042 }, - { "mine25d.lmp", 475 }, - { "mine25e.lmp", 945 }, - { "mine26.lmp", 6642 }, - { "mine26a.lmp", 646 }, - { "mine26b.lmp", 568 }, - { "mine26c.lmp", 650 }, - { "mine26d.lmp", 729 }, - { "mine26e.lmp", 1912 }, - { "mine27.lmp", 6783 }, - { "mine27a.lmp", 647 }, - { "mine27b.lmp", 646 }, - { "mine27c.lmp", 719 }, - { "mine27d.lmp", 727 }, - { "mine27e.lmp", 2659 }, - { "mine28.lmp", 12641 }, - { "mine28a.lmp", 1867 }, - { "mine28b.lmp", 1981 }, - { "mine28c.lmp", 2279 }, - { "mine28d.lmp", 2763 }, - { "mine28e.lmp", 2148 }, - { "mine29.lmp", 7173 }, - { "mine29a.lmp", 631 }, - { "mine29b.lmp", 663 }, - { "mine29c.lmp", 853 }, - { "mine29d.lmp", 787 }, - { "mine29e.lmp", 777 }, - { "mine30.lmp", 56963 }, - { "mine30a.lmp", 24107 }, - { "mine30b.lmp", 40738 }, - { "mine30c.lmp", 21350 }, - { "mine30d.lmp", 9993 }, - { "mine30e.lmp", 9790 }, - { "mine31.lmp", 38812 }, - { "mine31a.lmp", 13079 }, - { "mine31b.lmp", 14258 }, - { "mine31c.lmp", 13828 }, - { "mine31d.lmp", 6625 }, - { "mine31e.lmp", 8149 }, - { "mine32.lmp", 7983 }, - { "mine32a.lmp", 2775 }, - { "mine32b.lmp", 2944 }, - { "mine32c.lmp", 1755 }, - { "mine32d.lmp", 266 }, - { "mine32e.lmp", 1916 }, - { "mine33.lmp", 12331 }, - { "mine33a.lmp", 63 }, - { "mine33b.lmp", 366 }, - { "mine33c.lmp", 564 }, - { "mine33d.lmp", 848 }, - { "mine33e.lmp", 298 }, - { "mine33f.lmp", 258 }, - { "mine33g.lmp", 2972 }, - { "minesecret.lmp", 25068 }, - { "minetoswamp.lmp", 132973 }, - { "minetown.lmp", 842756 }, - { "minotaur.lmp", 4409895 }, - { "mysticlibrary.lmp", 360623 }, - { "ruins.lmp", 92624 }, - { "ruins00.lmp", 6373 }, + { "boss.lmp", 2376307 }, + { "bramscastle.lmp", 2944609 }, + { "caves.lmp", 777612 }, + { "caves00.lmp", 70935 }, + { "caves01.lmp", 13350 }, + { "caves01a.lmp", 5 }, + { "caves01b.lmp", 670 }, + { "caves01c.lmp", 2032 }, + { "caves01d.lmp", 709 }, + { "caves01e.lmp", 5 }, + { "caves01f.lmp", 955 }, + { "caves02.lmp", 8895 }, + { "caves02a.lmp", 3534 }, + { "caves02b.lmp", 3687 }, + { "caves02c.lmp", 1471 }, + { "caves02d.lmp", 2041 }, + { "caves02e.lmp", 3511 }, + { "caves03.lmp", 15723 }, + { "caves03a.lmp", 7023 }, + { "caves03b.lmp", 4916 }, + { "caves03c.lmp", 3309 }, + { "caves03d.lmp", 4233 }, + { "caves03e.lmp", 6928 }, + { "caves04.lmp", 19261 }, + { "caves04a.lmp", 11118 }, + { "caves04b.lmp", 4680 }, + { "caves04c.lmp", 6919 }, + { "caves04d.lmp", 3992 }, + { "caves04e.lmp", 11690 }, + { "caves05.lmp", 14060 }, + { "caves05a.lmp", 4825 }, + { "caves05b.lmp", 2998 }, + { "caves05c.lmp", 979 }, + { "caves05d.lmp", 1357 }, + { "caves05e.lmp", 2093 }, + { "caves06.lmp", 12636 }, + { "caves06a.lmp", 3451 }, + { "caves06b.lmp", 3100 }, + { "caves06c.lmp", 4876 }, + { "caves06d.lmp", 580 }, + { "caves06e.lmp", 3467 }, + { "caves07.lmp", 10099 }, + { "caves07a.lmp", 1952 }, + { "caves07b.lmp", 2021 }, + { "caves07c.lmp", 15547 }, + { "caves07d.lmp", 1394 }, + { "caves07e.lmp", 1868 }, + { "caves08.lmp", 10108 }, + { "caves08a.lmp", 2276 }, + { "caves08b.lmp", 1403 }, + { "caves08c.lmp", 2714 }, + { "caves08d.lmp", 3575 }, + { "caves08e.lmp", 7877 }, + { "caves09.lmp", 98747 }, + { "caves09a.lmp", 2836 }, + { "caves09b.lmp", 6610 }, + { "caves09c.lmp", 4989 }, + { "caves09d.lmp", 6168 }, + { "caves09e.lmp", 4466 }, + { "caves10.lmp", 48413 }, + { "caves10a.lmp", 8958 }, + { "caves10b.lmp", 14424 }, + { "caves10c.lmp", 15504 }, + { "caves10d.lmp", 23644 }, + { "caves10e.lmp", 13489 }, + { "caves11.lmp", 42506 }, + { "caves11a.lmp", 17838 }, + { "caves11b.lmp", 28178 }, + { "caves11c.lmp", 16684 }, + { "caves11d.lmp", 12527 }, + { "caves11e.lmp", 4818 }, + { "caves12.lmp", 61628 }, + { "caves12a.lmp", 17274 }, + { "caves12b.lmp", 103367 }, + { "caves12c.lmp", 38298 }, + { "caves12d.lmp", 11358 }, + { "caves12e.lmp", 27401 }, + { "caves13.lmp", 45478 }, + { "caves13a.lmp", 1855 }, + { "caves13b.lmp", 733 }, + { "caves13c.lmp", 1597 }, + { "caves13d.lmp", 3386 }, + { "caves13e.lmp", 2892 }, + { "caves14.lmp", 54322 }, + { "caves14a.lmp", 11869 }, + { "caves14b.lmp", 16048 }, + { "caves14c.lmp", 17928 }, + { "caves14d.lmp", 32882 }, + { "caves14e.lmp", 15132 }, + { "caves15.lmp", 38306 }, + { "caves15a.lmp", 12764 }, + { "caves15b.lmp", 9856 }, + { "caves15c.lmp", 13672 }, + { "caves15d.lmp", 9456 }, + { "caves15e.lmp", 18504 }, + { "caves16.lmp", 161684 }, + { "caves16a.lmp", 83942 }, + { "caves16b.lmp", 677034 }, + { "caves16c.lmp", 342654 }, + { "caves16d.lmp", 63219 }, + { "caves16e.lmp", 457447 }, + { "caves17.lmp", 58218 }, + { "caves17a.lmp", 12215 }, + { "caves17b.lmp", 29089 }, + { "caves17c.lmp", 13005 }, + { "caves17d.lmp", 21989 }, + { "caves17e.lmp", 36232 }, + { "caves18.lmp", 60668 }, + { "caves18a.lmp", 14858 }, + { "caves18b.lmp", 12622 }, + { "caves18c.lmp", 19374 }, + { "caves18d.lmp", 23105 }, + { "caves18e.lmp", 43279 }, + { "caves19.lmp", 39612 }, + { "caves19a.lmp", 14698 }, + { "caves19b.lmp", 34128 }, + { "caves19c.lmp", 45807 }, + { "caves19d.lmp", 16936 }, + { "caves19e.lmp", 99311 }, + { "caves20.lmp", 44471 }, + { "caves20a.lmp", 30426 }, + { "caves20b.lmp", 26914 }, + { "caves20c.lmp", 5701 }, + { "caves20d.lmp", 4510 }, + { "caves20e.lmp", 9019 }, + { "caves21.lmp", 25125 }, + { "caves21a.lmp", 4546 }, + { "caves21b.lmp", 10161 }, + { "caves21c.lmp", 9520 }, + { "caves21d.lmp", 5183 }, + { "caves21e.lmp", 9142 }, + { "caves22.lmp", 40155 }, + { "caves22a.lmp", 15014 }, + { "caves22b.lmp", 22535 }, + { "caves22c.lmp", 6148 }, + { "caves22d.lmp", 18672 }, + { "caves22e.lmp", 7577 }, + { "caves23.lmp", 42450 }, + { "caves23a.lmp", 28039 }, + { "caves23b.lmp", 46987 }, + { "caves23c.lmp", 123314 }, + { "caves23d.lmp", 28040 }, + { "caves23e.lmp", 59980 }, + { "caves24.lmp", 82126 }, + { "caves24a.lmp", 281444 }, + { "caves24b.lmp", 177105 }, + { "caves24c.lmp", 184456 }, + { "caves24d.lmp", 61361 }, + { "caves24e.lmp", 184273 }, + { "caves25.lmp", 32385 }, + { "caves25a.lmp", 3299 }, + { "caves25b.lmp", 16548 }, + { "caves25c.lmp", 7550 }, + { "caves25d.lmp", 8913 }, + { "caves25e.lmp", 6349 }, + { "caves26.lmp", 62762 }, + { "caves26a.lmp", 19832 }, + { "caves26b.lmp", 19226 }, + { "caves26c.lmp", 227249 }, + { "caves26d.lmp", 26573 }, + { "caves26e.lmp", 116762 }, + { "caves27.lmp", 38367 }, + { "caves27a.lmp", 9511 }, + { "caves27b.lmp", 12825 }, + { "caves27c.lmp", 19213 }, + { "caves27d.lmp", 12319 }, + { "caves27e.lmp", 13212 }, + { "caves28.lmp", 37880 }, + { "caves28a.lmp", 3451 }, + { "caves28b.lmp", 6985 }, + { "caves28c.lmp", 8995 }, + { "caves28d.lmp", 15744 }, + { "caves28e.lmp", 14598 }, + { "caves29.lmp", 55588 }, + { "caves29a.lmp", 48870 }, + { "caves29b.lmp", 38387 }, + { "caves29c.lmp", 58475 }, + { "caves29d.lmp", 24429 }, + { "caves29e.lmp", 42098 }, + { "caves30.lmp", 95255 }, + { "caves30a.lmp", 97061 }, + { "caves30b.lmp", 120983 }, + { "caves30c.lmp", 26725 }, + { "caves30d.lmp", 39825 }, + { "caves30e.lmp", 137162 }, + { "caves_lockb00.lmp", 50347 }, + { "caves_lockb00a.lmp", 758 }, + { "caves_lockb00b.lmp", 2047 }, + { "caves_lockb00c.lmp", 2047 }, + { "caves_lockb00d.lmp", 1281 }, + { "caves_lockb00e.lmp", 638 }, + { "caves_lockb01.lmp", 46872 }, + { "caves_lockb01a.lmp", 3270 }, + { "caves_lockb01b.lmp", 2157 }, + { "caves_lockb01c.lmp", 2528 }, + { "caves_lockb01d.lmp", 2528 }, + { "caves_lockb01e.lmp", 2528 }, + { "caves_lockg00.lmp", 142127 }, + { "caves_lockg00a.lmp", 74876 }, + { "caves_lockg00b.lmp", 53369 }, + { "caves_lockg00c.lmp", 60884 }, + { "caves_lockg00d.lmp", 53369 }, + { "caves_lockg00e.lmp", 53369 }, + { "caves_lockg01.lmp", 82884 }, + { "caves_lockg01a.lmp", 3333 }, + { "caves_lockg01b.lmp", 4979 }, + { "caves_lockg01c.lmp", 2843 }, + { "caves_lockg01d.lmp", 4979 }, + { "caves_lockg01e.lmp", 4979 }, + { "caves_locks00.lmp", 58373 }, + { "caves_locks00a.lmp", 1065 }, + { "caves_locks00b.lmp", 1283 }, + { "caves_locks00c.lmp", 3664 }, + { "caves_locks00d.lmp", 1954 }, + { "caves_locks00e.lmp", 1283 }, + { "caves_locks01.lmp", 88386 }, + { "caves_locks01a.lmp", 54280 }, + { "caves_locks01b.lmp", 54280 }, + { "caves_locks01c.lmp", 35511 }, + { "caves_locks01d.lmp", 67993 }, + { "caves_locks01e.lmp", 54280 }, + { "caveslair.lmp", 4603251 }, + { "cavessecret.lmp", 198959 }, + { "cavestocitadel.lmp", 276221 }, + { "citadel.lmp", 729069 }, + { "citadel00.lmp", 30887 }, + { "citadel01.lmp", 28663 }, + { "citadel01a.lmp", 2025 }, + { "citadel01b.lmp", 2224 }, + { "citadel01c.lmp", 2144 }, + { "citadel01d.lmp", 2377 }, + { "citadel01e.lmp", 645 }, + { "citadel01f.lmp", 267 }, + { "citadel01g.lmp", 6 }, + { "citadel02.lmp", 26869 }, + { "citadel02a.lmp", 222 }, + { "citadel02b.lmp", 222 }, + { "citadel02c.lmp", 222 }, + { "citadel02d.lmp", 222 }, + { "citadel02e.lmp", 222 }, + { "citadel02f.lmp", 222 }, + { "citadel02g.lmp", 222 }, + { "citadel03.lmp", 25004 }, + { "citadel03a.lmp", 20366 }, + { "citadel03b.lmp", 5705 }, + { "citadel03c.lmp", 8284 }, + { "citadel03d.lmp", 10044 }, + { "citadel03e.lmp", 6650 }, + { "citadel04.lmp", 25142 }, + { "citadel04a.lmp", 26797 }, + { "citadel04b.lmp", 12152 }, + { "citadel04c.lmp", 13801 }, + { "citadel04d.lmp", 12540 }, + { "citadel04e.lmp", 12358 }, + { "citadel05.lmp", 25303 }, + { "citadel05a.lmp", 29885 }, + { "citadel05b.lmp", 11557 }, + { "citadel05c.lmp", 12380 }, + { "citadel05d.lmp", 9701 }, + { "citadel05e.lmp", 12135 }, + { "citadel06.lmp", 24894 }, + { "citadel06a.lmp", 19673 }, + { "citadel06b.lmp", 11832 }, + { "citadel06c.lmp", 9337 }, + { "citadel06d.lmp", 11925 }, + { "citadel06e.lmp", 10981 }, + { "citadel07.lmp", 27616 }, + { "citadel07a.lmp", 24336 }, + { "citadel07b.lmp", 9201 }, + { "citadel07c.lmp", 13009 }, + { "citadel07d.lmp", 13903 }, + { "citadel07e.lmp", 11919 }, + { "citadel08.lmp", 27638 }, + { "citadel08a.lmp", 17486 }, + { "citadel08b.lmp", 9104 }, + { "citadel08c.lmp", 12105 }, + { "citadel08d.lmp", 11545 }, + { "citadel08e.lmp", 10684 }, + { "citadel09.lmp", 30974 }, + { "citadel09a.lmp", 13607 }, + { "citadel09b.lmp", 12649 }, + { "citadel09c.lmp", 14901 }, + { "citadel09d.lmp", 10477 }, + { "citadel09e.lmp", 13432 }, + { "citadel10.lmp", 30974 }, + { "citadel10a.lmp", 13615 }, + { "citadel10b.lmp", 15638 }, + { "citadel10c.lmp", 11770 }, + { "citadel10d.lmp", 12573 }, + { "citadel10e.lmp", 11709 }, + { "citadel11.lmp", 30981 }, + { "citadel11a.lmp", 11596 }, + { "citadel11b.lmp", 13165 }, + { "citadel11c.lmp", 15325 }, + { "citadel11d.lmp", 10151 }, + { "citadel11e.lmp", 11678 }, + { "citadel12.lmp", 30981 }, + { "citadel12a.lmp", 13627 }, + { "citadel12b.lmp", 11940 }, + { "citadel12c.lmp", 15322 }, + { "citadel12d.lmp", 15133 }, + { "citadel12e.lmp", 13684 }, + { "citadel13.lmp", 61060 }, + { "citadel13a.lmp", 52281 }, + { "citadel13b.lmp", 27828 }, + { "citadel13c.lmp", 25095 }, + { "citadel13d.lmp", 26551 }, + { "citadel13e.lmp", 30997 }, + { "citadel14.lmp", 121487 }, + { "citadel14a.lmp", 320209 }, + { "citadel14b.lmp", 60537 }, + { "citadel14c.lmp", 103428 }, + { "citadel14d.lmp", 100396 }, + { "citadel14e.lmp", 66475 }, + { "citadel15.lmp", 61045 }, + { "citadel15a.lmp", 31926 }, + { "citadel15b.lmp", 32523 }, + { "citadel15c.lmp", 26121 }, + { "citadel15d.lmp", 19366 }, + { "citadel15e.lmp", 7983 }, + { "citadel16.lmp", 56478 }, + { "citadel16a.lmp", 22937 }, + { "citadel16b.lmp", 30127 }, + { "citadel16c.lmp", 21344 }, + { "citadel16d.lmp", 19459 }, + { "citadel16e.lmp", 18046 }, + { "citadel17.lmp", 145462 }, + { "citadel17a.lmp", 8479 }, + { "citadel17b.lmp", 2019 }, + { "citadel17c.lmp", 7624 }, + { "citadel17d.lmp", 12917 }, + { "citadel17e.lmp", 34697 }, + { "citadel18.lmp", 20465 }, + { "citadel18a.lmp", 19558 }, + { "citadel18b.lmp", 15707 }, + { "citadel18c.lmp", 15337 }, + { "citadel18d.lmp", 38515 }, + { "citadel18e.lmp", 9150 }, + { "citadel18f.lmp", 9150 }, + { "citadel18g.lmp", 6530 }, + { "citadel18h.lmp", 13549 }, + { "citadel19.lmp", 87870 }, + { "citadel19a.lmp", 14140 }, + { "citadel19b.lmp", 26659 }, + { "citadel19c.lmp", 35660 }, + { "citadel19d.lmp", 29389 }, + { "citadel19e.lmp", 93118 }, + { "citadel20.lmp", 88689 }, + { "citadel20a.lmp", 14070 }, + { "citadel20b.lmp", 26064 }, + { "citadel20c.lmp", 52017 }, + { "citadel20d.lmp", 14975 }, + { "citadel20e.lmp", 25922 }, + { "citadel21.lmp", 97988 }, + { "citadel21a.lmp", 74930 }, + { "citadel21b.lmp", 61362 }, + { "citadel21c.lmp", 46887 }, + { "citadel21d.lmp", 97893 }, + { "citadel21e.lmp", 29411 }, + { "citadel22.lmp", 100038 }, + { "citadel22a.lmp", 42340 }, + { "citadel22b.lmp", 110372 }, + { "citadel22c.lmp", 33272 }, + { "citadel22d.lmp", 49546 }, + { "citadel22e.lmp", 45982 }, + { "citadel23.lmp", 97898 }, + { "citadel23a.lmp", 98092 }, + { "citadel23b.lmp", 46407 }, + { "citadel23c.lmp", 37996 }, + { "citadel23d.lmp", 124925 }, + { "citadel23e.lmp", 49170 }, + { "citadel24.lmp", 117952 }, + { "citadel24a.lmp", 67663 }, + { "citadel24b.lmp", 62924 }, + { "citadel24c.lmp", 62105 }, + { "citadel24d.lmp", 67413 }, + { "citadel24e.lmp", 71432 }, + { "citadel25.lmp", 115900 }, + { "citadel25a.lmp", 71955 }, + { "citadel25b.lmp", 45706 }, + { "citadel25c.lmp", 35265 }, + { "citadel25d.lmp", 44434 }, + { "citadel25e.lmp", 54799 }, + { "citadel26.lmp", 113611 }, + { "citadel26a.lmp", 62243 }, + { "citadel26b.lmp", 54638 }, + { "citadel26c.lmp", 97841 }, + { "citadel26d.lmp", 66569 }, + { "citadel26e.lmp", 91139 }, + { "citadel27.lmp", 112647 }, + { "citadel27a.lmp", 73940 }, + { "citadel27b.lmp", 71029 }, + { "citadel27c.lmp", 80975 }, + { "citadel27d.lmp", 91447 }, + { "citadel27e.lmp", 103281 }, + { "citadel28.lmp", 114478 }, + { "citadel28a.lmp", 59779 }, + { "citadel28b.lmp", 63758 }, + { "citadel28c.lmp", 42327 }, + { "citadel28d.lmp", 77504 }, + { "citadel28e.lmp", 62934 }, + { "citadel29.lmp", 109837 }, + { "citadel29a.lmp", 73513 }, + { "citadel29b.lmp", 53418 }, + { "citadel29c.lmp", 44164 }, + { "citadel29d.lmp", 65090 }, + { "citadel29e.lmp", 78366 }, + { "citadel30.lmp", 176987 }, + { "citadel30a.lmp", 20556 }, + { "citadel30b.lmp", 22488 }, + { "citadel30c.lmp", 25141 }, + { "citadel30d.lmp", 21869 }, + { "citadel30e.lmp", 25471 }, + { "citadel_lockb00.lmp", 34341 }, + { "citadel_lockb00a.lmp", 4979 }, + { "citadel_lockb00b.lmp", 5474 }, + { "citadel_lockb00c.lmp", 5474 }, + { "citadel_lockb00d.lmp", 5474 }, + { "citadel_lockb00e.lmp", 5474 }, + { "citadel_lockb01.lmp", 98500 }, + { "citadel_lockb01a.lmp", 36636 }, + { "citadel_lockb01b.lmp", 36636 }, + { "citadel_lockb01c.lmp", 36636 }, + { "citadel_lockb01d.lmp", 36636 }, + { "citadel_lockb01e.lmp", 36636 }, + { "citadel_lockg00.lmp", 242321 }, + { "citadel_lockg00a.lmp", 115040 }, + { "citadel_lockg00b.lmp", 115040 }, + { "citadel_lockg00c.lmp", 115040 }, + { "citadel_lockg00d.lmp", 115040 }, + { "citadel_lockg00e.lmp", 115286 }, + { "citadel_lockg01.lmp", 188038 }, + { "citadel_lockg01a.lmp", 6447 }, + { "citadel_lockg01b.lmp", 3446 }, + { "citadel_lockg01c.lmp", 3446 }, + { "citadel_lockg01d.lmp", 3446 }, + { "citadel_lockg01e.lmp", 3446 }, + { "citadel_locks00.lmp", 53252 }, + { "citadel_locks00b.lmp", 8723 }, + { "citadel_locks00c.lmp", 8170 }, + { "citadel_locks00d.lmp", 8170 }, + { "citadel_locks00e.lmp", 8170 }, + { "citadel_locks01.lmp", 60598 }, + { "citadel_locks01a.lmp", 4210 }, + { "citadel_locks01b.lmp", 6682 }, + { "citadel_locks01c.lmp", 4210 }, + { "citadel_locks01d.lmp", 5236 }, + { "citadel_locks01e.lmp", 6464 }, + { "citadelsecret.lmp", 104229 }, + { "gnomishmines.lmp", 603055 }, + { "greatcastle.lmp", 9978472 }, + { "hamlet.lmp", 11319079 }, + { "hell.lmp", 3511919 }, + { "hell00.lmp", 28651 }, + { "hell01.lmp", 22399 }, + { "hell01a.lmp", 12949 }, + { "hell01b.lmp", 18166 }, + { "hell01c.lmp", 19627 }, + { "hell01d.lmp", 16459 }, + { "hell01e.lmp", 36334 }, + { "hell01f.lmp", 18020 }, + { "hell02.lmp", 22399 }, + { "hell02a.lmp", 3763 }, + { "hell02b.lmp", 1670 }, + { "hell02c.lmp", 1910 }, + { "hell02d.lmp", 4375 }, + { "hell02e.lmp", 14071 }, + { "hell03.lmp", 50240 }, + { "hell03a.lmp", 12761 }, + { "hell03b.lmp", 40402 }, + { "hell03c.lmp", 40234 }, + { "hell03d.lmp", 69912 }, + { "hell03e.lmp", 15617 }, + { "hell04.lmp", 22972 }, + { "hell04a.lmp", 10679 }, + { "hell04b.lmp", 8986 }, + { "hell04c.lmp", 5239 }, + { "hell04d.lmp", 31329 }, + { "hell04e.lmp", 4952 }, + { "hell04f.lmp", 9569 }, + { "hell05.lmp", 25407 }, + { "hell05a.lmp", 7189 }, + { "hell05b.lmp", 3665 }, + { "hell05c.lmp", 32373 }, + { "hell05d.lmp", 22036 }, + { "hell05e.lmp", 10065 }, + { "hell06.lmp", 22609 }, + { "hell06a.lmp", 8241 }, + { "hell06b.lmp", 10302 }, + { "hell06c.lmp", 8056 }, + { "hell06d.lmp", 2877 }, + { "hell06e.lmp", 7147 }, + { "hell07.lmp", 36892 }, + { "hell07a.lmp", 17548 }, + { "hell07b.lmp", 28580 }, + { "hell07c.lmp", 32585 }, + { "hell07d.lmp", 13911 }, + { "hell07e.lmp", 12105 }, + { "hell08.lmp", 22399 }, + { "hell08a.lmp", 10072 }, + { "hell08b.lmp", 6364 }, + { "hell08c.lmp", 9463 }, + { "hell08d.lmp", 25955 }, + { "hell08e.lmp", 6529 }, + { "hell09.lmp", 120552 }, + { "hell09a.lmp", 42748 }, + { "hell09b.lmp", 42531 }, + { "hell09c.lmp", 34090 }, + { "hell09d.lmp", 39055 }, + { "hell09e.lmp", 41828 }, + { "hell09f.lmp", 31239 }, + { "hell10.lmp", 105532 }, + { "hell10a.lmp", 71620 }, + { "hell10b.lmp", 73741 }, + { "hell10c.lmp", 73427 }, + { "hell10d.lmp", 94277 }, + { "hell10e.lmp", 75177 }, + { "hell11.lmp", 154132 }, + { "hell11a.lmp", 39188 }, + { "hell11b.lmp", 960950 }, + { "hell11c.lmp", 297188 }, + { "hell11d.lmp", 311336 }, + { "hell11e.lmp", 45064 }, + { "hell12.lmp", 102711 }, + { "hell12a.lmp", 372802 }, + { "hell12b.lmp", 202404 }, + { "hell12c.lmp", 98709 }, + { "hell12d.lmp", 49665 }, + { "hell12e.lmp", 118405 }, + { "hell13.lmp", 89151 }, + { "hell13a.lmp", 8688 }, + { "hell13b.lmp", 79713 }, + { "hell13c.lmp", 82953 }, + { "hell13d.lmp", 58989 }, + { "hell13e.lmp", 156881 }, + { "hell14.lmp", 159494 }, + { "hell14a.lmp", 192538 }, + { "hell14b.lmp", 111381 }, + { "hell14c.lmp", 1156179 }, + { "hell14d.lmp", 1080797 }, + { "hell14e.lmp", 43268 }, + { "hell15.lmp", 186638 }, + { "hell15a.lmp", 277620 }, + { "hell15b.lmp", 16905 }, + { "hell15c.lmp", 110319 }, + { "hell15d.lmp", 24877 }, + { "hell15e.lmp", 39287 }, + { "hell16.lmp", 102356 }, + { "hell16a.lmp", 20283 }, + { "hell16b.lmp", 69443 }, + { "hell16c.lmp", 49121 }, + { "hell16d.lmp", 34319 }, + { "hell16e.lmp", 340840 }, + { "hell17.lmp", 102551 }, + { "hell17a.lmp", 47111 }, + { "hell17b.lmp", 62511 }, + { "hell17c.lmp", 696123 }, + { "hell17d.lmp", 65619 }, + { "hell17e.lmp", 54446 }, + { "hell18.lmp", 107665 }, + { "hell18a.lmp", 34227 }, + { "hell18b.lmp", 35552 }, + { "hell18c.lmp", 30816 }, + { "hell18d.lmp", 66616 }, + { "hell18e.lmp", 99723 }, + { "hell18f.lmp", 44282 }, + { "hell19.lmp", 46447 }, + { "hell19a.lmp", 13345 }, + { "hell19b.lmp", 6927 }, + { "hell19c.lmp", 13221 }, + { "hell19d.lmp", 19694 }, + { "hell19e.lmp", 27110 }, + { "hell20.lmp", 118897 }, + { "hell20a.lmp", 40697 }, + { "hell20b.lmp", 39985 }, + { "hell20c.lmp", 67615 }, + { "hell20d.lmp", 23485 }, + { "hell20e.lmp", 53263 }, + { "hell21.lmp", 90546 }, + { "hell21a.lmp", 30584 }, + { "hell21b.lmp", 139850 }, + { "hell21c.lmp", 58279 }, + { "hell21d.lmp", 47900 }, + { "hell21e.lmp", 57606 }, + { "hell22.lmp", 157265 }, + { "hell22a.lmp", 481602 }, + { "hell22b.lmp", 955291 }, + { "hell22c.lmp", 232845 }, + { "hell22d.lmp", 89909 }, + { "hell22e.lmp", 120716 }, + { "hell23.lmp", 21689 }, + { "hell23a.lmp", 9093 }, + { "hell23b.lmp", 33348 }, + { "hell23c.lmp", 27068 }, + { "hell23d.lmp", 25263 }, + { "hell23e.lmp", 28828 }, + { "hell24.lmp", 51865 }, + { "hell24a.lmp", 79994 }, + { "hell24b.lmp", 68952 }, + { "hell24c.lmp", 186384 }, + { "hell24d.lmp", 72132 }, + { "hell24e.lmp", 142110 }, + { "hell25.lmp", 50764 }, + { "hell25a.lmp", 89577 }, + { "hell25b.lmp", 28086 }, + { "hell25c.lmp", 23221 }, + { "hell25d.lmp", 33416 }, + { "hell25e.lmp", 13553 }, + { "hell26.lmp", 102672 }, + { "hell26a.lmp", 27154 }, + { "hell26b.lmp", 37682 }, + { "hell26c.lmp", 97908 }, + { "hell26d.lmp", 94776 }, + { "hell26e.lmp", 138670 }, + { "hell27.lmp", 17187 }, + { "hell27a.lmp", 3756 }, + { "hell27b.lmp", 3838 }, + { "hell27c.lmp", 4407 }, + { "hell27d.lmp", 2879 }, + { "hell27e.lmp", 6 }, + { "hell28.lmp", 25405 }, + { "hell28a.lmp", 11985 }, + { "hell28b.lmp", 12416 }, + { "hell28c.lmp", 11255 }, + { "hell28d.lmp", 11550 }, + { "hell28e.lmp", 14389 }, + { "hell29.lmp", 102673 }, + { "hell29a.lmp", 45392 }, + { "hell29b.lmp", 92088 }, + { "hell29c.lmp", 124389 }, + { "hell29d.lmp", 144983 }, + { "hell29e.lmp", 48684 }, + { "hell29f.lmp", 167923 }, + { "hell_lockg00.lmp", 138384 }, + { "hell_lockg00a.lmp", 18562 }, + { "hell_lockg00b.lmp", 18562 }, + { "hell_lockg00c.lmp", 18562 }, + { "hell_lockg00d.lmp", 20785 }, + { "hell_lockg00e.lmp", 10901 }, + { "hell_lockg01.lmp", 1827290 }, + { "hell_lockg01a.lmp", 15406 }, + { "hell_lockg01b.lmp", 15178 }, + { "hell_lockg01c.lmp", 15178 }, + { "hell_lockg01d.lmp", 15178 }, + { "hell_lockg01e.lmp", 15406 }, + { "hell_locks00.lmp", 318738 }, + { "hell_locks00a.lmp", 24755 }, + { "hell_locks00b.lmp", 24191 }, + { "hell_locks00c.lmp", 24635 }, + { "hell_locks00d.lmp", 24302 }, + { "hell_locks00e.lmp", 15503 }, + { "hell_locks01.lmp", 360448 }, + { "hell_locks01a.lmp", 124760 }, + { "hell_locks01b.lmp", 124760 }, + { "hell_locks01c.lmp", 124760 }, + { "hell_locks01d.lmp", 124760 }, + { "hell_locks01e.lmp", 112306 }, + { "hellboss.lmp", 4459727 }, + { "labyrinth.lmp", 397402 }, + { "labyrinth00.lmp", 119759 }, + { "labyrinth01.lmp", 32319 }, + { "labyrinth01a.lmp", 64189 }, + { "labyrinth01b.lmp", 65337 }, + { "labyrinth01c.lmp", 43746 }, + { "labyrinth01d.lmp", 26536 }, + { "labyrinth01e.lmp", 19636 }, + { "labyrinth02.lmp", 40567 }, + { "labyrinth02a.lmp", 420 }, + { "labyrinth02b.lmp", 38920 }, + { "labyrinth02c.lmp", 16008 }, + { "labyrinth02d.lmp", 24253 }, + { "labyrinth02e.lmp", 63921 }, + { "labyrinth02f.lmp", 126844 }, + { "labyrinth03.lmp", 149405 }, + { "labyrinth03a.lmp", 16472 }, + { "labyrinth03b.lmp", 111509 }, + { "labyrinth03c.lmp", 50370 }, + { "labyrinth03d.lmp", 236316 }, + { "labyrinth03e.lmp", 82737 }, + { "labyrinth03f.lmp", 191286 }, + { "labyrinth04.lmp", 109784 }, + { "labyrinth04a.lmp", 66827 }, + { "labyrinth04b.lmp", 80896 }, + { "labyrinth04c.lmp", 107797 }, + { "labyrinth04d.lmp", 89364 }, + { "labyrinth04e.lmp", 91803 }, + { "labyrinth04f.lmp", 67426 }, + { "labyrinth05.lmp", 110121 }, + { "labyrinth05a.lmp", 53007 }, + { "labyrinth05b.lmp", 47308 }, + { "labyrinth05c.lmp", 66118 }, + { "labyrinth05d.lmp", 53922 }, + { "labyrinth05e.lmp", 66820 }, + { "labyrinth06.lmp", 39633 }, + { "labyrinth06a.lmp", 16213 }, + { "labyrinth06b.lmp", 41773 }, + { "labyrinth06c.lmp", 52793 }, + { "labyrinth06d.lmp", 33467 }, + { "labyrinth06e.lmp", 35252 }, + { "labyrinth07.lmp", 39633 }, + { "labyrinth07a.lmp", 16244 }, + { "labyrinth07b.lmp", 64137 }, + { "labyrinth07c.lmp", 43007 }, + { "labyrinth07d.lmp", 49006 }, + { "labyrinth07e.lmp", 48286 }, + { "labyrinth08.lmp", 91975 }, + { "labyrinth08a.lmp", 132943 }, + { "labyrinth08b.lmp", 261965 }, + { "labyrinth08c.lmp", 116805 }, + { "labyrinth08d.lmp", 93423 }, + { "labyrinth08e.lmp", 166877 }, + { "labyrinth09.lmp", 70907 }, + { "labyrinth09a.lmp", 42123 }, + { "labyrinth09b.lmp", 123680 }, + { "labyrinth09c.lmp", 158047 }, + { "labyrinth09d.lmp", 41246 }, + { "labyrinth09e.lmp", 53113 }, + { "labyrinth10.lmp", 70911 }, + { "labyrinth10a.lmp", 37919 }, + { "labyrinth10b.lmp", 52942 }, + { "labyrinth10c.lmp", 104651 }, + { "labyrinth10d.lmp", 41875 }, + { "labyrinth10e.lmp", 77554 }, + { "labyrinth11.lmp", 102011 }, + { "labyrinth11a.lmp", 124995 }, + { "labyrinth11b.lmp", 44855 }, + { "labyrinth11c.lmp", 121035 }, + { "labyrinth11d.lmp", 245955 }, + { "labyrinth11e.lmp", 119560 }, + { "labyrinth12.lmp", 108662 }, + { "labyrinth12a.lmp", 248043 }, + { "labyrinth12b.lmp", 75322 }, + { "labyrinth12c.lmp", 251882 }, + { "labyrinth12d.lmp", 285746 }, + { "labyrinth12e.lmp", 93600 }, + { "labyrinth13.lmp", 42829 }, + { "labyrinth13a.lmp", 30982 }, + { "labyrinth13b.lmp", 21962 }, + { "labyrinth13c.lmp", 31714 }, + { "labyrinth13d.lmp", 9987 }, + { "labyrinth13e.lmp", 20182 }, + { "labyrinth13f.lmp", 12578 }, + { "labyrinth14.lmp", 67079 }, + { "labyrinth14a.lmp", 129808 }, + { "labyrinth14b.lmp", 45415 }, + { "labyrinth14c.lmp", 44746 }, + { "labyrinth14d.lmp", 100542 }, + { "labyrinth14e.lmp", 31548 }, + { "labyrinth15.lmp", 287900 }, + { "labyrinth15a.lmp", 22984 }, + { "labyrinth15b.lmp", 104822 }, + { "labyrinth15c.lmp", 114256 }, + { "labyrinth15d.lmp", 104887 }, + { "labyrinth15e.lmp", 64426 }, + { "labyrinth16.lmp", 92449 }, + { "labyrinth16a.lmp", 26543 }, + { "labyrinth16b.lmp", 62760 }, + { "labyrinth16c.lmp", 77205 }, + { "labyrinth16d.lmp", 39180 }, + { "labyrinth16e.lmp", 30724 }, + { "labyrinth17.lmp", 358170 }, + { "labyrinth17a.lmp", 27749 }, + { "labyrinth17b.lmp", 43026 }, + { "labyrinth17c.lmp", 33891 }, + { "labyrinth17d.lmp", 70771 }, + { "labyrinth17e.lmp", 123853 }, + { "labyrinth18.lmp", 142769 }, + { "labyrinth18a.lmp", 108928 }, + { "labyrinth18b.lmp", 97262 }, + { "labyrinth18c.lmp", 69372 }, + { "labyrinth18d.lmp", 68465 }, + { "labyrinth18e.lmp", 99163 }, + { "labyrinth19.lmp", 56735 }, + { "labyrinth19a.lmp", 3275 }, + { "labyrinth19b.lmp", 15120 }, + { "labyrinth19c.lmp", 37342 }, + { "labyrinth19d.lmp", 30150 }, + { "labyrinth19e.lmp", 63796 }, + { "labyrinth20.lmp", 68928 }, + { "labyrinth20a.lmp", 9243 }, + { "labyrinth20b.lmp", 68327 }, + { "labyrinth20c.lmp", 59151 }, + { "labyrinth20d.lmp", 42530 }, + { "labyrinth20e.lmp", 76531 }, + { "labyrinth21.lmp", 53319 }, + { "labyrinth21a.lmp", 3161 }, + { "labyrinth21b.lmp", 38170 }, + { "labyrinth21c.lmp", 12316 }, + { "labyrinth21d.lmp", 25530 }, + { "labyrinth21e.lmp", 60942 }, + { "labyrinth22.lmp", 35351 }, + { "labyrinth22a.lmp", 10229 }, + { "labyrinth22b.lmp", 12801 }, + { "labyrinth22c.lmp", 12265 }, + { "labyrinth22d.lmp", 10832 }, + { "labyrinth22e.lmp", 15763 }, + { "labyrinth23.lmp", 84812 }, + { "labyrinth23a.lmp", 60989 }, + { "labyrinth23b.lmp", 46318 }, + { "labyrinth23c.lmp", 38896 }, + { "labyrinth23d.lmp", 233512 }, + { "labyrinth23e.lmp", 81611 }, + { "labyrinth24.lmp", 55073 }, + { "labyrinth24a.lmp", 188853 }, + { "labyrinth24b.lmp", 74991 }, + { "labyrinth24c.lmp", 39266 }, + { "labyrinth24d.lmp", 17045 }, + { "labyrinth24e.lmp", 23087 }, + { "labyrinth25.lmp", 60492 }, + { "labyrinth25a.lmp", 12001 }, + { "labyrinth25b.lmp", 6878 }, + { "labyrinth25c.lmp", 11057 }, + { "labyrinth25d.lmp", 7201 }, + { "labyrinth25e.lmp", 4705 }, + { "labyrinth26.lmp", 10430 }, + { "labyrinth26a.lmp", 4453 }, + { "labyrinth26b.lmp", 5457 }, + { "labyrinth26c.lmp", 8932 }, + { "labyrinth26d.lmp", 3908 }, + { "labyrinth26e.lmp", 4013 }, + { "labyrinth27.lmp", 10433 }, + { "labyrinth27a.lmp", 4453 }, + { "labyrinth27b.lmp", 8752 }, + { "labyrinth27c.lmp", 4702 }, + { "labyrinth27d.lmp", 3343 }, + { "labyrinth27e.lmp", 4163 }, + { "labyrinth28.lmp", 20971 }, + { "labyrinth28a.lmp", 5511 }, + { "labyrinth28b.lmp", 4837 }, + { "labyrinth28c.lmp", 8471 }, + { "labyrinth28d.lmp", 5907 }, + { "labyrinth28e.lmp", 5629 }, + { "labyrinth29.lmp", 20516 }, + { "labyrinth29a.lmp", 2709 }, + { "labyrinth29b.lmp", 14281 }, + { "labyrinth29c.lmp", 12255 }, + { "labyrinth29d.lmp", 13714 }, + { "labyrinth29e.lmp", 27632 }, + { "labyrinth30.lmp", 67775 }, + { "labyrinth30a.lmp", 250787 }, + { "labyrinth30b.lmp", 76890 }, + { "labyrinth30c.lmp", 42344 }, + { "labyrinth30d.lmp", 122312 }, + { "labyrinth30e.lmp", 32283 }, + { "labyrinth31.lmp", 65567 }, + { "labyrinth31a.lmp", 18532 }, + { "labyrinth31b.lmp", 26136 }, + { "labyrinth31c.lmp", 44440 }, + { "labyrinth31d.lmp", 23865 }, + { "labyrinth31e.lmp", 21255 }, + { "labyrinth32.lmp", 49841 }, + { "labyrinth32a.lmp", 30907 }, + { "labyrinth32b.lmp", 35643 }, + { "labyrinth32c.lmp", 25911 }, + { "labyrinth32d.lmp", 5279 }, + { "labyrinth32e.lmp", 26968 }, + { "labyrinth_lockb00.lmp", 214460 }, + { "labyrinth_lockb00a.lmp", 30301 }, + { "labyrinth_lockb00b.lmp", 54237 }, + { "labyrinth_lockb00c.lmp", 54237 }, + { "labyrinth_lockb00d.lmp", 54237 }, + { "labyrinth_lockb00e.lmp", 41517 }, + { "labyrinth_lockb01.lmp", 41090 }, + { "labyrinth_lockb01a.lmp", 1157 }, + { "labyrinth_lockb01b.lmp", 2462 }, + { "labyrinth_lockb01c.lmp", 2462 }, + { "labyrinth_lockb01d.lmp", 2462 }, + { "labyrinth_lockb01e.lmp", 2462 }, + { "labyrinth_lockg00.lmp", 225106 }, + { "labyrinth_lockg00a.lmp", 4512 }, + { "labyrinth_lockg00b.lmp", 4808 }, + { "labyrinth_lockg00c.lmp", 3246 }, + { "labyrinth_lockg00d.lmp", 4808 }, + { "labyrinth_lockg00e.lmp", 3246 }, + { "labyrinth_lockg01.lmp", 277622 }, + { "labyrinth_lockg01a.lmp", 2305 }, + { "labyrinth_lockg01b.lmp", 1932 }, + { "labyrinth_lockg01c.lmp", 2305 }, + { "labyrinth_lockg01d.lmp", 2305 }, + { "labyrinth_lockg01e.lmp", 2305 }, + { "labyrinth_locks00.lmp", 103135 }, + { "labyrinth_locks00a.lmp", 31522 }, + { "labyrinth_locks00b.lmp", 30632 }, + { "labyrinth_locks00c.lmp", 33582 }, + { "labyrinth_locks00d.lmp", 33756 }, + { "labyrinth_locks00e.lmp", 33756 }, + { "labyrinth_locks01.lmp", 31629 }, + { "labyrinth_locks01a.lmp", 3013 }, + { "labyrinth_locks01b.lmp", 781 }, + { "labyrinth_locks01c.lmp", 781 }, + { "labyrinth_locks01d.lmp", 2469 }, + { "labyrinth_locks01e.lmp", 2651 }, + { "labyrinthsecret.lmp", 73135 }, + { "labyrinthtoruins.lmp", 137530 }, + { "mainmenu1.lmp", 12427 }, + { "mainmenu2.lmp", 23291 }, + { "mainmenu3.lmp", 67606 }, + { "mainmenu4.lmp", 59632 }, + { "mainmenu5.lmp", 122197 }, + { "mainmenu6.lmp", 84492 }, + { "mainmenu7.lmp", 464553 }, + { "mainmenu8.lmp", 97824 }, + { "mainmenu9.lmp", 4654457 }, + { "mine.lmp", 80741 }, + { "mine00.lmp", 12890 }, + { "mine01.lmp", 52889 }, + { "mine01a.lmp", 4094 }, + { "mine01b.lmp", 5156 }, + { "mine01c.lmp", 5524 }, + { "mine01d.lmp", 2780 }, + { "mine01e.lmp", 33508 }, + { "mine01f.lmp", 2436 }, + { "mine01g.lmp", 15844 }, + { "mine02.lmp", 16102 }, + { "mine02a.lmp", 1964 }, + { "mine02b.lmp", 2050 }, + { "mine02c.lmp", 3796 }, + { "mine02d.lmp", 19759 }, + { "mine02e.lmp", 2052 }, + { "mine02f.lmp", 375 }, + { "mine02g.lmp", 862 }, + { "mine03.lmp", 17657 }, + { "mine03a.lmp", 3440 }, + { "mine03b.lmp", 49719 }, + { "mine03c.lmp", 3274 }, + { "mine03d.lmp", 1451 }, + { "mine03e.lmp", 1516 }, + { "mine03f.lmp", 6794 }, + { "mine03g.lmp", 1331 }, + { "mine04.lmp", 21711 }, + { "mine04a.lmp", 2264 }, + { "mine04b.lmp", 6415 }, + { "mine04c.lmp", 4200 }, + { "mine04d.lmp", 2925 }, + { "mine04e.lmp", 914 }, + { "mine04f.lmp", 1042 }, + { "mine04g.lmp", 2415 }, + { "mine05.lmp", 33835 }, + { "mine05a.lmp", 4057 }, + { "mine05b.lmp", 8869 }, + { "mine05c.lmp", 3943 }, + { "mine05d.lmp", 6466 }, + { "mine05e.lmp", 3486 }, + { "mine06.lmp", 79441 }, + { "mine06a.lmp", 12895 }, + { "mine06b.lmp", 58604 }, + { "mine06c.lmp", 28704 }, + { "mine06d.lmp", 81643 }, + { "mine06e.lmp", 25051 }, + { "mine07.lmp", 107931 }, + { "mine07a.lmp", 13341 }, + { "mine07b.lmp", 24443 }, + { "mine07c.lmp", 49306 }, + { "mine07d.lmp", 17195 }, + { "mine07e.lmp", 13843 }, + { "mine08.lmp", 50089 }, + { "mine08a.lmp", 2824 }, + { "mine08b.lmp", 6387 }, + { "mine08c.lmp", 3666 }, + { "mine08d.lmp", 27312 }, + { "mine08e.lmp", 1614 }, + { "mine09.lmp", 57257 }, + { "mine09a.lmp", 10581 }, + { "mine09b.lmp", 10445 }, + { "mine09c.lmp", 46588 }, + { "mine09d.lmp", 30484 }, + { "mine09e.lmp", 10147 }, + { "mine10.lmp", 121482 }, + { "mine10a.lmp", 178759 }, + { "mine10b.lmp", 70918 }, + { "mine10c.lmp", 21147 }, + { "mine10d.lmp", 65233 }, + { "mine10e.lmp", 61077 }, + { "mine11.lmp", 74234 }, + { "mine11a.lmp", 7665 }, + { "mine11b.lmp", 7909 }, + { "mine11c.lmp", 4787 }, + { "mine11d.lmp", 18375 }, + { "mine11e.lmp", 55599 }, + { "mine12.lmp", 30195 }, + { "mine12a.lmp", 2408 }, + { "mine12b.lmp", 1064 }, + { "mine12c.lmp", 1064 }, + { "mine12d.lmp", 1064 }, + { "mine12e.lmp", 1066 }, + { "mine12f.lmp", 1123 }, + { "mine12g.lmp", 3173 }, + { "mine12h.lmp", 51714 }, + { "mine12i.lmp", 29186 }, + { "mine13.lmp", 43438 }, + { "mine13a.lmp", 6070 }, + { "mine13b.lmp", 6864 }, + { "mine13c.lmp", 94568 }, + { "mine13d.lmp", 14907 }, + { "mine13e.lmp", 14220 }, + { "mine14.lmp", 59017 }, + { "mine14a.lmp", 8371 }, + { "mine14b.lmp", 10552 }, + { "mine14c.lmp", 32460 }, + { "mine14d.lmp", 12894 }, + { "mine14e.lmp", 6706 }, + { "mine15.lmp", 44160 }, + { "mine15a.lmp", 14464 }, + { "mine15b.lmp", 16268 }, + { "mine15c.lmp", 15070 }, + { "mine15d.lmp", 11959 }, + { "mine15e.lmp", 6991 }, + { "mine16.lmp", 47683 }, + { "mine16a.lmp", 5567 }, + { "mine16b.lmp", 4516 }, + { "mine16c.lmp", 11200 }, + { "mine16d.lmp", 19302 }, + { "mine16e.lmp", 36439 }, + { "mine16f.lmp", 49227 }, + { "mine17.lmp", 18741 }, + { "mine17a.lmp", 4756 }, + { "mine17b.lmp", 6251 }, + { "mine17c.lmp", 6627 }, + { "mine17d.lmp", 5883 }, + { "mine17e.lmp", 7393 }, + { "mine17f.lmp", 7827 }, + { "mine18.lmp", 48006 }, + { "mine18a.lmp", 10791 }, + { "mine18b.lmp", 11978 }, + { "mine18c.lmp", 26618 }, + { "mine18d.lmp", 32406 }, + { "mine18e.lmp", 11426 }, + { "mine19.lmp", 21099 }, + { "mine19a.lmp", 2014 }, + { "mine19b.lmp", 2706 }, + { "mine19c.lmp", 2448 }, + { "mine19d.lmp", 2500 }, + { "mine19e.lmp", 4188 }, + { "mine20.lmp", 17481 }, + { "mine20a.lmp", 968 }, + { "mine20b.lmp", 1473 }, + { "mine20c.lmp", 1953 }, + { "mine20d.lmp", 1077 }, + { "mine20e.lmp", 10418 }, + { "mine21.lmp", 26317 }, + { "mine21a.lmp", 4635 }, + { "mine21b.lmp", 4623 }, + { "mine21c.lmp", 4872 }, + { "mine21d.lmp", 5830 }, + { "mine21e.lmp", 5158 }, + { "mine22.lmp", 20095 }, + { "mine22a.lmp", 937 }, + { "mine22b.lmp", 1229 }, + { "mine22c.lmp", 1229 }, + { "mine22d.lmp", 1353 }, + { "mine22e.lmp", 2587 }, + { "mine22f.lmp", 28494 }, + { "mine23.lmp", 20477 }, + { "mine23a.lmp", 2453 }, + { "mine23b.lmp", 2528 }, + { "mine23c.lmp", 3226 }, + { "mine23d.lmp", 1998 }, + { "mine23e.lmp", 14561 }, + { "mine23f.lmp", 3490 }, + { "mine24.lmp", 18353 }, + { "mine24a.lmp", 2206 }, + { "mine24b.lmp", 2573 }, + { "mine24c.lmp", 1544 }, + { "mine24d.lmp", 868 }, + { "mine24e.lmp", 1541 }, + { "mine25.lmp", 11992 }, + { "mine25a.lmp", 1512 }, + { "mine25b.lmp", 1099 }, + { "mine25c.lmp", 10042 }, + { "mine25d.lmp", 475 }, + { "mine25e.lmp", 945 }, + { "mine26.lmp", 6642 }, + { "mine26a.lmp", 646 }, + { "mine26b.lmp", 568 }, + { "mine26c.lmp", 650 }, + { "mine26d.lmp", 729 }, + { "mine26e.lmp", 1912 }, + { "mine27.lmp", 6783 }, + { "mine27a.lmp", 647 }, + { "mine27b.lmp", 646 }, + { "mine27c.lmp", 719 }, + { "mine27d.lmp", 727 }, + { "mine27e.lmp", 2659 }, + { "mine28.lmp", 12641 }, + { "mine28a.lmp", 1867 }, + { "mine28b.lmp", 1981 }, + { "mine28c.lmp", 2279 }, + { "mine28d.lmp", 2763 }, + { "mine28e.lmp", 2148 }, + { "mine29.lmp", 7173 }, + { "mine29a.lmp", 631 }, + { "mine29b.lmp", 663 }, + { "mine29c.lmp", 853 }, + { "mine29d.lmp", 685 }, + { "mine29e.lmp", 777 }, + { "mine30.lmp", 56963 }, + { "mine30a.lmp", 24107 }, + { "mine30b.lmp", 55131 }, + { "mine30c.lmp", 21350 }, + { "mine30d.lmp", 11448 }, + { "mine30e.lmp", 17946 }, + { "mine31.lmp", 38812 }, + { "mine31a.lmp", 22071 }, + { "mine31b.lmp", 14258 }, + { "mine31c.lmp", 13828 }, + { "mine31d.lmp", 6625 }, + { "mine31e.lmp", 8149 }, + { "mine32.lmp", 7834 }, + { "mine32a.lmp", 2775 }, + { "mine32b.lmp", 2944 }, + { "mine32c.lmp", 1449 }, + { "mine32d.lmp", 494 }, + { "mine32e.lmp", 1916 }, + { "mine33.lmp", 12331 }, + { "mine33a.lmp", 63 }, + { "mine33b.lmp", 366 }, + { "mine33c.lmp", 564 }, + { "mine33d.lmp", 960 }, + { "mine33e.lmp", 298 }, + { "mine33f.lmp", 258 }, + { "mine33g.lmp", 2972 }, + { "mine_lockb00.lmp", 9555 }, + { "mine_lockb00a.lmp", 2941 }, + { "mine_lockb00b.lmp", 4345 }, + { "mine_lockb00c.lmp", 2400 }, + { "mine_lockb00d.lmp", 4538 }, + { "mine_lockb00e.lmp", 2615 }, + { "mine_lockb01.lmp", 10787 }, + { "mine_lockb01a.lmp", 5580 }, + { "mine_lockb01b.lmp", 5580 }, + { "mine_lockb01c.lmp", 7068 }, + { "mine_lockb01d.lmp", 5580 }, + { "mine_lockb01e.lmp", 6561 }, + { "mine_lockg00.lmp", 118098 }, + { "mine_lockg00a.lmp", 3061 }, + { "mine_lockg00b.lmp", 3952 }, + { "mine_lockg00c.lmp", 5058 }, + { "mine_lockg00d.lmp", 2219 }, + { "mine_lockg00e.lmp", 5122 }, + { "mine_lockg01.lmp", 295126 }, + { "mine_lockg01a.lmp", 465 }, + { "mine_lockg01b.lmp", 465 }, + { "mine_lockg01c.lmp", 465 }, + { "mine_lockg01d.lmp", 1194 }, + { "mine_lockg01e.lmp", 465 }, + { "mine_lockg01f.lmp", 465 }, + { "mine_lockg01g.lmp", 1460 }, + { "mine_lockg01h.lmp", 1753 }, + { "mine_locks00.lmp", 195239 }, + { "mine_locks00a.lmp", 661 }, + { "mine_locks00b.lmp", 1195 }, + { "mine_locks00c.lmp", 1195 }, + { "mine_locks00d.lmp", 661 }, + { "mine_locks00e.lmp", 886 }, + { "mine_locks00f.lmp", 886 }, + { "mine_locks01.lmp", 70336 }, + { "mine_locks01a.lmp", 7961 }, + { "mine_locks01b.lmp", 5087 }, + { "mine_locks01c.lmp", 6543 }, + { "mine_locks01d.lmp", 5087 }, + { "mine_locks01e.lmp", 5087 }, + { "minesecret.lmp", 35723 }, + { "minetoswamp.lmp", 132973 }, + { "minetown.lmp", 842756 }, + { "minotaur.lmp", 4538337 }, + { "mysticlibrary.lmp", 364240 }, + { "orb_blue.lmp", 17502 }, + { "orb_green.lmp", 20522 }, + { "orb_red.lmp", 41907 }, + { "ruins.lmp", 92624 }, + { "ruins00.lmp", 6373 }, { "ruins01.lmp", 149157 }, { "ruins01a.lmp", 103231 }, { "ruins01b.lmp", 156192 }, @@ -832,7 +1265,7 @@ std::unordered_map mapHashes = { { "ruins02a.lmp", 97513 }, { "ruins02b.lmp", 38538 }, { "ruins02c.lmp", 59764 }, - { "ruins02d.lmp", 174860 }, + { "ruins02d.lmp", 178351 }, { "ruins02e.lmp", 74840 }, { "ruins03.lmp", 79141 }, { "ruins03a.lmp", 16088 }, @@ -860,7 +1293,7 @@ std::unordered_map mapHashes = { { "ruins06e.lmp", 56033 }, { "ruins07.lmp", 102161 }, { "ruins07a.lmp", 2150 }, - { "ruins07b.lmp", 81411 }, + { "ruins07b.lmp", 36768 }, { "ruins07c.lmp", 114854 }, { "ruins07d.lmp", 45084 }, { "ruins07e.lmp", 42387 }, @@ -880,12 +1313,12 @@ std::unordered_map mapHashes = { { "ruins10a.lmp", 10773 }, { "ruins10b.lmp", 9221 }, { "ruins10c.lmp", 9242 }, - { "ruins10d.lmp", 32823 }, + { "ruins10d.lmp", 27855 }, { "ruins10e.lmp", 18179 }, { "ruins11.lmp", 55530 }, { "ruins11a.lmp", 11046 }, { "ruins11b.lmp", 8791 }, - { "ruins11c.lmp", 39502 }, + { "ruins11c.lmp", 36519 }, { "ruins11d.lmp", 25593 }, { "ruins11e.lmp", 13047 }, { "ruins12.lmp", 24121 }, @@ -939,7 +1372,7 @@ std::unordered_map mapHashes = { { "ruins20.lmp", 239795 }, { "ruins20a.lmp", 39623 }, { "ruins20b.lmp", 152619 }, - { "ruins20c.lmp", 416612 }, + { "ruins20c.lmp", 410084 }, { "ruins20d.lmp", 57511 }, { "ruins20e.lmp", 369845 }, { "ruins21.lmp", 71662 }, @@ -1030,451 +1463,548 @@ std::unordered_map mapHashes = { { "ruins34c.lmp", 1477 }, { "ruins34d.lmp", 58 }, { "ruins34e.lmp", 129 }, - { "ruinssecret.lmp", 66694 }, - { "sanctum.lmp", 6476194 }, - { "shop-roomgen00.lmp", 11844 }, - { "shop-roomgen01.lmp", 9917 }, - { "shop-roomgen02.lmp", 40191 }, - { "shop-roomgen02a.lmp", 4754 }, - { "shop-roomgen02b.lmp", 5250 }, - { "shop-roomgen02c.lmp", 5481 }, - { "shop-roomgen02d.lmp", 4467 }, - { "shop-roomgen02e.lmp", 4575 }, - { "shop-roomgen02f.lmp", 4025 }, - { "shop-roomgen03.lmp", 5324 }, - { "shop-roomgen04.lmp", 6945 }, - { "shop-roomgen05.lmp", 12261 }, - { "shop-roomgen06.lmp", 12925 }, - { "shop-roomgen07.lmp", 10729 }, - { "shop-roomgen08.lmp", 12489 }, - { "shop-roomgen09.lmp", 9209 }, - { "shop-roomgen10.lmp", 11533 }, - { "shop00.lmp", 8172 }, - { "shop01.lmp", 14016 }, - { "shop02.lmp", 39289 }, - { "shop02a.lmp", 4905 }, - { "shop02b.lmp", 5250 }, - { "shop02c.lmp", 5481 }, - { "shop02d.lmp", 4467 }, - { "shop02e.lmp", 4962 }, - { "shop02f.lmp", 4025 }, - { "shop03.lmp", 9675 }, - { "shop04.lmp", 15493 }, - { "shop05.lmp", 13570 }, - { "shop06.lmp", 16915 }, - { "shop07.lmp", 13185 }, - { "shop08.lmp", 18483 }, - { "shop09.lmp", 10094 }, - { "shop10.lmp", 16181 }, - { "sokoban.lmp", 140500 }, - { "start.lmp", 648033 }, - { "swamp.lmp", 124034 }, - { "swamp00.lmp", 13629 }, - { "swamp01.lmp", 29459 }, - { "swamp01a.lmp", 5303 }, - { "swamp01b.lmp", 6563 }, - { "swamp01c.lmp", 10334 }, - { "swamp01d.lmp", 5571 }, - { "swamp01e.lmp", 4060 }, - { "swamp02.lmp", 39297 }, - { "swamp02a.lmp", 9131 }, - { "swamp02b.lmp", 11457 }, - { "swamp02c.lmp", 8877 }, - { "swamp02d.lmp", 7327 }, - { "swamp02e.lmp", 4203 }, - { "swamp03.lmp", 35148 }, - { "swamp03a.lmp", 6837 }, - { "swamp03b.lmp", 25871 }, - { "swamp03c.lmp", 3520 }, - { "swamp03d.lmp", 7686 }, - { "swamp03e.lmp", 15932 }, - { "swamp04.lmp", 51827 }, - { "swamp04a.lmp", 6931 }, - { "swamp04b.lmp", 5875 }, - { "swamp04c.lmp", 4610 }, - { "swamp04d.lmp", 7818 }, - { "swamp04e.lmp", 18431 }, - { "swamp05.lmp", 20558 }, - { "swamp05a.lmp", 2611 }, - { "swamp05b.lmp", 2966 }, - { "swamp05c.lmp", 5344 }, - { "swamp05d.lmp", 1610 }, - { "swamp05e.lmp", 2746 }, - { "swamp06.lmp", 60048 }, - { "swamp06a.lmp", 12354 }, - { "swamp06b.lmp", 31135 }, - { "swamp06c.lmp", 39770 }, - { "swamp06d.lmp", 60075 }, - { "swamp06e.lmp", 6324 }, - { "swamp07.lmp", 91130 }, - { "swamp07a.lmp", 12340 }, - { "swamp07b.lmp", 7290 }, - { "swamp07c.lmp", 20314 }, - { "swamp07d.lmp", 45805 }, - { "swamp07e.lmp", 43344 }, - { "swamp08.lmp", 31288 }, - { "swamp08a.lmp", 3734 }, - { "swamp08b.lmp", 5429 }, - { "swamp08c.lmp", 9406 }, - { "swamp08d.lmp", 31893 }, - { "swamp08e.lmp", 2913 }, - { "swamp09.lmp", 29565 }, - { "swamp09a.lmp", 1516 }, - { "swamp09b.lmp", 6762 }, - { "swamp09c.lmp", 6705 }, - { "swamp09d.lmp", 4955 }, - { "swamp09e.lmp", 3428 }, - { "swamp10.lmp", 27653 }, - { "swamp10a.lmp", 2322 }, - { "swamp10b.lmp", 5975 }, - { "swamp10c.lmp", 5260 }, - { "swamp10d.lmp", 6911 }, - { "swamp10e.lmp", 2344 }, - { "swamp10f.lmp", 3680 }, - { "swamp11.lmp", 64926 }, - { "swamp11a.lmp", 6851 }, - { "swamp11b.lmp", 11835 }, - { "swamp11c.lmp", 26869 }, - { "swamp11d.lmp", 46682 }, - { "swamp11e.lmp", 37481 }, - { "swamp12.lmp", 52448 }, - { "swamp12a.lmp", 7504 }, - { "swamp12b.lmp", 14581 }, - { "swamp12c.lmp", 8585 }, - { "swamp12d.lmp", 12097 }, - { "swamp12e.lmp", 13987 }, - { "swamp13.lmp", 30293 }, - { "swamp13a.lmp", 6911 }, - { "swamp13b.lmp", 10109 }, - { "swamp13c.lmp", 9103 }, - { "swamp13d.lmp", 5933 }, - { "swamp13e.lmp", 6334 }, - { "swamp14.lmp", 41242 }, - { "swamp14a.lmp", 9092 }, - { "swamp14b.lmp", 4317 }, - { "swamp14c.lmp", 14974 }, - { "swamp14d.lmp", 19525 }, - { "swamp14e.lmp", 6608 }, - { "swamp15.lmp", 352 }, - { "swamp16.lmp", 14028 }, - { "swamp16a.lmp", 1377 }, - { "swamp16b.lmp", 1411 }, - { "swamp16c.lmp", 856 }, - { "swamp16d.lmp", 2052 }, - { "swamp16e.lmp", 1154 }, - { "swamp17.lmp", 31463 }, - { "swamp17a.lmp", 5507 }, - { "swamp17b.lmp", 10976 }, - { "swamp17c.lmp", 9420 }, - { "swamp17d.lmp", 6550 }, - { "swamp17e.lmp", 4447 }, - { "swamp18.lmp", 18634 }, - { "swamp18a.lmp", 934 }, - { "swamp18b.lmp", 3264 }, - { "swamp18c.lmp", 5153 }, - { "swamp18d.lmp", 2974 }, - { "swamp18e.lmp", 1429 }, - { "swamp18f.lmp", 3207 }, - { "swamp18g.lmp", 3646 }, - { "swamp19.lmp", 20258 }, - { "swamp19a.lmp", 3302 }, - { "swamp19b.lmp", 1808 }, - { "swamp19c.lmp", 3136 }, - { "swamp19d.lmp", 3705 }, - { "swamp19e.lmp", 4800 }, - { "swamp20.lmp", 33017 }, - { "swamp20a.lmp", 4569 }, - { "swamp20b.lmp", 10215 }, - { "swamp20c.lmp", 5654 }, - { "swamp20d.lmp", 26165 }, - { "swamp20e.lmp", 6173 }, - { "swamp21.lmp", 16295 }, - { "swamp21a.lmp", 1887 }, - { "swamp21b.lmp", 3323 }, - { "swamp21c.lmp", 6881 }, - { "swamp21d.lmp", 25271 }, - { "swamp21e.lmp", 3401 }, - { "swamp22.lmp", 55275 }, - { "swamp22a.lmp", 13094 }, - { "swamp22b.lmp", 5215 }, - { "swamp22c.lmp", 16634 }, - { "swamp22d.lmp", 8710 }, - { "swamp22e.lmp", 26057 }, - { "swamp23.lmp", 63299 }, - { "swamp23a.lmp", 9089 }, - { "swamp23b.lmp", 18498 }, - { "swamp23c.lmp", 26586 }, - { "swamp23d.lmp", 8888 }, - { "swamp23e.lmp", 14992 }, - { "swamp24.lmp", 36395 }, - { "swamp24a.lmp", 5045 }, - { "swamp24b.lmp", 4267 }, - { "swamp24c.lmp", 5682 }, - { "swamp24d.lmp", 5928 }, - { "swamp24e.lmp", 12981 }, - { "swamp25.lmp", 34758 }, - { "swamp25a.lmp", 5060 }, - { "swamp25b.lmp", 6237 }, - { "swamp25c.lmp", 12252 }, - { "swamp25d.lmp", 5226 }, - { "swamp25e.lmp", 5717 }, - { "swamp25f.lmp", 10354 }, - { "swamp26.lmp", 40227 }, - { "swamp26a.lmp", 3747 }, - { "swamp26b.lmp", 2398 }, - { "swamp26c.lmp", 1005 }, - { "swamp26d.lmp", 3129 }, - { "swamp26e.lmp", 1262 }, - { "swamp27.lmp", 42021 }, - { "swamp27a.lmp", 4975 }, - { "swamp27b.lmp", 3725 }, - { "swamp27c.lmp", 2059 }, - { "swamp27d.lmp", 5312 }, - { "swamp27e.lmp", 4425 }, - { "swamp28.lmp", 6067 }, - { "swamp28a.lmp", 676 }, - { "swamp28b.lmp", 524 }, - { "swamp28c.lmp", 2016 }, - { "swamp28d.lmp", 1353 }, - { "swamp28e.lmp", 1457 }, - { "swamp28f.lmp", 338 }, - { "swamp29.lmp", 6111 }, - { "swamp29a.lmp", 591 }, - { "swamp29b.lmp", 596 }, - { "swamp29c.lmp", 1336 }, - { "swamp29d.lmp", 2619 }, - { "swamp29e.lmp", 1391 }, - { "swamp29f.lmp", 1249 }, - { "swamp29g.lmp", 1249 }, - { "swamp30.lmp", 54169 }, - { "swamp30a.lmp", 17250 }, - { "swamp30b.lmp", 69206 }, - { "swamp30c.lmp", 20925 }, - { "swamp30d.lmp", 18887 }, - { "swamp30e.lmp", 37453 }, - { "swamp31.lmp", 24287 }, - { "swamp31a.lmp", 7582 }, - { "swamp31b.lmp", 9169 }, - { "swamp31c.lmp", 2026 }, - { "swamp31d.lmp", 5365 }, - { "swamp31e.lmp", 35837 }, - { "swamp32.lmp", 30796 }, - { "swamp32a.lmp", 5496 }, - { "swamp32b.lmp", 6551 }, - { "swamp32c.lmp", 4638 }, - { "swamp32d.lmp", 6124 }, - { "swamp32e.lmp", 9867 }, - { "swamp33.lmp", 31993 }, - { "swamp33a.lmp", 9437 }, - { "swamp33b.lmp", 15085 }, - { "swamp33c.lmp", 6699 }, - { "swamp33d.lmp", 7910 }, - { "swamp33e.lmp", 10948 }, - { "swamp34.lmp", 36795 }, - { "swamp34a.lmp", 6929 }, - { "swamp34b.lmp", 68279 }, - { "swamp34c.lmp", 12514 }, - { "swamp34d.lmp", 5546 }, - { "swamp34e.lmp", 17278 }, - { "swampsecret.lmp", 24027 }, - { "swamptolabyrinth.lmp", 213824 }, - { "temple.lmp", 6911955 }, - { "tutorial1.lmp", 709674 }, - { "tutorial10.lmp", 1504767 }, - { "tutorial2.lmp", 1313718 }, - { "tutorial3.lmp", 1235301 }, - { "tutorial4.lmp", 1926157 }, - { "tutorial5.lmp", 873881 }, - { "tutorial6.lmp", 2024462 }, - { "tutorial7.lmp", 4059721 }, - { "tutorial8.lmp", 1721791 }, - { "tutorial9.lmp", 3166055 }, - { "tutorial_hub.lmp", 22944284 }, - { "underworld.lmp", 253866 }, - { "underworld00.lmp", 26338 }, - { "underworld01.lmp", 63364 }, - { "underworld01a.lmp", 15722 }, - { "underworld01b.lmp", 23578 }, - { "underworld01c.lmp", 16338 }, - { "underworld01d.lmp", 13107 }, - { "underworld01e.lmp", 21000 }, - { "underworld02.lmp", 35793 }, - { "underworld02a.lmp", 7965 }, - { "underworld02b.lmp", 11142 }, - { "underworld02c.lmp", 12186 }, - { "underworld02d.lmp", 9758 }, - { "underworld02e.lmp", 10867 }, - { "underworld03.lmp", 7801 }, - { "underworld03a.lmp", 739 }, - { "underworld03b.lmp", 1458 }, - { "underworld03c.lmp", 2290 }, - { "underworld03d.lmp", 1345 }, - { "underworld03e.lmp", 1423 }, - { "underworld04.lmp", 13996 }, - { "underworld04a.lmp", 2482 }, - { "underworld04b.lmp", 2530 }, - { "underworld04c.lmp", 3130 }, - { "underworld04d.lmp", 3326 }, - { "underworld04e.lmp", 3949 }, - { "underworld05.lmp", 40061 }, - { "underworld05a.lmp", 6687 }, - { "underworld05b.lmp", 8086 }, - { "underworld05c.lmp", 15740 }, - { "underworld05d.lmp", 12373 }, - { "underworld05e.lmp", 11278 }, - { "underworld06.lmp", 40061 }, - { "underworld06a.lmp", 9012 }, - { "underworld06b.lmp", 9570 }, - { "underworld06c.lmp", 11087 }, - { "underworld06d.lmp", 21558 }, - { "underworld06e.lmp", 12493 }, - { "underworld07.lmp", 27085 }, - { "underworld07a.lmp", 6214 }, - { "underworld07b.lmp", 8553 }, - { "underworld07c.lmp", 6616 }, - { "underworld07d.lmp", 46782 }, - { "underworld07e.lmp", 5087 }, - { "underworld08.lmp", 89325 }, - { "underworld08a.lmp", 14990 }, - { "underworld08b.lmp", 23417 }, - { "underworld08c.lmp", 27050 }, - { "underworld08d.lmp", 38494 }, - { "underworld08e.lmp", 66467 }, - { "underworld09.lmp", 76902 }, - { "underworld09a.lmp", 29965 }, - { "underworld09b.lmp", 44375 }, - { "underworld09c.lmp", 35307 }, - { "underworld09d.lmp", 27376 }, - { "underworld09e.lmp", 30058 }, - { "underworld10.lmp", 119996 }, - { "underworld10a.lmp", 135977 }, - { "underworld10b.lmp", 47378 }, - { "underworld10c.lmp", 37212 }, - { "underworld10d.lmp", 41839 }, - { "underworld10e.lmp", 41223 }, - { "underworld10f.lmp", 50632 }, - { "underworld11.lmp", 118540 }, - { "underworld11a.lmp", 12213 }, - { "underworld11b.lmp", 25786 }, - { "underworld11c.lmp", 13884 }, - { "underworld11d.lmp", 14126 }, - { "underworld11e.lmp", 8680 }, - { "underworld11f.lmp", 17197 }, - { "underworld12.lmp", 55688 }, - { "underworld12a.lmp", 12590 }, - { "underworld12b.lmp", 21302 }, - { "underworld12c.lmp", 14416 }, - { "underworld12d.lmp", 21990 }, - { "underworld12e.lmp", 11802 }, - { "underworld13.lmp", 55688 }, - { "underworld13a.lmp", 12987 }, - { "underworld13b.lmp", 14430 }, - { "underworld13c.lmp", 15534 }, - { "underworld13d.lmp", 31839 }, - { "underworld13e.lmp", 23587 }, - { "underworld14.lmp", 59545 }, - { "underworld14a.lmp", 10537 }, - { "underworld14b.lmp", 61997 }, - { "underworld14c.lmp", 15043 }, - { "underworld14d.lmp", 7921 }, - { "underworld14e.lmp", 22800 }, - { "underworld15.lmp", 119344 }, - { "underworld15a.lmp", 48251 }, - { "underworld15b.lmp", 48500 }, - { "underworld15c.lmp", 39405 }, - { "underworld15d.lmp", 22683 }, - { "underworld15e.lmp", 88967 }, - { "underworld16.lmp", 7801 }, - { "underworld16a.lmp", 1213 }, - { "underworld16b.lmp", 1486 }, - { "underworld16c.lmp", 2136 }, - { "underworld16d.lmp", 1496 }, - { "underworld16e.lmp", 2136 }, - { "underworld17.lmp", 77818 }, - { "underworld17a.lmp", 21652 }, - { "underworld17b.lmp", 40598 }, - { "underworld17c.lmp", 35498 }, - { "underworld17d.lmp", 16492 }, - { "underworld17e.lmp", 18237 }, - { "underworld18.lmp", 130922 }, - { "underworld18a.lmp", 23389 }, - { "underworld18b.lmp", 37706 }, - { "underworld18c.lmp", 26004 }, - { "underworld18d.lmp", 24737 }, - { "underworld18e.lmp", 19698 }, - { "underworld19.lmp", 147747 }, - { "underworld19a.lmp", 57816 }, - { "underworld19b.lmp", 47551 }, - { "underworld19c.lmp", 73591 }, - { "underworld19d.lmp", 48973 }, - { "underworld19e.lmp", 111087 }, - { "underworld20.lmp", 147553 }, - { "underworld20a.lmp", 44406 }, - { "underworld20b.lmp", 55526 }, - { "underworld20c.lmp", 203037 }, - { "underworld20d.lmp", 43029 }, - { "underworld20e.lmp", 55805 }, - { "underworld21.lmp", 144910 }, - { "underworld21a.lmp", 47510 }, - { "underworld21b.lmp", 76444 }, - { "underworld21c.lmp", 48584 }, - { "underworld21d.lmp", 48359 }, - { "underworld21e.lmp", 30448 }, - { "underworld22.lmp", 143298 }, - { "underworld22a.lmp", 64856 }, - { "underworld22b.lmp", 51769 }, - { "underworld22c.lmp", 59299 }, - { "underworld22d.lmp", 42229 }, - { "underworld22e.lmp", 36337 }, - { "underworld23.lmp", 27113 }, - { "underworld23a.lmp", 4307 }, - { "underworld23b.lmp", 6289 }, - { "underworld23c.lmp", 9948 }, - { "underworld23d.lmp", 10823 }, - { "underworld23e.lmp", 9851 }, - { "underworld24.lmp", 27113 }, - { "underworld24a.lmp", 4337 }, - { "underworld24b.lmp", 6629 }, - { "underworld24c.lmp", 7018 }, - { "underworld24d.lmp", 5098 }, - { "underworld24e.lmp", 7379 }, - { "underworld25.lmp", 60508 }, - { "underworld25a.lmp", 39942 }, - { "underworld25b.lmp", 16973 }, - { "underworld25c.lmp", 13818 }, - { "underworld25d.lmp", 20478 }, - { "underworld25e.lmp", 13543 }, - { "underworld26.lmp", 11893 }, - { "underworld26a.lmp", 2706 }, - { "underworld26b.lmp", 3206 }, - { "underworld26c.lmp", 3087 }, - { "underworld26d.lmp", 3343 }, - { "underworld26e.lmp", 3667 }, - { "underworld27.lmp", 122626 }, - { "underworld27a.lmp", 58537 }, - { "underworld27b.lmp", 44335 }, - { "underworld27c.lmp", 22451 }, - { "underworld27d.lmp", 42906 }, - { "underworld27e.lmp", 44300 }, - { "underworld28.lmp", 32847 }, - { "underworld28a.lmp", 12273 }, - { "underworld28b.lmp", 7472 }, - { "underworld28c.lmp", 7944 }, - { "underworld28d.lmp", 5599 }, - { "underworld28e.lmp", 8830 }, - { "underworld29.lmp", 40133 }, - { "underworld29a.lmp", 15495 }, - { "underworld29b.lmp", 14568 }, - { "underworld29c.lmp", 12048 }, - { "underworld29d.lmp", 16846 }, - { "underworld29e.lmp", 17605 }, - { "underworld30.lmp", 112313 }, - { "underworld30a.lmp", 97910 }, - { "underworld30b.lmp", 190872 }, - { "underworld30c.lmp", 47525 }, - { "underworld30d.lmp", 49543 }, - { "underworld30e.lmp", 46600 }, - { "warpzone.lmp", 3133088 }, + { "ruins_lockb00.lmp", 24183 }, + { "ruins_lockb00a.lmp", 764 }, + { "ruins_lockb00b.lmp", 764 }, + { "ruins_lockb00c.lmp", 764 }, + { "ruins_lockb00d.lmp", 764 }, + { "ruins_lockb00e.lmp", 764 }, + { "ruins_lockb01.lmp", 15457 }, + { "ruins_lockb01a.lmp", 1700 }, + { "ruins_lockb01b.lmp", 1701 }, + { "ruins_lockb01c.lmp", 1338 }, + { "ruins_lockb01d.lmp", 913 }, + { "ruins_lockb01e.lmp", 1768 }, + { "ruins_lockg00.lmp", 239522 }, + { "ruins_lockg00a.lmp", 862 }, + { "ruins_lockg00b.lmp", 850 }, + { "ruins_lockg00c.lmp", 850 }, + { "ruins_lockg00d.lmp", 850 }, + { "ruins_lockg00e.lmp", 850 }, + { "ruins_lockg01.lmp", 106934 }, + { "ruins_lockg01a.lmp", 2713 }, + { "ruins_lockg01b.lmp", 2713 }, + { "ruins_lockg01c.lmp", 2713 }, + { "ruins_lockg01d.lmp", 2713 }, + { "ruins_lockg01e.lmp", 3826 }, + { "ruins_locks00.lmp", 194261 }, + { "ruins_locks00a.lmp", 2767 }, + { "ruins_locks00b.lmp", 1394 }, + { "ruins_locks00c.lmp", 7258 }, + { "ruins_locks00d.lmp", 764 }, + { "ruins_locks00e.lmp", 335 }, + { "ruins_locks01.lmp", 507264 }, + { "ruins_locks01a.lmp", 5360 }, + { "ruins_locks01b.lmp", 6936 }, + { "ruins_locks01c.lmp", 4618 }, + { "ruins_locks01d.lmp", 4618 }, + { "ruins_locks01e.lmp", 4618 }, + { "ruinssecret.lmp", 66694 }, + { "sanctum.lmp", 6476194 }, + { "shop-roomgen00.lmp", 11844 }, + { "shop-roomgen01.lmp", 9917 }, + { "shop-roomgen02.lmp", 40191 }, + { "shop-roomgen02a.lmp", 4754 }, + { "shop-roomgen02b.lmp", 5250 }, + { "shop-roomgen02c.lmp", 5481 }, + { "shop-roomgen02d.lmp", 4467 }, + { "shop-roomgen02e.lmp", 4575 }, + { "shop-roomgen02f.lmp", 4025 }, + { "shop-roomgen03.lmp", 5324 }, + { "shop-roomgen04.lmp", 6945 }, + { "shop-roomgen05.lmp", 12261 }, + { "shop-roomgen06.lmp", 12925 }, + { "shop-roomgen07.lmp", 10729 }, + { "shop-roomgen08.lmp", 12489 }, + { "shop-roomgen09.lmp", 9209 }, + { "shop-roomgen10.lmp", 11533 }, + { "shop00.lmp", 8172 }, + { "shop01.lmp", 14016 }, + { "shop02.lmp", 39289 }, + { "shop02a.lmp", 4905 }, + { "shop02b.lmp", 5250 }, + { "shop02c.lmp", 5481 }, + { "shop02d.lmp", 4467 }, + { "shop02e.lmp", 4962 }, + { "shop02f.lmp", 4025 }, + { "shop03.lmp", 9675 }, + { "shop04.lmp", 15493 }, + { "shop05.lmp", 13570 }, + { "shop06.lmp", 16915 }, + { "shop07.lmp", 13185 }, + { "shop08.lmp", 18483 }, + { "shop09.lmp", 10094 }, + { "shop10.lmp", 16181 }, + { "sokoban.lmp", 140500 }, + { "start.lmp", 648033 }, + { "swamp.lmp", 124034 }, + { "swamp00.lmp", 13629 }, + { "swamp01.lmp", 29459 }, + { "swamp01a.lmp", 5303 }, + { "swamp01b.lmp", 6563 }, + { "swamp01c.lmp", 10334 }, + { "swamp01d.lmp", 5571 }, + { "swamp01e.lmp", 4060 }, + { "swamp02.lmp", 39297 }, + { "swamp02a.lmp", 9131 }, + { "swamp02b.lmp", 11457 }, + { "swamp02c.lmp", 8877 }, + { "swamp02d.lmp", 7327 }, + { "swamp02e.lmp", 4203 }, + { "swamp03.lmp", 35148 }, + { "swamp03a.lmp", 6837 }, + { "swamp03b.lmp", 25871 }, + { "swamp03c.lmp", 3520 }, + { "swamp03d.lmp", 7686 }, + { "swamp03e.lmp", 15932 }, + { "swamp04.lmp", 51827 }, + { "swamp04a.lmp", 6931 }, + { "swamp04b.lmp", 5875 }, + { "swamp04c.lmp", 4610 }, + { "swamp04d.lmp", 7818 }, + { "swamp04e.lmp", 18431 }, + { "swamp05.lmp", 20558 }, + { "swamp05a.lmp", 2611 }, + { "swamp05b.lmp", 2966 }, + { "swamp05c.lmp", 5344 }, + { "swamp05d.lmp", 1610 }, + { "swamp05e.lmp", 2746 }, + { "swamp06.lmp", 60048 }, + { "swamp06a.lmp", 12354 }, + { "swamp06b.lmp", 31135 }, + { "swamp06c.lmp", 50761 }, + { "swamp06d.lmp", 60075 }, + { "swamp06e.lmp", 6324 }, + { "swamp07.lmp", 91130 }, + { "swamp07a.lmp", 12340 }, + { "swamp07b.lmp", 7290 }, + { "swamp07c.lmp", 20314 }, + { "swamp07d.lmp", 74954 }, + { "swamp07e.lmp", 43344 }, + { "swamp08.lmp", 31288 }, + { "swamp08a.lmp", 3734 }, + { "swamp08b.lmp", 5429 }, + { "swamp08c.lmp", 9406 }, + { "swamp08d.lmp", 31893 }, + { "swamp08e.lmp", 2913 }, + { "swamp09.lmp", 29565 }, + { "swamp09a.lmp", 1516 }, + { "swamp09b.lmp", 6762 }, + { "swamp09c.lmp", 6705 }, + { "swamp09d.lmp", 4955 }, + { "swamp09e.lmp", 3428 }, + { "swamp10.lmp", 27653 }, + { "swamp10a.lmp", 2322 }, + { "swamp10b.lmp", 5975 }, + { "swamp10c.lmp", 5260 }, + { "swamp10d.lmp", 6911 }, + { "swamp10e.lmp", 2344 }, + { "swamp10f.lmp", 3680 }, + { "swamp11.lmp", 64926 }, + { "swamp11a.lmp", 6851 }, + { "swamp11b.lmp", 11835 }, + { "swamp11c.lmp", 26869 }, + { "swamp11d.lmp", 46682 }, + { "swamp11e.lmp", 43713 }, + { "swamp12.lmp", 52448 }, + { "swamp12a.lmp", 9704 }, + { "swamp12b.lmp", 17788 }, + { "swamp12c.lmp", 8585 }, + { "swamp12d.lmp", 12097 }, + { "swamp12e.lmp", 13987 }, + { "swamp13.lmp", 30293 }, + { "swamp13a.lmp", 6911 }, + { "swamp13b.lmp", 10109 }, + { "swamp13c.lmp", 14051 }, + { "swamp13d.lmp", 5933 }, + { "swamp13e.lmp", 6334 }, + { "swamp14.lmp", 41242 }, + { "swamp14a.lmp", 9092 }, + { "swamp14b.lmp", 4317 }, + { "swamp14c.lmp", 14974 }, + { "swamp14d.lmp", 19525 }, + { "swamp14e.lmp", 6608 }, + { "swamp15.lmp", 352 }, + { "swamp16.lmp", 14028 }, + { "swamp16a.lmp", 1377 }, + { "swamp16b.lmp", 1411 }, + { "swamp16c.lmp", 856 }, + { "swamp16d.lmp", 2052 }, + { "swamp16e.lmp", 1154 }, + { "swamp17.lmp", 31463 }, + { "swamp17a.lmp", 5507 }, + { "swamp17b.lmp", 10976 }, + { "swamp17c.lmp", 9420 }, + { "swamp17d.lmp", 6550 }, + { "swamp17e.lmp", 4447 }, + { "swamp18.lmp", 18634 }, + { "swamp18a.lmp", 934 }, + { "swamp18b.lmp", 3264 }, + { "swamp18c.lmp", 5153 }, + { "swamp18d.lmp", 2974 }, + { "swamp18e.lmp", 1429 }, + { "swamp18f.lmp", 3207 }, + { "swamp18g.lmp", 3646 }, + { "swamp19.lmp", 20258 }, + { "swamp19a.lmp", 3302 }, + { "swamp19b.lmp", 1808 }, + { "swamp19c.lmp", 3136 }, + { "swamp19d.lmp", 3705 }, + { "swamp19e.lmp", 4800 }, + { "swamp20.lmp", 33017 }, + { "swamp20a.lmp", 4569 }, + { "swamp20b.lmp", 10215 }, + { "swamp20c.lmp", 5654 }, + { "swamp20d.lmp", 26165 }, + { "swamp20e.lmp", 6173 }, + { "swamp21.lmp", 16295 }, + { "swamp21a.lmp", 1887 }, + { "swamp21b.lmp", 3323 }, + { "swamp21c.lmp", 6881 }, + { "swamp21d.lmp", 25271 }, + { "swamp21e.lmp", 3401 }, + { "swamp22.lmp", 55275 }, + { "swamp22a.lmp", 13094 }, + { "swamp22b.lmp", 5215 }, + { "swamp22c.lmp", 16634 }, + { "swamp22d.lmp", 8710 }, + { "swamp22e.lmp", 26057 }, + { "swamp23.lmp", 63299 }, + { "swamp23a.lmp", 9039 }, + { "swamp23b.lmp", 24466 }, + { "swamp23c.lmp", 26586 }, + { "swamp23d.lmp", 8888 }, + { "swamp23e.lmp", 14992 }, + { "swamp24.lmp", 36395 }, + { "swamp24a.lmp", 5045 }, + { "swamp24b.lmp", 4267 }, + { "swamp24c.lmp", 5682 }, + { "swamp24d.lmp", 7998 }, + { "swamp24e.lmp", 12981 }, + { "swamp25.lmp", 34758 }, + { "swamp25a.lmp", 5060 }, + { "swamp25b.lmp", 6237 }, + { "swamp25c.lmp", 12252 }, + { "swamp25d.lmp", 5226 }, + { "swamp25e.lmp", 5717 }, + { "swamp25f.lmp", 10354 }, + { "swamp26.lmp", 40227 }, + { "swamp26a.lmp", 3747 }, + { "swamp26b.lmp", 2398 }, + { "swamp26c.lmp", 1005 }, + { "swamp26d.lmp", 3129 }, + { "swamp26e.lmp", 1262 }, + { "swamp27.lmp", 42021 }, + { "swamp27a.lmp", 4975 }, + { "swamp27b.lmp", 3725 }, + { "swamp27c.lmp", 2059 }, + { "swamp27d.lmp", 5312 }, + { "swamp27e.lmp", 4425 }, + { "swamp28.lmp", 6067 }, + { "swamp28a.lmp", 676 }, + { "swamp28b.lmp", 524 }, + { "swamp28c.lmp", 2016 }, + { "swamp28d.lmp", 1353 }, + { "swamp28e.lmp", 1457 }, + { "swamp28f.lmp", 338 }, + { "swamp29.lmp", 6111 }, + { "swamp29a.lmp", 591 }, + { "swamp29b.lmp", 596 }, + { "swamp29c.lmp", 1336 }, + { "swamp29d.lmp", 2619 }, + { "swamp29e.lmp", 1391 }, + { "swamp29f.lmp", 1249 }, + { "swamp29g.lmp", 1249 }, + { "swamp30.lmp", 54169 }, + { "swamp30a.lmp", 17250 }, + { "swamp30b.lmp", 69206 }, + { "swamp30c.lmp", 20925 }, + { "swamp30d.lmp", 18887 }, + { "swamp30e.lmp", 28043 }, + { "swamp31.lmp", 24287 }, + { "swamp31a.lmp", 7582 }, + { "swamp31b.lmp", 9169 }, + { "swamp31c.lmp", 2026 }, + { "swamp31d.lmp", 5365 }, + { "swamp31e.lmp", 35837 }, + { "swamp32.lmp", 30796 }, + { "swamp32a.lmp", 5496 }, + { "swamp32b.lmp", 6551 }, + { "swamp32c.lmp", 4638 }, + { "swamp32d.lmp", 6124 }, + { "swamp32e.lmp", 9867 }, + { "swamp33.lmp", 31993 }, + { "swamp33a.lmp", 9437 }, + { "swamp33b.lmp", 15085 }, + { "swamp33c.lmp", 6699 }, + { "swamp33d.lmp", 7910 }, + { "swamp33e.lmp", 10948 }, + { "swamp34.lmp", 36795 }, + { "swamp34a.lmp", 6929 }, + { "swamp34b.lmp", 68279 }, + { "swamp34c.lmp", 12514 }, + { "swamp34d.lmp", 5546 }, + { "swamp34e.lmp", 17278 }, + { "swamp_lockb00.lmp", 23909 }, + { "swamp_lockb00a.lmp", 2629 }, + { "swamp_lockb00b.lmp", 3570 }, + { "swamp_lockb00c.lmp", 4100 }, + { "swamp_lockb00d.lmp", 3570 }, + { "swamp_lockb00e.lmp", 2711 }, + { "swamp_lockb01.lmp", 7499 }, + { "swamp_lockb01a.lmp", 372 }, + { "swamp_lockb01b.lmp", 738 }, + { "swamp_lockb01c.lmp", 738 }, + { "swamp_lockb01d.lmp", 738 }, + { "swamp_lockb01e.lmp", 594 }, + { "swamp_lockg00.lmp", 235696 }, + { "swamp_lockg00a.lmp", 1592 }, + { "swamp_lockg00b.lmp", 1604 }, + { "swamp_lockg00c.lmp", 1241 }, + { "swamp_lockg00d.lmp", 1253 }, + { "swamp_lockg00e.lmp", 1604 }, + { "swamp_lockg01.lmp", 81081 }, + { "swamp_lockg01a.lmp", 2586 }, + { "swamp_lockg01b.lmp", 2586 }, + { "swamp_lockg01c.lmp", 2586 }, + { "swamp_lockg01d.lmp", 3455 }, + { "swamp_lockg01e.lmp", 2586 }, + { "swamp_locks00.lmp", 36851 }, + { "swamp_locks00a.lmp", 11806 }, + { "swamp_locks00b.lmp", 8958 }, + { "swamp_locks00c.lmp", 8958 }, + { "swamp_locks00d.lmp", 6773 }, + { "swamp_locks00e.lmp", 8390 }, + { "swamp_locks01.lmp", 41985 }, + { "swamp_locks01a.lmp", 883 }, + { "swamp_locks01b.lmp", 883 }, + { "swamp_locks01c.lmp", 883 }, + { "swamp_locks01d.lmp", 883 }, + { "swamp_locks01e.lmp", 883 }, + { "swampsecret.lmp", 24027 }, + { "swamptolabyrinth.lmp", 213824 }, + { "temple.lmp", 7248698 }, + { "tutorial1.lmp", 709674 }, + { "tutorial10.lmp", 1504767 }, + { "tutorial2.lmp", 1313718 }, + { "tutorial3.lmp", 1235301 }, + { "tutorial4.lmp", 1926157 }, + { "tutorial5.lmp", 961826 }, + { "tutorial6.lmp", 2024462 }, + { "tutorial7.lmp", 4059721 }, + { "tutorial8.lmp", 1721791 }, + { "tutorial9.lmp", 3166055 }, + { "tutorial_hub.lmp", 66168004 }, + { "underworld.lmp", 253866 }, + { "underworld00.lmp", 26034 }, + { "underworld01.lmp", 63364 }, + { "underworld01a.lmp", 15722 }, + { "underworld01b.lmp", 23578 }, + { "underworld01c.lmp", 16338 }, + { "underworld01d.lmp", 13107 }, + { "underworld01e.lmp", 21000 }, + { "underworld02.lmp", 35793 }, + { "underworld02a.lmp", 7965 }, + { "underworld02b.lmp", 11142 }, + { "underworld02c.lmp", 12186 }, + { "underworld02d.lmp", 9758 }, + { "underworld02e.lmp", 10867 }, + { "underworld03.lmp", 7801 }, + { "underworld03a.lmp", 739 }, + { "underworld03b.lmp", 1458 }, + { "underworld03c.lmp", 2290 }, + { "underworld03d.lmp", 1345 }, + { "underworld03e.lmp", 1423 }, + { "underworld04.lmp", 13996 }, + { "underworld04a.lmp", 2482 }, + { "underworld04b.lmp", 2530 }, + { "underworld04c.lmp", 3130 }, + { "underworld04d.lmp", 3326 }, + { "underworld04e.lmp", 3949 }, + { "underworld05.lmp", 40061 }, + { "underworld05a.lmp", 6687 }, + { "underworld05b.lmp", 8086 }, + { "underworld05c.lmp", 15740 }, + { "underworld05d.lmp", 12373 }, + { "underworld05e.lmp", 11278 }, + { "underworld06.lmp", 40061 }, + { "underworld06a.lmp", 9012 }, + { "underworld06b.lmp", 9570 }, + { "underworld06c.lmp", 11087 }, + { "underworld06d.lmp", 21558 }, + { "underworld06e.lmp", 12493 }, + { "underworld07.lmp", 27085 }, + { "underworld07a.lmp", 6214 }, + { "underworld07b.lmp", 8553 }, + { "underworld07c.lmp", 6616 }, + { "underworld07d.lmp", 46782 }, + { "underworld07e.lmp", 5087 }, + { "underworld08.lmp", 89325 }, + { "underworld08a.lmp", 14990 }, + { "underworld08b.lmp", 23417 }, + { "underworld08c.lmp", 27050 }, + { "underworld08d.lmp", 38494 }, + { "underworld08e.lmp", 66467 }, + { "underworld09.lmp", 76902 }, + { "underworld09a.lmp", 29965 }, + { "underworld09b.lmp", 44375 }, + { "underworld09c.lmp", 35307 }, + { "underworld09d.lmp", 27376 }, + { "underworld09e.lmp", 30058 }, + { "underworld10.lmp", 119996 }, + { "underworld10a.lmp", 135977 }, + { "underworld10b.lmp", 47378 }, + { "underworld10c.lmp", 37212 }, + { "underworld10d.lmp", 41839 }, + { "underworld10e.lmp", 41223 }, + { "underworld10f.lmp", 50632 }, + { "underworld11.lmp", 118540 }, + { "underworld11a.lmp", 12213 }, + { "underworld11b.lmp", 25786 }, + { "underworld11c.lmp", 13884 }, + { "underworld11d.lmp", 14126 }, + { "underworld11e.lmp", 8680 }, + { "underworld11f.lmp", 17197 }, + { "underworld12.lmp", 55688 }, + { "underworld12a.lmp", 12590 }, + { "underworld12b.lmp", 21302 }, + { "underworld12c.lmp", 14416 }, + { "underworld12d.lmp", 21990 }, + { "underworld12e.lmp", 11802 }, + { "underworld13.lmp", 55688 }, + { "underworld13a.lmp", 12987 }, + { "underworld13b.lmp", 14430 }, + { "underworld13c.lmp", 15534 }, + { "underworld13d.lmp", 31839 }, + { "underworld13e.lmp", 23587 }, + { "underworld14.lmp", 59545 }, + { "underworld14a.lmp", 10537 }, + { "underworld14b.lmp", 61997 }, + { "underworld14c.lmp", 15043 }, + { "underworld14d.lmp", 7921 }, + { "underworld14e.lmp", 22800 }, + { "underworld15.lmp", 119344 }, + { "underworld15a.lmp", 48251 }, + { "underworld15b.lmp", 48500 }, + { "underworld15c.lmp", 45549 }, + { "underworld15d.lmp", 22683 }, + { "underworld15e.lmp", 88967 }, + { "underworld16.lmp", 7801 }, + { "underworld16a.lmp", 1213 }, + { "underworld16b.lmp", 1486 }, + { "underworld16c.lmp", 2136 }, + { "underworld16d.lmp", 1496 }, + { "underworld16e.lmp", 2136 }, + { "underworld17.lmp", 77818 }, + { "underworld17a.lmp", 21652 }, + { "underworld17b.lmp", 40598 }, + { "underworld17c.lmp", 35498 }, + { "underworld17d.lmp", 16492 }, + { "underworld17e.lmp", 18237 }, + { "underworld18.lmp", 130922 }, + { "underworld18a.lmp", 23389 }, + { "underworld18b.lmp", 37706 }, + { "underworld18c.lmp", 26004 }, + { "underworld18d.lmp", 24737 }, + { "underworld18e.lmp", 19698 }, + { "underworld19.lmp", 147747 }, + { "underworld19a.lmp", 77694 }, + { "underworld19b.lmp", 47551 }, + { "underworld19c.lmp", 73591 }, + { "underworld19d.lmp", 48973 }, + { "underworld19e.lmp", 111087 }, + { "underworld20.lmp", 147553 }, + { "underworld20a.lmp", 77981 }, + { "underworld20b.lmp", 55526 }, + { "underworld20c.lmp", 203037 }, + { "underworld20d.lmp", 43029 }, + { "underworld20e.lmp", 51511 }, + { "underworld21.lmp", 144910 }, + { "underworld21a.lmp", 94963 }, + { "underworld21b.lmp", 76444 }, + { "underworld21c.lmp", 48584 }, + { "underworld21d.lmp", 48359 }, + { "underworld21e.lmp", 30448 }, + { "underworld22.lmp", 143298 }, + { "underworld22a.lmp", 64856 }, + { "underworld22b.lmp", 71046 }, + { "underworld22c.lmp", 59299 }, + { "underworld22d.lmp", 42229 }, + { "underworld22e.lmp", 36337 }, + { "underworld23.lmp", 27113 }, + { "underworld23a.lmp", 4307 }, + { "underworld23b.lmp", 6289 }, + { "underworld23c.lmp", 9948 }, + { "underworld23d.lmp", 10823 }, + { "underworld23e.lmp", 9851 }, + { "underworld24.lmp", 27113 }, + { "underworld24a.lmp", 4337 }, + { "underworld24b.lmp", 6629 }, + { "underworld24c.lmp", 7018 }, + { "underworld24d.lmp", 5098 }, + { "underworld24e.lmp", 7379 }, + { "underworld25.lmp", 60508 }, + { "underworld25a.lmp", 39942 }, + { "underworld25b.lmp", 16973 }, + { "underworld25c.lmp", 13818 }, + { "underworld25d.lmp", 20478 }, + { "underworld25e.lmp", 13543 }, + { "underworld26.lmp", 11893 }, + { "underworld26a.lmp", 2706 }, + { "underworld26b.lmp", 3206 }, + { "underworld26c.lmp", 3087 }, + { "underworld26d.lmp", 3343 }, + { "underworld26e.lmp", 3667 }, + { "underworld27.lmp", 122626 }, + { "underworld27a.lmp", 58537 }, + { "underworld27b.lmp", 44335 }, + { "underworld27c.lmp", 22451 }, + { "underworld27d.lmp", 42906 }, + { "underworld27e.lmp", 44300 }, + { "underworld28.lmp", 32847 }, + { "underworld28a.lmp", 12273 }, + { "underworld28b.lmp", 7472 }, + { "underworld28c.lmp", 7944 }, + { "underworld28d.lmp", 5599 }, + { "underworld28e.lmp", 8830 }, + { "underworld29.lmp", 40133 }, + { "underworld29a.lmp", 15495 }, + { "underworld29b.lmp", 14568 }, + { "underworld29c.lmp", 12048 }, + { "underworld29d.lmp", 16846 }, + { "underworld29e.lmp", 17605 }, + { "underworld30.lmp", 112313 }, + { "underworld30a.lmp", 97910 }, + { "underworld30b.lmp", 190872 }, + { "underworld30c.lmp", 47525 }, + { "underworld30d.lmp", 49543 }, + { "underworld30e.lmp", 46600 }, + { "underworld_lockg00.lmp", 66751 }, + { "underworld_lockg00a.lmp", 4372 }, + { "underworld_lockg00b.lmp", 3164 }, + { "underworld_lockg00c.lmp", 4743 }, + { "underworld_lockg00d.lmp", 4743 }, + { "underworld_lockg00e.lmp", 4025 }, + { "underworld_lockg01.lmp", 74650 }, + { "underworld_lockg01a.lmp", 39260 }, + { "underworld_lockg01b.lmp", 35938 }, + { "underworld_lockg01c.lmp", 39260 }, + { "underworld_lockg01d.lmp", 35938 }, + { "underworld_lockg01e.lmp", 35938 }, + { "underworld_locks00.lmp", 61078 }, + { "underworld_locks00a.lmp", 4679 }, + { "underworld_locks00b.lmp", 3210 }, + { "underworld_locks00c.lmp", 3210 }, + { "underworld_locks00d.lmp", 4679 }, + { "underworld_locks00e.lmp", 4475 }, + { "underworld_locks00f.lmp", 3750 }, + { "underworld_locks01.lmp", 281717 }, + { "underworld_locks01a.lmp", 2231 }, + { "underworld_locks01b.lmp", 2111 }, + { "underworld_locks01c.lmp", 2231 }, + { "underworld_locks01d.lmp", 2885 }, + { "underworld_locks01e.lmp", 2987 }, + { "warpzone.lmp", 3133088 } }; const std::vector officialLevelsTxtOrder = @@ -1738,9 +2268,17 @@ voxel_t* loadVoxel(char* filename) { return nullptr; } + Sint32 header = 0; + file->read(&header, sizeof(Sint32), 1); + if ( header == 542658390 ) + { + FileIO::close(file); + assert(false && "incorrect .vox file format, check log.txt"); + printlog("error: loadVoxel file: %s is using magicavoxel .vox file format, export as slab .vox instead!", filename); + return nullptr; + } model = (voxel_t*)malloc(sizeof(voxel_t)); - model->sizex = 0; - file->read(&model->sizex, sizeof(Sint32), 1); + model->sizex = header; model->sizey = 0; file->read(&model->sizey, sizeof(Sint32), 1); model->sizez = 0; @@ -1769,6 +2307,7 @@ voxel_t* loadVoxel(char* filename) constexpr float hellAmbience = 32.f; #ifndef EDITOR static ConsoleVariable cvar_hell_ambience("/hell_ambience", hellAmbience); +static ConsoleVariable cvar_map_ambience("/map_ambience", { 0.f, 0.f, 0.f, 0.f }); #endif /*------------------------------------------------------------------------------- @@ -1855,7 +2394,22 @@ int loadMap(const char* filename2, map_t* destmap, list_t* entlist, list_t* crea // read map version number fp->read(valid_data, sizeof(char), strlen("BARONY LMPV2.0")); - if ( strncmp(valid_data, "BARONY LMPV2.9", strlen("BARONY LMPV2.0")) == 0 ) + if ( strncmp(valid_data, "BARONY LMPV3.2", strlen("BARONY LMPV2.0")) == 0 ) + { + // floor deco walls + editorVersion = 32; + } + else if ( strncmp(valid_data, "BARONY LMPV3.1", strlen("BARONY LMPV2.0")) == 0 ) + { + // wall lock fix + editorVersion = 31; + } + else if ( strncmp(valid_data, "BARONY LMPV3.0", strlen("BARONY LMPV2.0")) == 0 ) + { + // light source rgb + editorVersion = 30; + } + else if ( strncmp(valid_data, "BARONY LMPV2.9", strlen("BARONY LMPV2.0")) == 0 ) { // V2.9 version of editor - chest mimic chance, and gates, pressure plate triggers editorVersion = 29; @@ -1955,6 +2509,7 @@ int loadMap(const char* filename2, map_t* destmap, list_t* entlist, list_t* crea list_FreeAll(map.worldUI); } destmap->liquidSfxPlayedTiles.clear(); + destmap->tileAttributes.clear(); } if ( destmap->tiles != nullptr ) { @@ -2069,415 +2624,449 @@ int loadMap(const char* filename2, map_t* destmap, list_t* entlist, list_t* crea { fp->read(&sprite, sizeof(Sint32), 1); entity = newEntity(sprite, 0, entlist, nullptr); //TODO: Figure out when we need to assign an entity to the global monster list. And do it! - switch( editorVersion ) - { case 1: - // V1.0 of editor version + if ( editorVersion == 1 ) + { + // V1.0 of editor version switch ( checkSpriteType(sprite) ) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - setSpriteAttributes(entity, nullptr, nullptr); - break; - default: - break; - } - break; + case 1: case 2: - case 21: - case 22: - case 23: - case 24: - case 25: - case 26: - case 27: - case 28: - case 29: - // V2.0+ of editor version - switch ( checkSpriteType(sprite) ) - { - case 1: - if ( multiplayer != CLIENT ) - { - // need to give the entity its list stuff. - // create an empty first node for traversal purposes - node_t* node2 = list_AddNodeFirst(&entity->children); - node2->element = NULL; - node2->deconstructor = &emptyDeconstructor; - - myStats = new Stat(entity->sprite); - node2 = list_AddNodeLast(&entity->children); - node2->element = myStats; - node2->size = sizeof(myStats); - node2->deconstructor = &statDeconstructor; - - sex_t dummyVar = MALE; - // we don't actually embed the sex from the editor - // advance the fp since we read in 0 always. - // otherwise it would overwrite the value of a handplaced succubus or a certain icey lich. - // certainly were a lot of male adventurers locked in cells... - fp->read(&dummyVar, sizeof(sex_t), 1); - fp->read(&myStats->name, sizeof(char[128]), 1); - fp->read(&myStats->HP, sizeof(Sint32), 1); - fp->read(&myStats->MAXHP, sizeof(Sint32), 1); - fp->read(&myStats->OLDHP, sizeof(Sint32), 1); - fp->read(&myStats->MP, sizeof(Sint32), 1); - fp->read(&myStats->MAXMP, sizeof(Sint32), 1); - fp->read(&myStats->STR, sizeof(Sint32), 1); - fp->read(&myStats->DEX, sizeof(Sint32), 1); - fp->read(&myStats->CON, sizeof(Sint32), 1); - fp->read(&myStats->INT, sizeof(Sint32), 1); - fp->read(&myStats->PER, sizeof(Sint32), 1); - fp->read(&myStats->CHR, sizeof(Sint32), 1); - fp->read(&myStats->LVL, sizeof(Sint32), 1); - fp->read(&myStats->GOLD, sizeof(Sint32), 1); - - fp->read(&myStats->RANDOM_MAXHP, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_HP, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_MAXMP, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_MP, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_STR, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_CON, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_DEX, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_INT, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_PER, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_CHR, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_LVL, sizeof(Sint32), 1); - fp->read(&myStats->RANDOM_GOLD, sizeof(Sint32), 1); - - if ( editorVersion >= 22 ) - { - fp->read(&myStats->EDITOR_ITEMS, sizeof(Sint32), ITEM_SLOT_NUM); - } - else - { - // read old map formats - fp->read(&myStats->EDITOR_ITEMS, sizeof(Sint32), 96); - } - fp->read(&myStats->MISC_FLAGS, sizeof(Sint32), 32); - } - //Read dummy values to move fp for the client - else - { - dummyStats = new Stat(entity->sprite); - fp->read(&dummyStats->sex, sizeof(sex_t), 1); - fp->read(&dummyStats->name, sizeof(char[128]), 1); - fp->read(&dummyStats->HP, sizeof(Sint32), 1); - fp->read(&dummyStats->MAXHP, sizeof(Sint32), 1); - fp->read(&dummyStats->OLDHP, sizeof(Sint32), 1); - fp->read(&dummyStats->MP, sizeof(Sint32), 1); - fp->read(&dummyStats->MAXMP, sizeof(Sint32), 1); - fp->read(&dummyStats->STR, sizeof(Sint32), 1); - fp->read(&dummyStats->DEX, sizeof(Sint32), 1); - fp->read(&dummyStats->CON, sizeof(Sint32), 1); - fp->read(&dummyStats->INT, sizeof(Sint32), 1); - fp->read(&dummyStats->PER, sizeof(Sint32), 1); - fp->read(&dummyStats->CHR, sizeof(Sint32), 1); - fp->read(&dummyStats->LVL, sizeof(Sint32), 1); - fp->read(&dummyStats->GOLD, sizeof(Sint32), 1); - - fp->read(&dummyStats->RANDOM_MAXHP, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_HP, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_MAXMP, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_MP, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_STR, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_CON, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_DEX, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_INT, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_PER, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_CHR, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_LVL, sizeof(Sint32), 1); - fp->read(&dummyStats->RANDOM_GOLD, sizeof(Sint32), 1); - - if ( editorVersion >= 22 ) - { - fp->read(&dummyStats->EDITOR_ITEMS, sizeof(Sint32), ITEM_SLOT_NUM); - } - else - { - fp->read(&dummyStats->EDITOR_ITEMS, sizeof(Sint32), 96); - } - fp->read(&dummyStats->MISC_FLAGS, sizeof(Sint32), 32); - delete dummyStats; - } - break; - case 2: - if ( editorVersion >= 29 ) - { - fp->read(&entity->yaw, sizeof(real_t), 1); - fp->read(&entity->skill[9], sizeof(Sint32), 1); - fp->read(&entity->chestLocked, sizeof(Sint32), 1); - fp->read(&entity->chestMimicChance, sizeof(Sint32), 1); - } - else - { - setSpriteAttributes(entity, nullptr, nullptr); - fp->read(&entity->yaw, sizeof(real_t), 1); - fp->read(&entity->skill[9], sizeof(Sint32), 1); - fp->read(&entity->chestLocked, sizeof(Sint32), 1); - } - break; - case 3: - fp->read(&entity->skill[10], sizeof(Sint32), 1); - fp->read(&entity->skill[11], sizeof(Sint32), 1); - fp->read(&entity->skill[12], sizeof(Sint32), 1); - fp->read(&entity->skill[13], sizeof(Sint32), 1); - fp->read(&entity->skill[15], sizeof(Sint32), 1); + case 3: + case 4: + case 5: + case 6: + case 7: + setSpriteAttributes(entity, nullptr, nullptr); + break; + default: + break; + } + } + else + { + // V2.0+ of editor version + switch ( checkSpriteType(sprite) ) + { + case 1: + if ( multiplayer != CLIENT ) + { + // need to give the entity its list stuff. + // create an empty first node for traversal purposes + node_t* node2 = list_AddNodeFirst(&entity->children); + node2->element = NULL; + node2->deconstructor = &emptyDeconstructor; + + myStats = new Stat(entity->sprite); + node2 = list_AddNodeLast(&entity->children); + node2->element = myStats; + node2->size = sizeof(myStats); + node2->deconstructor = &statDeconstructor; + + sex_t dummyVar = MALE; + // we don't actually embed the sex from the editor + // advance the fp since we read in 0 always. + // otherwise it would overwrite the value of a handplaced succubus or a certain icey lich. + // certainly were a lot of male adventurers locked in cells... + fp->read(&dummyVar, sizeof(sex_t), 1); + fp->read(&myStats->name, sizeof(char[128]), 1); + fp->read(&myStats->HP, sizeof(Sint32), 1); + fp->read(&myStats->MAXHP, sizeof(Sint32), 1); + fp->read(&myStats->OLDHP, sizeof(Sint32), 1); + fp->read(&myStats->MP, sizeof(Sint32), 1); + fp->read(&myStats->MAXMP, sizeof(Sint32), 1); + fp->read(&myStats->STR, sizeof(Sint32), 1); + fp->read(&myStats->DEX, sizeof(Sint32), 1); + fp->read(&myStats->CON, sizeof(Sint32), 1); + fp->read(&myStats->INT, sizeof(Sint32), 1); + fp->read(&myStats->PER, sizeof(Sint32), 1); + fp->read(&myStats->CHR, sizeof(Sint32), 1); + fp->read(&myStats->LVL, sizeof(Sint32), 1); + fp->read(&myStats->GOLD, sizeof(Sint32), 1); + + fp->read(&myStats->RANDOM_MAXHP, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_HP, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_MAXMP, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_MP, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_STR, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_CON, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_DEX, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_INT, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_PER, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_CHR, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_LVL, sizeof(Sint32), 1); + fp->read(&myStats->RANDOM_GOLD, sizeof(Sint32), 1); + if ( editorVersion >= 22 ) { - fp->read(&entity->skill[16], sizeof(Sint32), 1); - } - break; - case 4: - fp->read(&entity->skill[0], sizeof(Sint32), 1); - fp->read(&entity->skill[1], sizeof(Sint32), 1); - fp->read(&entity->skill[2], sizeof(Sint32), 1); - fp->read(&entity->skill[3], sizeof(Sint32), 1); - fp->read(&entity->skill[4], sizeof(Sint32), 1); - fp->read(&entity->skill[5], sizeof(Sint32), 1); - if ( editorVersion >= 29 ) - { - fp->read(&entity->skill[9], sizeof(Sint32), 1); - } - break; - case 5: - fp->read(&entity->yaw, sizeof(real_t), 1); - fp->read(&entity->crystalNumElectricityNodes, sizeof(Sint32), 1); - fp->read(&entity->crystalTurnReverse, sizeof(Sint32), 1); - fp->read(&entity->crystalSpellToActivate, sizeof(Sint32), 1); - break; - case 6: - fp->read(&entity->leverTimerTicks, sizeof(Sint32), 1); - break; - case 7: - if ( editorVersion >= 24 ) - { - fp->read(&entity->boulderTrapRefireAmount, sizeof(Sint32), 1); - fp->read(&entity->boulderTrapRefireDelay, sizeof(Sint32), 1); - fp->read(&entity->boulderTrapPreDelay, sizeof(Sint32), 1); + fp->read(&myStats->EDITOR_ITEMS, sizeof(Sint32), ITEM_SLOT_NUM); } else { - setSpriteAttributes(entity, nullptr, nullptr); + // read old map formats + fp->read(&myStats->EDITOR_ITEMS, sizeof(Sint32), 96); } - break; - case 8: - fp->read(&entity->pedestalOrbType, sizeof(Sint32), 1); - fp->read(&entity->pedestalHasOrb, sizeof(Sint32), 1); - fp->read(&entity->pedestalInvertedPower, sizeof(Sint32), 1); - fp->read(&entity->pedestalInGround, sizeof(Sint32), 1); - fp->read(&entity->pedestalLockOrb, sizeof(Sint32), 1); - break; - case 9: - fp->read(&entity->teleporterX, sizeof(Sint32), 1); - fp->read(&entity->teleporterY, sizeof(Sint32), 1); - fp->read(&entity->teleporterType, sizeof(Sint32), 1); - break; - case 10: - if ( editorVersion >= 28 ) + fp->read(&myStats->MISC_FLAGS, sizeof(Sint32), 32); + } + //Read dummy values to move fp for the client + else + { + dummyStats = new Stat(entity->sprite); + fp->read(&dummyStats->sex, sizeof(sex_t), 1); + fp->read(&dummyStats->name, sizeof(char[128]), 1); + fp->read(&dummyStats->HP, sizeof(Sint32), 1); + fp->read(&dummyStats->MAXHP, sizeof(Sint32), 1); + fp->read(&dummyStats->OLDHP, sizeof(Sint32), 1); + fp->read(&dummyStats->MP, sizeof(Sint32), 1); + fp->read(&dummyStats->MAXMP, sizeof(Sint32), 1); + fp->read(&dummyStats->STR, sizeof(Sint32), 1); + fp->read(&dummyStats->DEX, sizeof(Sint32), 1); + fp->read(&dummyStats->CON, sizeof(Sint32), 1); + fp->read(&dummyStats->INT, sizeof(Sint32), 1); + fp->read(&dummyStats->PER, sizeof(Sint32), 1); + fp->read(&dummyStats->CHR, sizeof(Sint32), 1); + fp->read(&dummyStats->LVL, sizeof(Sint32), 1); + fp->read(&dummyStats->GOLD, sizeof(Sint32), 1); + + fp->read(&dummyStats->RANDOM_MAXHP, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_HP, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_MAXMP, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_MP, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_STR, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_CON, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_DEX, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_INT, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_PER, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_CHR, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_LVL, sizeof(Sint32), 1); + fp->read(&dummyStats->RANDOM_GOLD, sizeof(Sint32), 1); + + if ( editorVersion >= 22 ) { - fp->read(&entity->ceilingTileModel, sizeof(Sint32), 1); - fp->read(&entity->ceilingTileDir, sizeof(Sint32), 1); - fp->read(&entity->ceilingTileAllowTrap, sizeof(Sint32), 1); - fp->read(&entity->ceilingTileBreakable, sizeof(Sint32), 1); + fp->read(&dummyStats->EDITOR_ITEMS, sizeof(Sint32), ITEM_SLOT_NUM); } else { - setSpriteAttributes(entity, nullptr, nullptr); - fp->read(&entity->ceilingTileModel, sizeof(Sint32), 1); + fp->read(&dummyStats->EDITOR_ITEMS, sizeof(Sint32), 96); } - break; - case 11: - fp->read(&entity->spellTrapType, sizeof(Sint32), 1); - fp->read(&entity->spellTrapRefire, sizeof(Sint32), 1); - fp->read(&entity->spellTrapLatchPower, sizeof(Sint32), 1); - fp->read(&entity->spellTrapFloorTile, sizeof(Sint32), 1); - fp->read(&entity->spellTrapRefireRate, sizeof(Sint32), 1); - break; - case 12: - if ( entity->sprite == 60 ) // chair - { - if ( editorVersion >= 25 ) - { - fp->read(&entity->furnitureDir, sizeof(Sint32), 1); - } - else - { - // don't read data, set default. - setSpriteAttributes(entity, nullptr, nullptr); - } - } - else - { - fp->read(&entity->furnitureDir, sizeof(Sint32), 1); - } - break; - case 13: - fp->read(&entity->floorDecorationModel, sizeof(Sint32), 1); - fp->read(&entity->floorDecorationRotation, sizeof(Sint32), 1); - fp->read(&entity->floorDecorationHeightOffset, sizeof(Sint32), 1); - if ( editorVersion >= 25 ) - { - fp->read(&entity->floorDecorationXOffset, sizeof(Sint32), 1); - fp->read(&entity->floorDecorationYOffset, sizeof(Sint32), 1); - for ( int i = 8; i < 60; ++i ) - { - fp->read(&entity->skill[i], sizeof(Sint32), 1); - } - } - break; - case 14: - fp->read(&entity->soundSourceToPlay, sizeof(Sint32), 1); - fp->read(&entity->soundSourceVolume, sizeof(Sint32), 1); - fp->read(&entity->soundSourceLatchOn, sizeof(Sint32), 1); - fp->read(&entity->soundSourceDelay, sizeof(Sint32), 1); - fp->read(&entity->soundSourceOrigin, sizeof(Sint32), 1); - break; - case 15: - fp->read(&entity->lightSourceAlwaysOn, sizeof(Sint32), 1); - fp->read(&entity->lightSourceBrightness, sizeof(Sint32), 1); - fp->read(&entity->lightSourceInvertPower, sizeof(Sint32), 1); - fp->read(&entity->lightSourceLatchOn, sizeof(Sint32), 1); - fp->read(&entity->lightSourceRadius, sizeof(Sint32), 1); - fp->read(&entity->lightSourceFlicker, sizeof(Sint32), 1); - fp->read(&entity->lightSourceDelay, sizeof(Sint32), 1); - break; - case 16: + fp->read(&dummyStats->MISC_FLAGS, sizeof(Sint32), 32); + delete dummyStats; + } + break; + case 2: + if ( editorVersion >= 29 ) { - fp->read(&entity->textSourceColorRGB, sizeof(Sint32), 1); - fp->read(&entity->textSourceVariables4W, sizeof(Sint32), 1); - fp->read(&entity->textSourceDelay, sizeof(Sint32), 1); - fp->read(&entity->textSourceIsScript, sizeof(Sint32), 1); - for ( int i = 4; i < 60; ++i ) - { - fp->read(&entity->skill[i], sizeof(Sint32), 1); - } - break; + fp->read(&entity->yaw, sizeof(real_t), 1); + fp->read(&entity->skill[9], sizeof(Sint32), 1); + fp->read(&entity->chestLocked, sizeof(Sint32), 1); + fp->read(&entity->chestMimicChance, sizeof(Sint32), 1); } - case 17: - fp->read(&entity->signalInputDirection, sizeof(Sint32), 1); - fp->read(&entity->signalActivateDelay, sizeof(Sint32), 1); - fp->read(&entity->signalTimerInterval, sizeof(Sint32), 1); - fp->read(&entity->signalTimerRepeatCount, sizeof(Sint32), 1); - fp->read(&entity->signalTimerLatchInput, sizeof(Sint32), 1); - if ( editorVersion >= 29 ) - { - fp->read(&entity->signalInvertOutput, sizeof(Sint32), 1); - } - break; - case 18: - fp->read(&entity->portalCustomSprite, sizeof(Sint32), 1); - fp->read(&entity->portalCustomSpriteAnimationFrames, sizeof(Sint32), 1); - fp->read(&entity->portalCustomZOffset, sizeof(Sint32), 1); - fp->read(&entity->portalCustomLevelsToJump, sizeof(Sint32), 1); - fp->read(&entity->portalNotSecret, sizeof(Sint32), 1); - fp->read(&entity->portalCustomRequiresPower, sizeof(Sint32), 1); - for ( int i = 11; i <= 18; ++i ) - { - fp->read(&entity->skill[i], sizeof(Sint32), 1); - } - break; - case 19: + else + { + setSpriteAttributes(entity, nullptr, nullptr); + fp->read(&entity->yaw, sizeof(real_t), 1); + fp->read(&entity->skill[9], sizeof(Sint32), 1); + fp->read(&entity->chestLocked, sizeof(Sint32), 1); + } + break; + case 3: + fp->read(&entity->skill[10], sizeof(Sint32), 1); + fp->read(&entity->skill[11], sizeof(Sint32), 1); + fp->read(&entity->skill[12], sizeof(Sint32), 1); + fp->read(&entity->skill[13], sizeof(Sint32), 1); + fp->read(&entity->skill[15], sizeof(Sint32), 1); + if ( editorVersion >= 22 ) + { + fp->read(&entity->skill[16], sizeof(Sint32), 1); + } + break; + case 4: + fp->read(&entity->skill[0], sizeof(Sint32), 1); + fp->read(&entity->skill[1], sizeof(Sint32), 1); + fp->read(&entity->skill[2], sizeof(Sint32), 1); + fp->read(&entity->skill[3], sizeof(Sint32), 1); + fp->read(&entity->skill[4], sizeof(Sint32), 1); + fp->read(&entity->skill[5], sizeof(Sint32), 1); + if ( editorVersion >= 29 ) + { + fp->read(&entity->skill[9], sizeof(Sint32), 1); + } + break; + case 5: + fp->read(&entity->yaw, sizeof(real_t), 1); + fp->read(&entity->crystalNumElectricityNodes, sizeof(Sint32), 1); + fp->read(&entity->crystalTurnReverse, sizeof(Sint32), 1); + fp->read(&entity->crystalSpellToActivate, sizeof(Sint32), 1); + break; + case 6: + fp->read(&entity->leverTimerTicks, sizeof(Sint32), 1); + break; + case 7: + if ( editorVersion >= 24 ) + { + fp->read(&entity->boulderTrapRefireAmount, sizeof(Sint32), 1); + fp->read(&entity->boulderTrapRefireDelay, sizeof(Sint32), 1); + fp->read(&entity->boulderTrapPreDelay, sizeof(Sint32), 1); + } + else + { + setSpriteAttributes(entity, nullptr, nullptr); + } + break; + case 8: + fp->read(&entity->pedestalOrbType, sizeof(Sint32), 1); + fp->read(&entity->pedestalHasOrb, sizeof(Sint32), 1); + fp->read(&entity->pedestalInvertedPower, sizeof(Sint32), 1); + fp->read(&entity->pedestalInGround, sizeof(Sint32), 1); + fp->read(&entity->pedestalLockOrb, sizeof(Sint32), 1); + break; + case 9: + fp->read(&entity->teleporterX, sizeof(Sint32), 1); + fp->read(&entity->teleporterY, sizeof(Sint32), 1); + fp->read(&entity->teleporterType, sizeof(Sint32), 1); + break; + case 10: + if ( editorVersion >= 28 ) + { + fp->read(&entity->ceilingTileModel, sizeof(Sint32), 1); + fp->read(&entity->ceilingTileDir, sizeof(Sint32), 1); + fp->read(&entity->ceilingTileAllowTrap, sizeof(Sint32), 1); + fp->read(&entity->ceilingTileBreakable, sizeof(Sint32), 1); + } + else + { + setSpriteAttributes(entity, nullptr, nullptr); + fp->read(&entity->ceilingTileModel, sizeof(Sint32), 1); + } + break; + case 11: + fp->read(&entity->spellTrapType, sizeof(Sint32), 1); + fp->read(&entity->spellTrapRefire, sizeof(Sint32), 1); + fp->read(&entity->spellTrapLatchPower, sizeof(Sint32), 1); + fp->read(&entity->spellTrapFloorTile, sizeof(Sint32), 1); + fp->read(&entity->spellTrapRefireRate, sizeof(Sint32), 1); + break; + case 12: + if ( entity->sprite == 60 ) // chair + { if ( editorVersion >= 25 ) { fp->read(&entity->furnitureDir, sizeof(Sint32), 1); - fp->read(&entity->furnitureTableSpawnChairs, sizeof(Sint32), 1); - fp->read(&entity->furnitureTableRandomItemChance, sizeof(Sint32), 1); } else { // don't read data, set default. setSpriteAttributes(entity, nullptr, nullptr); } - break; - case 20: - fp->read(&entity->skill[11], sizeof(Sint32), 1); - fp->read(&entity->skill[12], sizeof(Sint32), 1); - fp->read(&entity->skill[15], sizeof(Sint32), 1); - for ( int i = 40; i <= 52; ++i ) - { - fp->read(&entity->skill[i], sizeof(Sint32), 1); - } - break; - case 21: - if ( editorVersion >= 26 ) - { - fp->read(&entity->doorForceLockedUnlocked, sizeof(Sint32), 1); - fp->read(&entity->doorDisableLockpicks, sizeof(Sint32), 1); - fp->read(&entity->doorDisableOpening, sizeof(Sint32), 1); - } - break; - case 22: - if ( editorVersion >= 26 ) - { - fp->read(&entity->gateDisableOpening, sizeof(Sint32), 1); - } - break; - case 23: - if ( editorVersion >= 26 ) - { - fp->read(&entity->playerStartDir, sizeof(Sint32), 1); - } - break; - case 24: - fp->read(&entity->statueDir, sizeof(Sint32), 1); - fp->read(&entity->statueId, sizeof(Sint32), 1); - break; - case 25: - fp->read(&entity->shrineDir, sizeof(Sint32), 1); - fp->read(&entity->shrineZ, sizeof(Sint32), 1); - if ( editorVersion >= 27 ) + } + else + { + fp->read(&entity->furnitureDir, sizeof(Sint32), 1); + } + break; + case 13: + fp->read(&entity->floorDecorationModel, sizeof(Sint32), 1); + fp->read(&entity->floorDecorationRotation, sizeof(Sint32), 1); + fp->read(&entity->floorDecorationHeightOffset, sizeof(Sint32), 1); + if ( editorVersion >= 25 ) + { + fp->read(&entity->floorDecorationXOffset, sizeof(Sint32), 1); + fp->read(&entity->floorDecorationYOffset, sizeof(Sint32), 1); + if ( editorVersion >= 32 ) { - fp->read(&entity->shrineDestXOffset, sizeof(Sint32), 1); - fp->read(&entity->shrineDestYOffset, sizeof(Sint32), 1); + fp->read(&entity->floorDecorationDestroyIfNoWall, sizeof(Sint32), 1); } - break; - case 26: - fp->read(&entity->shrineDir, sizeof(Sint32), 1); - fp->read(&entity->shrineZ, sizeof(Sint32), 1); - break; - case 27: - fp->read(&entity->colliderDecorationModel, sizeof(Sint32), 1); - fp->read(&entity->colliderDecorationRotation, sizeof(Sint32), 1); - fp->read(&entity->colliderDecorationHeightOffset, sizeof(Sint32), 1); - fp->read(&entity->colliderDecorationXOffset, sizeof(Sint32), 1); - fp->read(&entity->colliderDecorationYOffset, sizeof(Sint32), 1); - fp->read(&entity->colliderHasCollision, sizeof(Sint32), 1); - fp->read(&entity->colliderSizeX, sizeof(Sint32), 1); - fp->read(&entity->colliderSizeY, sizeof(Sint32), 1); - fp->read(&entity->colliderMaxHP, sizeof(Sint32), 1); - fp->read(&entity->colliderDiggable, sizeof(Sint32), 1); - fp->read(&entity->colliderDamageTypes, sizeof(Sint32), 1); - break; - case 28: - fp->read(&entity->signalInputDirection, sizeof(Sint32), 1); - fp->read(&entity->signalActivateDelay, sizeof(Sint32), 1); - fp->read(&entity->signalTimerInterval, sizeof(Sint32), 1); - fp->read(&entity->signalTimerRepeatCount, sizeof(Sint32), 1); - fp->read(&entity->signalTimerLatchInput, sizeof(Sint32), 1); - fp->read(&entity->signalInvertOutput, sizeof(Sint32), 1); - break; - case 29: - if ( editorVersion >= 29 ) + else { - fp->read(&entity->pressurePlateTriggerType, sizeof(Sint32), 1); + entity->floorDecorationDestroyIfNoWall = -1; } - else + for ( int i = 8; i < 60; ++i ) { - // don't read data, set default. - setSpriteAttributes(entity, nullptr, nullptr); + fp->read(&entity->skill[i], sizeof(Sint32), 1); } - break; - default: - break; + } + break; + case 14: + fp->read(&entity->soundSourceToPlay, sizeof(Sint32), 1); + fp->read(&entity->soundSourceVolume, sizeof(Sint32), 1); + fp->read(&entity->soundSourceLatchOn, sizeof(Sint32), 1); + fp->read(&entity->soundSourceDelay, sizeof(Sint32), 1); + fp->read(&entity->soundSourceOrigin, sizeof(Sint32), 1); + break; + case 15: + if ( editorVersion < 30 ) + { + // set default data for rgb + setSpriteAttributes(entity, nullptr, nullptr); + } + fp->read(&entity->lightSourceAlwaysOn, sizeof(Sint32), 1); + fp->read(&entity->lightSourceBrightness, sizeof(Sint32), 1); + fp->read(&entity->lightSourceInvertPower, sizeof(Sint32), 1); + fp->read(&entity->lightSourceLatchOn, sizeof(Sint32), 1); + fp->read(&entity->lightSourceRadius, sizeof(Sint32), 1); + fp->read(&entity->lightSourceFlicker, sizeof(Sint32), 1); + fp->read(&entity->lightSourceDelay, sizeof(Sint32), 1); + if ( editorVersion >= 30 ) + { + fp->read(&entity->lightSourceRGB, sizeof(Sint32), 1); + } + break; + case 16: + { + fp->read(&entity->textSourceColorRGB, sizeof(Sint32), 1); + fp->read(&entity->textSourceVariables4W, sizeof(Sint32), 1); + fp->read(&entity->textSourceDelay, sizeof(Sint32), 1); + fp->read(&entity->textSourceIsScript, sizeof(Sint32), 1); + for ( int i = 4; i < 60; ++i ) + { + fp->read(&entity->skill[i], sizeof(Sint32), 1); + } + break; } - break; - default: - break; + case 17: + fp->read(&entity->signalInputDirection, sizeof(Sint32), 1); + fp->read(&entity->signalActivateDelay, sizeof(Sint32), 1); + fp->read(&entity->signalTimerInterval, sizeof(Sint32), 1); + fp->read(&entity->signalTimerRepeatCount, sizeof(Sint32), 1); + fp->read(&entity->signalTimerLatchInput, sizeof(Sint32), 1); + if ( editorVersion >= 29 ) + { + fp->read(&entity->signalInvertOutput, sizeof(Sint32), 1); + } + break; + case 18: + fp->read(&entity->portalCustomSprite, sizeof(Sint32), 1); + fp->read(&entity->portalCustomSpriteAnimationFrames, sizeof(Sint32), 1); + fp->read(&entity->portalCustomZOffset, sizeof(Sint32), 1); + fp->read(&entity->portalCustomLevelsToJump, sizeof(Sint32), 1); + fp->read(&entity->portalNotSecret, sizeof(Sint32), 1); + fp->read(&entity->portalCustomRequiresPower, sizeof(Sint32), 1); + for ( int i = 11; i <= 18; ++i ) + { + fp->read(&entity->skill[i], sizeof(Sint32), 1); + } + break; + case 19: + if ( editorVersion >= 25 ) + { + fp->read(&entity->furnitureDir, sizeof(Sint32), 1); + fp->read(&entity->furnitureTableSpawnChairs, sizeof(Sint32), 1); + fp->read(&entity->furnitureTableRandomItemChance, sizeof(Sint32), 1); + } + else + { + // don't read data, set default. + setSpriteAttributes(entity, nullptr, nullptr); + } + break; + case 20: + fp->read(&entity->skill[11], sizeof(Sint32), 1); + fp->read(&entity->skill[12], sizeof(Sint32), 1); + fp->read(&entity->skill[15], sizeof(Sint32), 1); + for ( int i = 40; i <= 52; ++i ) + { + fp->read(&entity->skill[i], sizeof(Sint32), 1); + } + break; + case 21: + if ( editorVersion >= 26 ) + { + fp->read(&entity->doorForceLockedUnlocked, sizeof(Sint32), 1); + fp->read(&entity->doorDisableLockpicks, sizeof(Sint32), 1); + fp->read(&entity->doorDisableOpening, sizeof(Sint32), 1); + } + break; + case 32: + fp->read(&entity->doorUnlockWhenPowered, sizeof(Sint32), 1); + fp->read(&entity->doorDisableLockpicks, sizeof(Sint32), 1); + fp->read(&entity->doorDisableOpening, sizeof(Sint32), 1); + fp->read(&entity->doorForceLockedUnlocked, sizeof(Sint32), 1); + break; + case 22: + if ( editorVersion >= 26 ) + { + fp->read(&entity->gateDisableOpening, sizeof(Sint32), 1); + } + break; + case 23: + if ( editorVersion >= 26 ) + { + fp->read(&entity->playerStartDir, sizeof(Sint32), 1); + } + break; + case 24: + fp->read(&entity->statueDir, sizeof(Sint32), 1); + fp->read(&entity->statueId, sizeof(Sint32), 1); + break; + case 25: + fp->read(&entity->shrineDir, sizeof(Sint32), 1); + fp->read(&entity->shrineZ, sizeof(Sint32), 1); + if ( editorVersion >= 27 ) + { + fp->read(&entity->shrineDestXOffset, sizeof(Sint32), 1); + fp->read(&entity->shrineDestYOffset, sizeof(Sint32), 1); + } + break; + case 26: + fp->read(&entity->shrineDir, sizeof(Sint32), 1); + fp->read(&entity->shrineZ, sizeof(Sint32), 1); + break; + case 27: + fp->read(&entity->colliderDecorationModel, sizeof(Sint32), 1); + fp->read(&entity->colliderDecorationRotation, sizeof(Sint32), 1); + fp->read(&entity->colliderDecorationHeightOffset, sizeof(Sint32), 1); + fp->read(&entity->colliderDecorationXOffset, sizeof(Sint32), 1); + fp->read(&entity->colliderDecorationYOffset, sizeof(Sint32), 1); + fp->read(&entity->colliderHasCollision, sizeof(Sint32), 1); + fp->read(&entity->colliderSizeX, sizeof(Sint32), 1); + fp->read(&entity->colliderSizeY, sizeof(Sint32), 1); + fp->read(&entity->colliderMaxHP, sizeof(Sint32), 1); + fp->read(&entity->colliderDiggable, sizeof(Sint32), 1); + fp->read(&entity->colliderDamageTypes, sizeof(Sint32), 1); + break; + case 28: + fp->read(&entity->signalInputDirection, sizeof(Sint32), 1); + fp->read(&entity->signalActivateDelay, sizeof(Sint32), 1); + fp->read(&entity->signalTimerInterval, sizeof(Sint32), 1); + fp->read(&entity->signalTimerRepeatCount, sizeof(Sint32), 1); + fp->read(&entity->signalTimerLatchInput, sizeof(Sint32), 1); + fp->read(&entity->signalInvertOutput, sizeof(Sint32), 1); + break; + case 29: + if ( editorVersion >= 29 ) + { + fp->read(&entity->pressurePlateTriggerType, sizeof(Sint32), 1); + } + else + { + // don't read data, set default. + setSpriteAttributes(entity, nullptr, nullptr); + } + break; + case 30: + if ( editorVersion < 31 ) + { + setSpriteAttributes(entity, nullptr, nullptr); + } + fp->read(&entity->wallLockMaterial, sizeof(Sint32), 1); + fp->read(&entity->wallLockInvertPower, sizeof(Sint32), 1); + fp->read(&entity->wallLockTurnable, sizeof(Sint32), 1); + if ( editorVersion >= 31 ) + { + fp->read(&entity->wallLockPickable, sizeof(Sint32), 1); + fp->read(&entity->wallLockPickableSkeletonKey, sizeof(Sint32), 1); + fp->read(&entity->wallLockAutoGenKey, sizeof(Sint32), 1); + } + break; + case 31: + fp->read(&entity->wallLockInvertPower, sizeof(Sint32), 1); + fp->read(&entity->wallLockTimer, sizeof(Sint32), 1); + break; + case 33: + fp->read(&entity->skill[0], sizeof(Sint32), 1); + break; + default: + break; + } } if ( entity->behavior == actMonster || entity->behavior == actPlayer ) { @@ -2493,6 +3082,24 @@ int loadMap(const char* filename2, map_t* destmap, list_t* entlist, list_t* crea FileIO::close(fp); + std::string mapShortName = filename2; + size_t found = mapShortName.rfind("/"); + if ( found != std::string::npos ) + { + mapShortName = mapShortName.substr(found + 1); + } + else + { + found = mapShortName.rfind("\\"); + if ( found != std::string::npos ) + { + mapShortName = mapShortName.substr(found + 1); + } + } + size_t size = std::min(mapShortName.size(), sizeof(destmap->filename) - 1); + memcpy(destmap->filename, mapShortName.c_str(), size); + destmap->filename[size] = '\0'; + if ( destmap == &map ) { nummonsters = 0; @@ -2514,6 +3121,10 @@ int loadMap(const char* filename2, map_t* destmap, list_t* entlist, list_t* crea #endif #endif +#ifndef EDITOR + map.setMapHDRSettings(); +#endif + // create new lightmap for (int c = 0; c < MAXPLAYERS + 1; ++c) { auto& lightmap = lightmaps[c]; @@ -2524,6 +3135,50 @@ int loadMap(const char* filename2, map_t* destmap, list_t* entlist, list_t* crea { memset(lightmap.data(), 0, sizeof(vec4_t) * map.width * map.height); memset(lightmapSmoothed.data(), 0, sizeof(vec4_t) * (map.width + 2) * (map.height + 2)); + +#ifndef EDITOR + if ( !strncmp(map.filename, "fortress", 8) ) + { + Vector4 ambienceColor = {128.f, 128.f, 152.f, 1.f}; + ambienceColor.x *= ambienceColor.w; + ambienceColor.y *= ambienceColor.w; + ambienceColor.z *= ambienceColor.w; + for ( int c = 0; c < destmap->width * destmap->height; c++ ) + { + lightmap[c].x = ambienceColor.x; + lightmap[c].y = ambienceColor.y; + lightmap[c].z = ambienceColor.z; + } + for ( int c = 0; c < (destmap->width + 2) * (destmap->height + 2); c++ ) + { + lightmapSmoothed[c].x = ambienceColor.x; + lightmapSmoothed[c].y = ambienceColor.y; + lightmapSmoothed[c].z = ambienceColor.z; + } + } + if ( (svFlags & SV_FLAG_CHEATS) && + (cvar_map_ambience->x > 0.01 + || cvar_map_ambience->y > 0.01 + || cvar_map_ambience->z > 0.01) ) + { + auto ambienceColor = *cvar_map_ambience; + ambienceColor.x *= ambienceColor.w; + ambienceColor.y *= ambienceColor.w; + ambienceColor.z *= ambienceColor.w; + for ( int c = 0; c < destmap->width * destmap->height; c++ ) + { + lightmap[c].x = ambienceColor.x; + lightmap[c].y = ambienceColor.y; + lightmap[c].z = ambienceColor.z; + } + for ( int c = 0; c < (destmap->width + 2) * (destmap->height + 2); c++ ) + { + lightmapSmoothed[c].x = ambienceColor.x; + lightmapSmoothed[c].y = ambienceColor.y; + lightmapSmoothed[c].z = ambienceColor.z; + } + } +#endif } else { @@ -2603,20 +3258,6 @@ int loadMap(const char* filename2, map_t* destmap, list_t* entlist, list_t* crea } } - std::string mapShortName = filename2; - size_t found = mapShortName.rfind("/"); - if ( found != std::string::npos ) - { - mapShortName = mapShortName.substr(found + 1); - } - else - { - found = mapShortName.rfind("\\"); - if ( found != std::string::npos ) - { - mapShortName = mapShortName.substr(found + 1); - } - } for ( c = 0; c < 512; c++ ) { @@ -2627,11 +3268,6 @@ int loadMap(const char* filename2, map_t* destmap, list_t* entlist, list_t* crea { *checkMapHash = mapHashData; } - - size_t size = std::min(mapShortName.size(), sizeof(destmap->filename) - 1); - memcpy(destmap->filename, mapShortName.c_str(), size); - destmap->filename[size] = '\0'; - return numentities; } @@ -2676,7 +3312,7 @@ int saveMap(const char* filename2) return 1; } - fp->write("BARONY LMPV2.9", sizeof(char), strlen("BARONY LMPV2.0")); // magic code + fp->write("BARONY LMPV3.2", sizeof(char), strlen("BARONY LMPV2.0")); // magic code fp->write(map.name, sizeof(char), 32); // map filename fp->write(map.author, sizeof(char), 32); // map author fp->write(&map.width, sizeof(Uint32), 1); // map width @@ -2805,6 +3441,7 @@ int saveMap(const char* filename2) fp->write(&entity->floorDecorationHeightOffset, sizeof(Sint32), 1); fp->write(&entity->floorDecorationXOffset, sizeof(Sint32), 1); fp->write(&entity->floorDecorationYOffset, sizeof(Sint32), 1); + fp->write(&entity->floorDecorationDestroyIfNoWall, sizeof(Sint32), 1); for ( int i = 8; i < 60; ++i ) { fp->write(&entity->skill[i], sizeof(Sint32), 1); @@ -2825,6 +3462,7 @@ int saveMap(const char* filename2) fp->write(&entity->lightSourceRadius, sizeof(Sint32), 1); fp->write(&entity->lightSourceFlicker, sizeof(Sint32), 1); fp->write(&entity->lightSourceDelay, sizeof(Sint32), 1); + fp->write(&entity->lightSourceRGB, sizeof(Sint32), 1); break; case 16: { @@ -2877,6 +3515,12 @@ int saveMap(const char* filename2) fp->write(&entity->doorDisableLockpicks, sizeof(Sint32), 1); fp->write(&entity->doorDisableOpening, sizeof(Sint32), 1); break; + case 32: + fp->write(&entity->doorUnlockWhenPowered, sizeof(Sint32), 1); + fp->write(&entity->doorDisableLockpicks, sizeof(Sint32), 1); + fp->write(&entity->doorDisableOpening, sizeof(Sint32), 1); + fp->write(&entity->doorForceLockedUnlocked, sizeof(Sint32), 1); + break; case 22: fp->write(&entity->gateDisableOpening, sizeof(Sint32), 1); break; @@ -2921,6 +3565,21 @@ int saveMap(const char* filename2) case 29: fp->write(&entity->pressurePlateTriggerType, sizeof(Sint32), 1); break; + case 30: + fp->write(&entity->wallLockMaterial, sizeof(Sint32), 1); + fp->write(&entity->wallLockInvertPower, sizeof(Sint32), 1); + fp->write(&entity->wallLockTurnable, sizeof(Sint32), 1); + fp->write(&entity->wallLockPickable, sizeof(Sint32), 1); + fp->write(&entity->wallLockPickableSkeletonKey, sizeof(Sint32), 1); + fp->write(&entity->wallLockAutoGenKey, sizeof(Sint32), 1); + break; + case 31: + fp->write(&entity->wallLockInvertPower, sizeof(Sint32), 1); + fp->write(&entity->wallLockTimer, sizeof(Sint32), 1); + break; + case 33: + fp->write(&entity->skill[0], sizeof(Sint32), 1); + break; default: break; } @@ -5283,7 +5942,7 @@ void physfsReloadMonsterLimbFiles() for ( int c = 1; c < NUMMONSTERS; c++ ) { // initialize all offsets to zero - for ( int x = 0; x < 20; x++ ) + for ( int x = 0; x < 30; x++ ) { limbs[c][x][0] = 0; limbs[c][x][1] = 0; @@ -5332,7 +5991,7 @@ void physfsReloadMonsterLimbFiles() } // process line - if ( sscanf(data, "%d", &limb) != 1 || limb >= 20 || limb < 0 ) + if ( sscanf(data, "%d", &limb) != 1 || limb >= 30 || limb < 0 ) { printlog("warning: syntax error in '%s':%d\n invalid limb index!\n", filename, line); continue; diff --git a/src/files.hpp b/src/files.hpp index 01065b76c..0252db4e6 100644 --- a/src/files.hpp +++ b/src/files.hpp @@ -322,9 +322,6 @@ bool physfsSearchSpritesToUpdate(); bool physfsIsMapLevelListModded(); bool physfsSearchItemSpritesToUpdate(); void physfsReloadItemSprites(bool reloadAll); -bool physfsSearchItemsTxtToUpdate(); -bool physfsSearchItemsGlobalTxtToUpdate(); -void physfsReloadItemsTxt(); bool physfsSearchMonsterLimbFilesToUpdate(); void physfsReloadMonsterLimbFiles(); void physfsReloadSystemImages(); diff --git a/src/game.cpp b/src/game.cpp index 24b442dbf..1de72ab0f 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -310,11 +310,13 @@ void TimerExperiments::updateEntityInterpolationPosition(Entity* entity) || entity->behavior == &actHudShield || entity->behavior == &actHudArm || entity->behavior == &actHudAdditional + || entity->behavior == &actHudAdditional2 || entity->behavior == &actHudArrowModel || entity->behavior == &actLeftHandMagic || entity->behavior == &actRightHandMagic || entity->behavior == &actCircuit - || entity->behavior == &actDoor ) + || entity->behavior == &actDoor + || entity->behavior == &actIronDoor ) { entity->bUseRenderInterpolation = false; } @@ -389,7 +391,15 @@ void TimerExperiments::renderCameras(view_t& camera, int player) if ( players[player]->entity ) { - if ( !(players[player]->entity->skill[3] == 1) ) // skill[3] is debug cam + if ( players[player]->entity->skill[3] == 2 ) + { + camera.x = TimerExperiments::cameraRenderState[player].x.position; + camera.y = TimerExperiments::cameraRenderState[player].y.position; + camera.ang = TimerExperiments::cameraRenderState[player].yaw.position; + camera.vang = TimerExperiments::cameraRenderState[player].pitch.position; + camera.z = TimerExperiments::cameraRenderState[player].z.position; // this uses PLAYER_CAMERAZ_ACCEL, not entity Z + } + else if ( !(players[player]->entity->skill[3] != 0) ) // skill[3] is debug cam { if ( bDebug ) { @@ -1092,7 +1102,7 @@ void gameLogic(void) { continue; } - if ( stats[c]->EFFECTS[EFF_DRUNK] ) + if ( stats[c]->getEffectActive(EFF_DRUNK) ) { // goat/drunkards no spin! if ( stats[c]->type == GOATMAN ) @@ -1121,7 +1131,7 @@ void gameLogic(void) } else { - if ( stats[c]->EFFECTS[EFF_WITHDRAWAL] || stats[c]->EFFECTS[EFF_DISORIENTED] ) + if ( stats[c]->getEffectActive(EFF_WITHDRAWAL) || stats[c]->getEffectActive(EFF_DISORIENTED) ) { // special widthdrawal shakes if ( drunkextend[c] < 0.2 ) @@ -1200,7 +1210,7 @@ void gameLogic(void) entity->flags[BURNING] = false; continue; } - if ( flickerLights || entity->ticks % TICKS_PER_SECOND == 1 ) + /*if ( flickerLights || entity->ticks % TICKS_PER_SECOND == 1 ) { j = 1 + local_rng.rand() % 4; for ( c = 0; c < j; ++c ) @@ -1217,6 +1227,12 @@ void gameLogic(void) } } } + }*/ + if ( entity->ticks % 10 == 0 ) + { + if ( Entity* fx = spawnFlameSprites(entity, 233) ) + { + } } } } @@ -1224,6 +1240,9 @@ void gameLogic(void) // damage indicator timers handleDamageIndicatorTicks(); +#ifdef STEAMWORKS + MainMenu::richPresence.process(); +#endif if ( intro == true ) { @@ -1297,52 +1316,7 @@ void gameLogic(void) } else { - if ( multiplayer == SERVER ) - { - if ( ticks % 4 == 0 ) - { - // continue informing clients of entities they need to delete - for ( i = 1; i < MAXPLAYERS; i++ ) - { - if ( players[i]->isLocalPlayer() ) - { - continue; - } - j = 0; - for ( node = entitiesToDelete[i].first; node != NULL; node = nextnode ) - { - nextnode = node->next; - - deleteent_t* deleteent = nullptr; - if (net_packet && net_packet->data) { - // send the delete entity command to the client - strcpy((char*)net_packet->data, "ENTD"); - deleteent = (deleteent_t*)node->element; - SDLNet_Write32(deleteent->uid, &net_packet->data[4]); - net_packet->address.host = net_clients[i - 1].host; - net_packet->address.port = net_clients[i - 1].port; - net_packet->len = 8; - sendPacket(net_sock, -1, net_packet, i - 1); - - // quit reminding clients after a certain number of attempts] - if (deleteent) { - deleteent->tries++; - if (deleteent->tries >= MAXTRIES) - { - list_RemoveNode(node); - } - } - } - - j++; - if ( j >= MAXDELETES ) - { - break; - } - } - } - } - } + static ConsoleVariable cvar_appraisal_auto_switch("/appraisal_auto_switch", true); DebugStats.eventsT2 = std::chrono::high_resolution_clock::now(); if ( multiplayer != CLIENT ) // server/singleplayer code { @@ -1496,6 +1470,34 @@ void gameLogic(void) steamAchievementClient(c, "BARONY_ACH_GILDED"); } + { + int numsongs = 0; + if ( ((int)stats[c]->getEffectActive(EFF_ENSEMBLE_FLUTE) - 1) >= Stat::kEnsembleBreakPointTier4 ) + { + ++numsongs; + } + if ( ((int)stats[c]->getEffectActive(EFF_ENSEMBLE_LUTE) - 1) >= Stat::kEnsembleBreakPointTier4 ) + { + ++numsongs; + } + if ( ((int)stats[c]->getEffectActive(EFF_ENSEMBLE_LYRE) - 1) >= Stat::kEnsembleBreakPointTier4 ) + { + ++numsongs; + } + if ( ((int)stats[c]->getEffectActive(EFF_ENSEMBLE_HORN) - 1) >= Stat::kEnsembleBreakPointTier4 ) + { + ++numsongs; + } + if ( ((int)stats[c]->getEffectActive(EFF_ENSEMBLE_DRUM) - 1) >= Stat::kEnsembleBreakPointTier4 ) + { + ++numsongs; + } + if ( numsongs >= 4 ) + { + steamAchievementClient(c, "BARONY_ACH_POWER_BALLAD"); + } + } + if ( stats[c]->helmet && stats[c]->helmet->type == HAT_WOLF_HOOD && stats[c]->helmet->beatitude > 0 ) { @@ -1524,9 +1526,9 @@ void gameLogic(void) steamAchievementClient(c, "BARONY_ACH_COMEDIAN"); } - if ( stats[c]->EFFECTS[EFF_SHRINE_RED_BUFF] - && stats[c]->EFFECTS[EFF_SHRINE_GREEN_BUFF] - && stats[c]->EFFECTS[EFF_SHRINE_BLUE_BUFF] ) + if ( stats[c]->getEffectActive(EFF_SHRINE_RED_BUFF) + && stats[c]->getEffectActive(EFF_SHRINE_GREEN_BUFF) + && stats[c]->getEffectActive(EFF_SHRINE_BLUE_BUFF) ) { steamAchievementClient(c, "BARONY_ACH_WELL_PREPARED"); } @@ -1652,7 +1654,8 @@ void gameLogic(void) //if( TICKS_PER_SECOND ) //generatePathMaps(); - bool debugMonsterTimer = false && !gamePaused && keystatus[SDLK_g]; + static ConsoleVariable cvar_debug_monster_timer("/debug_monster_timer", false); + bool debugMonsterTimer = *cvar_debug_monster_timer && !gamePaused && keystatus[SDLK_F1]; if ( debugMonsterTimer ) { printlog("loop start"); @@ -1706,6 +1709,7 @@ void gameLogic(void) } } + bool loadedNextLevel = false; for ( node = map.entities->first; node != nullptr; node = nextnode ) { nextnode = node->next; @@ -1725,6 +1729,7 @@ void gameLogic(void) && entity->behavior != &actHudWeapon && entity->behavior != &actHudShield && entity->behavior != &actHudAdditional + && entity->behavior != &actHudAdditional2 && entity->behavior != &actHudArrowModel && entity->behavior != &actLeftHandMagic && entity->behavior != &actRightHandMagic @@ -1825,6 +1830,7 @@ void gameLogic(void) { // when this flag is set, it's time to load the next level. loadnextlevel = false; + loadedNextLevel = true; int totalFloorGold = 0; int totalFloorItems = 0; @@ -1941,6 +1947,7 @@ void gameLogic(void) players[i]->hud.weapon = nullptr; players[i]->hud.magicLeftHand = nullptr; players[i]->hud.magicRightHand = nullptr; + players[i]->hud.magicRangefinder = nullptr; players[i]->ghost.reset(); FollowerMenu[i].recentEntity = nullptr; FollowerMenu[i].followerToCommand = nullptr; @@ -1967,6 +1974,7 @@ void gameLogic(void) { soundNotification_group->stop(); } + ensembleSounds.stopPlaying(true); VoiceChat.deinitRecording(false); #elif defined USE_OPENAL if ( sound_group ) @@ -2196,6 +2204,11 @@ void gameLogic(void) std::atomic_bool loading_done {false}; auto loading_task = std::async(std::launch::async, [&loading_done](){ gameplayCustomManager.readFromFile(); + if ( gameplayCustomManager.inUse() ) + { + conductGameChallenges[CONDUCT_MODDED] = 1; + Mods::disableSteamAchievements = true; + } textSourceScript.scriptVariables.clear(); updateLoadingScreen(10); @@ -2245,10 +2258,13 @@ void gameLogic(void) CalloutMenu[i].closeCalloutMenuGUI(); } players[i]->hud.followerBars.clear(); + spellcastingAnimationManager_deactivate(&cast_animation[i]); } EnemyHPDamageBarHandler::dumpCache(); + AOEIndicators_t::cleanup(); monsterAllyFormations.reset(); particleTimerEmitterHitEntities.clear(); + particleTimerEffects.clear(); monsterTrapIgnoreEntities.clear(); minimapHighlights.clear(); @@ -2388,17 +2404,17 @@ void gameLogic(void) { if (players[c] && players[c]->entity && !client_disconnected[c]) { - if ( stats[c] && stats[c]->EFFECTS[EFF_POLYMORPH] && stats[c]->playerPolymorphStorage != NOTHING ) + if ( stats[c] && stats[c]->getEffectActive(EFF_POLYMORPH) && stats[c]->playerPolymorphStorage != NOTHING ) { players[c]->entity->effectPolymorph = stats[c]->playerPolymorphStorage; serverUpdateEntitySkill(players[c]->entity, 50); // update visual polymorph effect for clients. } - if ( stats[c] && stats[c]->EFFECTS[EFF_SHAPESHIFT] && stats[c]->playerShapeshiftStorage != NOTHING ) + if ( stats[c] && stats[c]->getEffectActive(EFF_SHAPESHIFT) && stats[c]->playerShapeshiftStorage != NOTHING ) { players[c]->entity->effectShapeshift = stats[c]->playerShapeshiftStorage; serverUpdateEntitySkill(players[c]->entity, 53); // update visual polymorph effect for clients. } - if ( stats[c] && stats[c]->EFFECTS[EFF_VAMPIRICAURA] && stats[c]->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) + if ( stats[c] && stats[c]->getEffectActive(EFF_VAMPIRICAURA) && stats[c]->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) { players[c]->entity->playerVampireCurse = 1; serverUpdateEntitySkill(players[c]->entity, 51); // update curse progression @@ -2472,6 +2488,10 @@ void gameLogic(void) { monster->monsterAllySummonRank = (stats[c]->playerSummon2PERCHR & 0x0000FF00) >> 8; } + else if ( monsterStats->getAttribute("SUMMONED_CREATURE") != "" ) + { + monster->monsterAllySummonRank = std::stoi(monsterStats->getAttribute("SUMMONED_CREATURE")); + } serverUpdateEntitySkill(monster, 46); // update monsterAllyClass serverUpdateEntitySkill(monster, 44); // update monsterAllyPickupItems serverUpdateEntitySkill(monster, 50); // update monsterAllySummonRank @@ -2584,6 +2604,88 @@ void gameLogic(void) Compendium_t::Events_t::onLevelChangeEvent(c, Compendium_t::Events_t::previousCurrentLevel, Compendium_t::Events_t::previousSecretlevel, prevmapname, playerDied[c]); players[c]->compendiumProgress.playerAliveTimeTotal = 0; players[c]->compendiumProgress.playerGameTimeTotal = 0; + + if ( c == clientnum && !playerDied[c] ) + { + if ( stats[c]->type == MYCONID && stats[c]->playerRace == RACE_MYCONID && stats[c]->stat_appearance == 0 + && stats[c]->helmet && gameStatistics[STATISTICS_NO_CAP] >= 0 ) + { + gameStatistics[STATISTICS_NO_CAP]++; + if ( gameStatistics[STATISTICS_NO_CAP] >= 5 ) + { + steamAchievement("BARONY_ACH_NO_CAP"); + } + } + if ( stats[c]->getEffectActive(EFF_GROWTH) >= 2 + && ((stats[c]->type == MYCONID && stats[c]->playerRace == RACE_MYCONID) + || (stats[c]->type == DRYAD && stats[c]->playerRace == RACE_DRYAD)) && stats[c]->stat_appearance == 0 + && !stats[c]->helmet && gameStatistics[STATISTICS_DONT_TOUCH_HAIR] >= 0 ) + { + gameStatistics[STATISTICS_DONT_TOUCH_HAIR]++; + if ( gameStatistics[STATISTICS_DONT_TOUCH_HAIR] >= 25 ) + { + steamAchievement("BARONY_ACH_DONT_TOUCH_HAIR"); + } + } + if ( stats[c]->type == SALAMANDER && stats[c]->playerRace == RACE_SALAMANDER && stats[c]->stat_appearance == 0 + && stats[c]->getEffectActive(EFF_SALAMANDER_HEART) >= 3 && stats[c]->getEffectActive(EFF_SALAMANDER_HEART) <= 4 + && gameStatistics[STATISTICS_GARGOYLES_QUEST] >= 0 ) + { + gameStatistics[STATISTICS_GARGOYLES_QUEST]++; + if ( gameStatistics[STATISTICS_GARGOYLES_QUEST] >= 10 ) + { + steamAchievement("BARONY_ACH_GARGOYLES_QUEST"); + } + } + if ( stats[c]->type == SALAMANDER && stats[c]->playerRace == RACE_SALAMANDER && stats[c]->stat_appearance == 0 + && stats[c]->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && stats[c]->getEffectActive(EFF_SALAMANDER_HEART) <= 2 + && gameStatistics[STATISTICS_FIRE_FIGHTER] >= 0 ) + { + gameStatistics[STATISTICS_FIRE_FIGHTER]++; + if ( gameStatistics[STATISTICS_FIRE_FIGHTER] >= 5 ) + { + steamAchievement("BARONY_ACH_FIRE_FIGHTER"); + } + } + if ( stats[c]->type == SALAMANDER && stats[c]->playerRace == RACE_SALAMANDER && stats[c]->stat_appearance == 0 + && !stats[c]->getEffectActive(EFF_SALAMANDER_HEART) + && gameStatistics[STATISTICS_DISCIPLINE] >= 0 ) + { + gameStatistics[STATISTICS_DISCIPLINE]++; + if ( gameStatistics[STATISTICS_DISCIPLINE] >= 25 ) + { + steamAchievement("BARONY_ACH_DISCIPLINE"); + } + } + } + + // unsustain any previous effects + node_t* spellnode; + spellnode = stats[c]->magic_effects.first; + while ( spellnode ) + { + node_t* oldnode = spellnode; + spellnode = spellnode->next; + if ( spell_t* spell = (spell_t*)oldnode->element ) + { + spell->magic_effects_node = NULL; + if ( spell->sustainEffectDissipate >= 0 ) + { + if ( stats[c]->getEffectActive(spell->sustainEffectDissipate) ) + { + if ( stats[c]->EFFECTS_TIMERS[spell->sustainEffectDissipate] > 0 ) + { + stats[c]->EFFECTS_TIMERS[spell->sustainEffectDissipate] = 1; + } + } + list_RemoveNode(oldnode); + } + else + { + spell->sustain = false; + } + } + } } // save at end of level change @@ -2775,9 +2877,13 @@ void gameLogic(void) client_selected[j] = NULL; } + Player::PlayerMechanics_t::ensembleMusicUpdate(); + // world UI Player::WorldUI_t::handleTooltips(); + AOEIndicators_t::update(); + int backpack_sizey[MAXPLAYERS]; for ( int player = 0; player < MAXPLAYERS; ++player ) @@ -2817,6 +2923,21 @@ void gameLogic(void) } int bloodCount = 0; + std::map numKeys; + + Item* previousAppraiseItem = nullptr; + bool updateItemToAppraise = false; + if ( players[player]->inventoryUI.appraisal.current_item != 0 && *cvar_appraisal_auto_switch ) + { + if ( previousAppraiseItem = uidToItem(players[player]->inventoryUI.appraisal.current_item) ) + { + if ( previousAppraiseItem->uid != players[player]->inventoryUI.appraisal.manual_appraised_item ) + { + updateItemToAppraise = true; + } + } + } + for ( node = stats[player]->inventory.first; node != NULL; node = nextnode ) { nextnode = node->next; @@ -2826,9 +2947,21 @@ void gameLogic(void) continue; } - if ( item->notifyIcon && itemCategory(item) == SPELL_CAT ) + if ( itemCategory(item) == SPELL_CAT ) { - players[player]->magic.bHasUnreadNewSpell = true; + if ( item->notifyIcon ) + { + players[player]->magic.bHasUnreadNewSpell = true; + } + item->spellNotifyIcon = false; + if ( auto spell = getSpellFromItem(player, item, false) ) + { + if ( stats[player]->getProficiency(spell->skillID) < SKILL_LEVEL_LEGENDARY + && spell->difficulty > (stats[player]->getProficiency(spell->skillID) - 20) ) + { + item->spellNotifyIcon = true; + } + } } // unlock achievements for special collected items @@ -2846,6 +2979,16 @@ void gameLogic(void) case ARTIFACT_SPEAR: steamAchievement("BARONY_ACH_SPEAR_OF_DESTINY"); break; + case KEY_STONE: + case KEY_BONE: + case KEY_BRONZE: + case KEY_IRON: + case KEY_SILVER: + case KEY_GOLD: + case KEY_CRYSTAL: + case KEY_MACHINE: + numKeys[item->type]++; + break; default: break; } @@ -2899,6 +3042,21 @@ void gameLogic(void) } } + if ( item->type == TOOL_DUCK && players[player]->entity ) + { + if ( item->getDuckPlayer() != player || item->status == BROKEN ) + { + messagePlayer(player, MESSAGE_INVENTORY, Language::get(6869)); + bool droppedAll = false; + droppedAll = dropItem(item, player, false, true); + if ( droppedAll ) + { + item = nullptr; + continue; + } + } + } + // drop any inventory items you don't have room for if ( itemCategory(item) != SPELL_CAT && item->x != Player::PaperDoll_t::ITEM_PAPERDOLL_COORDINATE @@ -2922,10 +3080,16 @@ void gameLogic(void) } else { - if ( auto_appraise_new_items && players[player]->inventoryUI.appraisal.timer == 0 + if ( auto_appraise_new_items + && (players[player]->inventoryUI.appraisal.timer == 0 || (ticks % 10 == 0 && updateItemToAppraise) ) && !(item->identified) && players[player]->inventoryUI.appraisal.appraisalPossible(item) ) { int appraisal_time = players[player]->inventoryUI.appraisal.getAppraisalTime(item); + if ( players[player]->inventoryUI.appraisal.appraisalProgressionItems.find(item->uid) + != players[player]->inventoryUI.appraisal.appraisalProgressionItems.end() ) + { + appraisal_time = std::min(appraisal_time, players[player]->inventoryUI.appraisal.appraisalProgressionItems[item->uid]); + } if ( appraisal_time < auto_appraise_lowest_time[player] ) { auto_appraise_target[player] = item; @@ -2934,6 +3098,11 @@ void gameLogic(void) } } } + + if ( numKeys.size() >= 4 ) + { + steamAchievement("BARONY_ACH_JANITOR"); + } } DebugStats.eventsT6 = std::chrono::high_resolution_clock::now(); @@ -3240,6 +3409,7 @@ void gameLogic(void) && entity->behavior != &actHudWeapon && entity->behavior != &actHudShield && entity->behavior != &actHudAdditional + && entity->behavior != &actHudAdditional2 && entity->behavior != &actHudArrowModel && entity->behavior != &actLeftHandMagic && entity->behavior != &actRightHandMagic @@ -3461,9 +3631,13 @@ void gameLogic(void) entity->ranbehavior = false; } + Player::PlayerMechanics_t::ensembleMusicUpdate(); + // world UI Player::WorldUI_t::handleTooltips(); + AOEIndicators_t::update(); + auto& playerInventory = players[clientnum]->inventoryUI; const int inventorySizeX = playerInventory.getSizeX(); int backpack_sizey = playerInventory.DEFAULT_INVENTORY_SIZEY; @@ -3483,7 +3657,20 @@ void gameLogic(void) } int bloodCount = 0; + std::map numKeys; players[clientnum]->magic.bHasUnreadNewSpell = false; + Item* previousAppraiseItem = nullptr; + bool updateItemToAppraise = false; + if ( players[clientnum]->inventoryUI.appraisal.current_item != 0 && *cvar_appraisal_auto_switch ) + { + if ( previousAppraiseItem = uidToItem(players[clientnum]->inventoryUI.appraisal.current_item) ) + { + if ( previousAppraiseItem->uid != players[clientnum]->inventoryUI.appraisal.manual_appraised_item ) + { + updateItemToAppraise = true; + } + } + } for ( node = stats[clientnum]->inventory.first; node != NULL; node = nextnode ) { nextnode = node->next; @@ -3492,9 +3679,22 @@ void gameLogic(void) { continue; } - if ( item->notifyIcon && itemCategory(item) == SPELL_CAT ) + + if ( itemCategory(item) == SPELL_CAT ) { - players[clientnum]->magic.bHasUnreadNewSpell = true; + if ( item->notifyIcon ) + { + players[clientnum]->magic.bHasUnreadNewSpell = true; + } + item->spellNotifyIcon = false; + if ( auto spell = getSpellFromItem(clientnum, item, false) ) + { + if ( stats[clientnum]->getProficiency(spell->skillID) < SKILL_LEVEL_LEGENDARY + && spell->difficulty > (stats[clientnum]->getProficiency(spell->skillID) - 20) ) + { + item->spellNotifyIcon = true; + } + } } // unlock achievements for special collected items @@ -3512,6 +3712,16 @@ void gameLogic(void) case ARTIFACT_SPEAR: steamAchievement("BARONY_ACH_SPEAR_OF_DESTINY"); break; + case KEY_STONE: + case KEY_BONE: + case KEY_BRONZE: + case KEY_IRON: + case KEY_SILVER: + case KEY_GOLD: + case KEY_CRYSTAL: + case KEY_MACHINE: + numKeys[item->type]++; + break; default: break; } @@ -3565,6 +3775,21 @@ void gameLogic(void) } } + if ( item->type == TOOL_DUCK && players[clientnum]->entity ) + { + if ( item->getDuckPlayer() != clientnum || item->status == BROKEN ) + { + messagePlayer(clientnum, MESSAGE_INVENTORY, Language::get(6869)); + bool droppedAll = false; + droppedAll = dropItem(item, clientnum, false, true); + if ( droppedAll ) + { + item = nullptr; + continue; + } + } + } + // drop any inventory items you don't have room for if ( itemCategory(item) != SPELL_CAT && item->x != Player::PaperDoll_t::ITEM_PAPERDOLL_COORDINATE @@ -3588,10 +3813,16 @@ void gameLogic(void) } else { - if ( auto_appraise_new_items && players[clientnum]->inventoryUI.appraisal.timer == 0 + if ( auto_appraise_new_items && + (players[clientnum]->inventoryUI.appraisal.timer == 0 || (ticks % 10 == 0 && updateItemToAppraise)) && !(item->identified) && players[clientnum]->inventoryUI.appraisal.appraisalPossible(item) ) { int appraisal_time = players[clientnum]->inventoryUI.appraisal.getAppraisalTime(item); + if ( players[clientnum]->inventoryUI.appraisal.appraisalProgressionItems.find(item->uid) + != players[clientnum]->inventoryUI.appraisal.appraisalProgressionItems.end() ) + { + appraisal_time = std::min(appraisal_time, players[clientnum]->inventoryUI.appraisal.appraisalProgressionItems[item->uid]); + } if (appraisal_time < auto_appraise_lowest_time[clientnum]) { auto_appraise_target[clientnum] = item; @@ -3601,6 +3832,11 @@ void gameLogic(void) } } + if ( numKeys.size() >= 4 ) + { + steamAchievement("BARONY_ACH_JANITOR"); + } + if ( kills[SHOPKEEPER] >= 3 ) { steamAchievement("BARONY_ACH_PROFESSIONAL_BURGLAR"); @@ -3618,7 +3854,11 @@ void gameLogic(void) { if ( auto_appraise_target[i] != NULL ) { - players[i]->inventoryUI.appraisal.appraiseItem(auto_appraise_target[i]); + if ( (auto_appraise_target[i]->uid != players[i]->inventoryUI.appraisal.current_item) + || players[i]->inventoryUI.appraisal.timer == 0 ) + { + players[i]->inventoryUI.appraisal.appraiseItem(auto_appraise_target[i]); + } } } } @@ -4407,6 +4647,64 @@ bool handleEvents(void) mousey = event.motion.y * factorY; mousexrel += event.motion.xrel; mouseyrel += event.motion.yrel; + + //{ + // // debug code for checking checking mouse motions during lag + //static std::map < Uint32, std::vector> evs; + //evs[event.motion.timestamp].push_back(event.motion); + //if ( evs[event.motion.timestamp].size() > 1 ) + //{ + // static std::map>>>> ss; + // auto& val = ss[event.motion.timestamp][event.motion.x][event.motion.y][event.motion.xrel][event.motion.yrel]; + // val++; + // if ( evs[event.motion.timestamp].size() >= 2000 ) + // { + // mousexrel -= event.motion.xrel; + // mouseyrel -= event.motion.yrel; + // messagePlayer(0, MESSAGE_DEBUG, "cleared"); + // } + //} + //if ( evs.size() > 100 ) + //{ + // size_t m = 0; + // for ( auto& v : evs ) + // { + // m = std::max(v.second.size(), m); + // } + // //if ( m > 25 ) + // { + // for ( auto& v : evs ) + // { + // //if ( v.second.size() > 25 ) + // { + // int netx = 0; + // int nety = 0; + // int dupes = 0; + // std::map>>> ss; + // for ( auto& s : v.second ) + // { + // ss[s.x][s.y][s.xrel][s.yrel]++; + // if ( ss[s.x][s.y][s.xrel][s.yrel] > 1 ) + // { + // ++dupes; + // } + // netx += s.xrel; + // nety += s.yrel; + // } + // if ( dupes > 0 ) + // { + // int rx = 0; + // int ry = 0; + // SDL_GetRelativeMouseState(&rx, &ry); + // messagePlayer(0, MESSAGE_DEBUG, "net x: %d y: %d | dupes: %d | rx: %d ry: %d", netx, nety, dupes, rx, ry); + // } + // } + // } + // messagePlayer(0, MESSAGE_DEBUG, "max dupe: %d", m); + // } + // evs.clear(); + //} + //} if (initialized) { @@ -5447,12 +5745,14 @@ void ingameHud() // spellcasting // player needs to be alive if ( players[player]->isLocalPlayerAlive() - && !gamePaused ) + && !gamePaused + && !players[player]->ghost.isActive() ) { const bool shootmode = players[player]->shootmode; bool hasSpellbook = false; bool tryHotbarQuickCast = players[player]->hotbar.faceMenuQuickCast; bool tryInventoryQuickCast = players[player]->magic.doQuickCastSpell(); + bool tryTomeQuickCast = players[player]->magic.doQuickCastTome(); if ( stats[player]->shield && itemCategory(stats[player]->shield) == SPELLBOOK ) { hasSpellbook = true; @@ -5460,7 +5760,8 @@ void ingameHud() players[player]->hotbar.faceMenuQuickCast = false; bool allowCasting = false; - if ( tryInventoryQuickCast ) + bool castAnimationTouch = false; + if ( tryInventoryQuickCast || tryTomeQuickCast ) { allowCasting = true; } @@ -5471,7 +5772,27 @@ void ingameHud() bool castMemorizedSpell = input.binaryToggle("Cast Spell"); bool castSpellbook = (hasSpellbook && input.binaryToggle("Defend")); - if (tryHotbarQuickCast || castMemorizedSpell || castSpellbook ) + if ( inputs.hasController(player) && cast_animation[player].spellWaitingAttackInput() ) + { + allowCasting = false; + castAnimationTouch = true; + + if ( FollowerMenu[player].followerMenuIsOpen() || CalloutMenu[player].calloutMenuIsOpen() ) + { + // nothing, let menucancel close them + } + else if ( input.binaryToggle("MenuCancel") ) + { + input.consumeBinaryToggle("MenuCancel"); + input.consumeBindingsSharedWithBinding("MenuCancel"); + input.consumeBinaryToggle("Hotbar Left"); + input.consumeBinaryToggle("Hotbar Up / Select"); + input.consumeBinaryToggle("Hotbar Right"); + + castSpellInit(players[player]->entity->getUID(), players[player]->magic.selectedSpell(), false, false); + } + } + else if (tryHotbarQuickCast || castMemorizedSpell || castSpellbook ) { allowCasting = true; if ( tryHotbarQuickCast == false ) @@ -5575,15 +5896,38 @@ void ingameHud() } if ( tryInventoryQuickCast ) { - castSpellInit(players[player]->entity->getUID(), players[player]->magic.quickCastSpell(), false); + castSpellInit(players[player]->entity->getUID(), players[player]->magic.quickCastSpell(), false, false); + } + else if ( tryTomeQuickCast ) + { + if ( Item* item = uidToItem(players[player]->magic.quickCastTome()) ) + { + if ( auto spellID = item->getTomeSpellID() ) + { + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + if ( !cast_animation[player].active && !cast_animation[player].active_spellbook ) + { + castSpellInit(players[player]->entity->getUID(), spell, false, true); + if ( cast_animation[player].active ) + { + list_RemoveNode(item->node); + } + } + } + } + } + } } else if ( hasSpellbook && input.consumeBinaryToggle("Defend") ) { - castSpellInit(players[player]->entity->getUID(), getSpellFromID(getSpellIDFromSpellbook(stats[player]->shield->type)), true); + castSpellInit(players[player]->entity->getUID(), getSpellFromID(getSpellIDFromSpellbook(stats[player]->shield->type)), true, false); } else { - castSpellInit(players[player]->entity->getUID(), players[player]->magic.selectedSpell(), false); + castSpellInit(players[player]->entity->getUID(), players[player]->magic.selectedSpell(), false, false); } if ( players[player]->magic.selectedSpell() ) { @@ -5595,22 +5939,52 @@ void ingameHud() { if ( tryInventoryQuickCast ) { - castSpellInit(players[player]->entity->getUID(), players[player]->magic.quickCastSpell(), false); + castSpellInit(players[player]->entity->getUID(), players[player]->magic.quickCastSpell(), false, false); + } + else if ( tryTomeQuickCast ) + { + if ( Item* item = uidToItem(players[player]->magic.quickCastTome()) ) + { + if ( auto spellID = item->getTomeSpellID() ) + { + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + if ( !cast_animation[player].active && !cast_animation[player].active_spellbook ) + { + castSpellInit(players[player]->entity->getUID(), spell, false, true); + if ( cast_animation[player].active ) + { + list_RemoveNode(item->node); + } + } + } + } + } + } } else if ( hasSpellbook && input.consumeBinaryToggle("Defend") ) { - castSpellInit(players[player]->entity->getUID(), getSpellFromID(getSpellIDFromSpellbook(stats[player]->shield->type)), true); + castSpellInit(players[player]->entity->getUID(), getSpellFromID(getSpellIDFromSpellbook(stats[player]->shield->type)), true, false); } else { - castSpellInit(players[player]->entity->getUID(), players[player]->magic.selectedSpell(), false); + castSpellInit(players[player]->entity->getUID(), players[player]->magic.selectedSpell(), false, false); } } } input.consumeBinaryToggle("Defend"); } - input.consumeBinaryToggle("Cast Spell"); + if ( !castAnimationTouch ) + { + if ( !(cast_animation[player].stage == 4 || cast_animation[player].stage == 9) ) // allow recast if pressed during touch throw window + { + input.consumeBinaryToggle("Cast Spell"); + } + } } + players[player]->magic.resetQuickCastTome(); players[player]->magic.resetQuickCastSpell(); bool worldUIBlocksFollowerCycle = ( @@ -5764,6 +6138,7 @@ void ingameHud() GenericGUI[player].tinkerGUI.updateTinkerMenu(); GenericGUI[player].alchemyGUI.updateAlchemyMenu(); GenericGUI[player].assistShrineGUI.updateAssistShrine(); + GenericGUI[player].mailboxGUI.updateMailMenu(); GenericGUI[player].featherGUI.updateFeatherMenu(); GenericGUI[player].itemfxGUI.updateItemEffectMenu(); players[player]->GUI.dropdownMenu.process(); @@ -5835,8 +6210,8 @@ void ingameHud() printTextFormatted(font8x8_bmp, x, y + 72, "flx: %4f | fly: %4f", inputs.getController(player)->oldFloatRightX, inputs.getController(player)->oldFloatRightY); printTextFormatted(font8x8_bmp, x, y + 84, "deadzonex: %3.1f%% | deadzoney: %3.1f%%", - inputs.getController(player)->leftStickDeadzone * 100 / 32767.0, - inputs.getController(player)->rightStickDeadzone * 100 / 32767.0); + playerSettings[multiplayer ? 0 : player].leftStickDeadzone * 100 / 32767.0, + playerSettings[multiplayer ? 0 : player].rightStickDeadzone * 100 / 32767.0); } if ( players[player]->entity ) { @@ -5846,8 +6221,8 @@ void ingameHud() if ( inputs.hasController(player) ) { printTextFormatted(font8x8_bmp, x, y + 112, "leftx: %4f | lefty: %4f", - inputs.getController(player)->getLeftXPercent(), - inputs.getController(player)->getLeftYPercent()); + inputs.getController(player)->getLeftXPercent(player), + inputs.getController(player)->getLeftYPercent(player)); } if ( players[player]->entity ) { @@ -6215,8 +6590,16 @@ void drawAllPlayerCameras() { if (players[c]->ghost.isActive()) { *cvar_hdrBrightness = {0.9f, 0.9f, 1.2f, 1.0f}; *cvar_fogColor = {0.7f, 0.7f, 1.1f, 0.25f}; + if ( !strncmp(map.filename, "fortress", 8) ) + { + cvar_fogColor->w = 1.f; + } *cvar_fogDistance = 350.f; } + else if ( players[c] && players[c]->entity && players[c]->entity->isBlind() ) + { + *cvar_fogDistance = 0.f; // no fog when blind to ensure black + } // do occlusion culling from the perspective of this camera DebugStats.drawWorldT2 = std::chrono::high_resolution_clock::now(); @@ -6263,22 +6646,36 @@ void drawAllPlayerCameras() { globalLightModifierActive = GLOBAL_LIGHT_MODIFIER_INUSE; globalLightModifier = 0.f; globalLightModifierEntities = 0.f; - if ( stats[c]->mask && stats[c]->mask->type == TOOL_BLINDFOLD_TELEPATHY ) + if ( !intro ) { - for ( node_t* mapNode = map.creatures->first; mapNode != nullptr; mapNode = mapNode->next ) + bool selfTelepath = stats[c]->mask && stats[c]->mask->type == TOOL_BLINDFOLD_TELEPATHY; + if ( selfTelepath ) { - Entity* mapCreature = (Entity*)mapNode->element; - if ( mapCreature && !intro ) + for ( node_t* mapNode = map.creatures->first; mapNode != nullptr; mapNode = mapNode->next ) { - mapCreature->monsterEntityRenderAsTelepath = 1; + Entity* mapCreature = (Entity*)mapNode->element; + if ( mapCreature && + (selfTelepath + /*|| (mapCreature->getStats() && mapCreature->getStats()->getEffectActive(EFF_DETECT_ENEMY))*/ + ) ) + { + /*if ( mapCreature->getStats() && mapCreature->getStats()->getEffectActive(EFF_DETECT_ENEMY) ) + { + mapCreature->monsterEntityRenderAsTelepath = 2; + } + else*/ + { + mapCreature->monsterEntityRenderAsTelepath = 1; + } + } } } } } int PERModifier = 0; - if ( stats[c] && stats[c]->EFFECTS[EFF_BLIND] - && !stats[c]->EFFECTS[EFF_ASLEEP] && !stats[c]->EFFECTS[EFF_MESSY] ) + if ( stats[c] && stats[c]->getEffectActive(EFF_BLIND) + && !stats[c]->getEffectActive(EFF_ASLEEP) && !stats[c]->getEffectActive(EFF_MESSY) ) { // blind but not messy or asleep = allow PER to let you see the world a little. PERModifier = players[c]->entity->getPER() / 5; @@ -6367,10 +6764,8 @@ void drawAllPlayerCameras() { glEndCamera(&camera, true, map); // undo ghost fog - if (players[c]->ghost.isActive()) { - *cvar_hdrBrightness = {1.0f, 1.0f, 1.0f, 1.0f}; - *cvar_fogColor = {0.0f, 0.0f, 0.0f, 1.0f}; - *cvar_fogDistance = 0.0f; + if (players[c]->ghost.isActive() || (players[c]->entity && players[c]->entity->isBlind()) ) { + map.setMapHDRSettings(); } if (shaking && players[c] && players[c]->entity && !gamePaused) @@ -6455,7 +6850,7 @@ static void doConsoleCommands() { if (input.consumeBinaryToggle("Console Command")) { input.consumeBindingsSharedWithBinding("Console Command"); - if (!command) { + if (!command && !inputstr ) { confirm = true; } } @@ -6659,6 +7054,9 @@ int main(int argc, char** argv) { #ifdef WINDOWS SetUnhandledExceptionFilter(unhandled_handler); +#ifdef _DEBUG + //_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); +#endif #endif // WINDOWS #ifdef NINTENDO nxInit(); @@ -7080,6 +7478,7 @@ int main(int argc, char** argv) players[i]->hud.weapon = nullptr; players[i]->hud.magicLeftHand = nullptr; players[i]->hud.magicRightHand = nullptr; + players[i]->hud.magicRangefinder = nullptr; players[i]->ghost.reset(); FollowerMenu[i].recentEntity = nullptr; FollowerMenu[i].followerToCommand = nullptr; @@ -7483,13 +7882,24 @@ int main(int argc, char** argv) auto& camera = cameras[c]; auto& cvars = cameravars[c]; TimerExperiments::renderCameras(camera, c); - camera.ang += cvars.shakex2; - camera.vang += cvars.shakey2 / 200.0; + + real_t mult = 1.0; + if ( !intro && stats[c]->getEffectActive(EFF_DELAY_PAIN) ) + { + mult = 0.25; + } + camera.ang += mult * cvars.shakex2; + camera.vang += mult * cvars.shakey2 / 200.0; for ( auto& HPBar : enemyHPDamageBarHandler[c].HPBars ) { HPBar.second.updateWorldCoordinates(); // update enemy bar world coordinates before drawEntities3D called } + players[c]->worldUI.worldTooltipDialogue.playerDialogue.updateWorldCoordinates(); // update dialogue world coordinates before drawEntities3D called + for ( auto& worldTooltipDialogue : players[c]->worldUI.worldTooltipDialogue.sharedDialogues ) + { + worldTooltipDialogue.second.updateWorldCoordinates(); + } } if ( !MainMenu::isCutsceneActive() && fadealpha < 255 ) diff --git a/src/game.hpp b/src/game.hpp index 01798038b..06f4976ef 100644 --- a/src/game.hpp +++ b/src/game.hpp @@ -25,9 +25,9 @@ // REMEMBER TO CHANGE THIS WITH EVERY NEW OFFICIAL VERSION!!! #ifdef NINTENDO -static const char VERSION[] = "v4.3.2"; +static const char VERSION[] = "v5.0.1"; #else -static const char VERSION[] = "v4.3.2"; +static const char VERSION[] = "v5.0.1"; #endif #define GAME_CODE @@ -83,7 +83,6 @@ extern bool oassailant[MAXPLAYERS]; extern int assailantTimer[MAXPLAYERS]; static const int COMBAT_MUSIC_COOLDOWN = 200; // 200 ticks of combat music before it fades away. extern list_t removedEntities; -extern list_t entitiesToDelete[MAXPLAYERS]; extern char maptoload[256], configtoload[256]; extern bool loadingmap, loadingconfig; extern int startfloor; @@ -99,9 +98,9 @@ extern real_t time_diff; extern real_t t, ot, frameval[AVERAGEFRAMES]; extern Uint32 cycles, pingtime; extern real_t fps; -static const int NUMCLASSES = 21; -#define NUMRACES 13 -#define NUMPLAYABLERACES 9 +static const int NUMCLASSES = 26; +#define NUMRACES 18 +#define NUMPLAYABLERACES 14 extern char address[64]; extern bool loadnextlevel; extern int skipLevelsOnLoad; @@ -155,7 +154,12 @@ enum PlayerClasses : int CLASS_MACHINIST, CLASS_PUNISHER, CLASS_SHAMAN, - CLASS_HUNTER + CLASS_HUNTER, + CLASS_BARD, + CLASS_SAPPER, + CLASS_SCION, + CLASS_HERMIT, + CLASS_PALADIN }; static const std::vector playerClassInternalNames = { @@ -179,7 +183,12 @@ static const std::vector playerClassInternalNames = { "class_machinist", "class_punisher", "class_shaman", - "class_hunter" + "class_hunter", + "class_bard", + "class_sapper", + "class_scion", + "class_hermit", + "class_paladin" }; static const int CLASS_SHAMAN_NUM_STARTING_SPELLS = 15; @@ -198,7 +207,13 @@ enum PlayerRaces : int RACE_RAT, RACE_TROLL, RACE_SPIDER, - RACE_IMP + RACE_IMP, + RACE_GNOME, + RACE_GREMLIN, + RACE_DRYAD, + RACE_MYCONID, + RACE_SALAMANDER, + RACE_ENUM_END }; bool achievementUnlocked(const char* achName); @@ -209,7 +224,6 @@ void steamAchievementEntity(Entity* my, const char* achName); // give steam achi void steamStatisticUpdate(int statisticNum, ESteamStatTypes type, int value); void steamStatisticUpdateClient(int player, int statisticNum, ESteamStatTypes type, int value); void steamIndicateStatisticProgress(int statisticNum, ESteamStatTypes type); -void freePlayerEquipment(int x); void pauseGame(int mode, int ignoreplayer); int initGame(); void initGameDatafiles(bool moddedReload); @@ -229,6 +243,7 @@ void actStatueAnimator(Entity* my); void actStatue(Entity* my); void actDoorFrame(Entity* my); void actDeathCam(Entity* my); +void actProjectSpiritCam(Entity* my); void actDeathGhost(Entity* my); void actDeathGhostLimb(Entity* my); void actPlayerLimb(Entity* my); @@ -240,13 +255,21 @@ void actHudArm(Entity* my); void actHudShield(Entity* my); void actHudAdditional(Entity* my); void actHudArrowModel(Entity* my); +void actHudAdditional2(Entity* my); void actItem(Entity* my); void actGoldBag(Entity* my); void actGib(Entity* my); +void actGreasePuddleSpawner(Entity* my); +void actGreasePuddle(Entity* my); +void actMiscPuddle(Entity* my); +void spawnGreasePuddleSpawner(Entity* caster, real_t x, real_t y, int duration); void actDamageGib(Entity* my); +void actFociGib(Entity* my); +Entity* spawnFociGib(real_t x, real_t y, real_t z, real_t dir, real_t velocityBonus, Uint32 parentUid, int sprite, Uint32 seed); Entity* spawnGib(Entity* parentent, int customGibSprite = -1); Entity* spawnDamageGib(Entity* parentent, Sint32 dmgAmount, int gibDmgType, int displayType = 0, bool updateClients = false); Entity* spawnGibClient(Sint16 x, Sint16 y, Sint16 z, Sint16 sprite); +Entity* spawnMiscPuddle(Entity* parentent, real_t x, real_t y, int sprite, bool updateClients = false); void serverSpawnGibForClient(Entity* gib); void actLadder(Entity* my); void actLadderUp(Entity* my); @@ -254,7 +277,11 @@ void actPortal(Entity* my); void actWinningPortal(Entity* my); void actFlame(Entity* my); void actCampfire(Entity* my); +void actCauldron(Entity* my); +void actWorkbench(Entity* my); +void actMailbox(Entity* my); Entity* spawnFlame(Entity* parentent, Sint32 sprite); +Entity* spawnFlameSprites(Entity* parentent, Sint32 sprite); Entity* castMagic(Entity* parentent); void actSprite(Entity* my); void actSpriteNametag(Entity* my); @@ -304,6 +331,10 @@ void actSoundSource(Entity* my); void actLightSource(Entity* my); void actSignalTimer(Entity* my); void actSignalGateAND(Entity* my); +void actWallLock(Entity* my); +void actWallButton(Entity* my); +void actWind(Entity* my); +void createWaterSplash(real_t x, real_t y, int lifetime); void startMessages(); bool frameRateLimit(Uint32 maxFrameRate, bool resetAccumulator = true, bool sleep = false); @@ -329,8 +360,9 @@ extern char last_port[64]; //TODO: Maybe increase with level or something? //TODO: Pause health regen during combat? -#define HEAL_TIME 600 //10 seconds. //Original time: 3600 (1 minute) -#define MAGIC_REGEN_TIME 300 // 5 seconds +#define HEAL_TIME 600 //12 seconds. //Original time: 3600 (1 minute) +#define MAGIC_REGEN_TIME 600 // 12 seconds +#define MAGIC_REGEN_AUTOMATON_TIME 300 #define DEFAULT_HP 30 #define DEFAULT_MP 30 @@ -368,9 +400,10 @@ extern std::vector randomNPCNamesMale; extern std::vector randomNPCNamesFemale; extern bool enabledDLCPack1; extern bool enabledDLCPack2; +extern bool enabledDLCPack3; extern std::vector physFSFilesInDirectory; void loadRandomNames(); -void mapLevel(int player); +int mapLevel(int player, int radius, int _x, int _y, bool usingSpell); void mapLevel2(int player); void mapFoodOnLevel(int player); bool mapTileDiggable(const int x, const int y); diff --git a/src/init.cpp b/src/init.cpp index cc8e6de5b..52ff37384 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -9,12 +9,13 @@ -------------------------------------------------------------------------------*/ +#include "main.hpp" + #include #include #include #include -#include "main.hpp" #ifdef NINTENDO #include "nintendo/nxplatform.hpp" #endif // NINTENDO diff --git a/src/init_game.cpp b/src/init_game.cpp index 5b0fdb1c2..399ae1eab 100644 --- a/src/init_game.cpp +++ b/src/init_game.cpp @@ -91,6 +91,7 @@ void initGameDatafiles(bool moddedReload) { EquipmentModelOffsets.readFromFile(monstertypename[c], c); } + EquipmentModelOffsets.readBaseItemsFromFile(); setupSpells(); CompendiumEntries.readMonstersFromFile(); Compendium_t::Events_t::itemDisplayedEventsList.clear(); @@ -115,6 +116,7 @@ void initGameDatafiles(bool moddedReload) CompendiumEntries.readModelLimbsFromFile("world"); CompendiumEntries.readModelLimbsFromFile("codex"); MainMenu::MainMenuBanners_t::readFromFile(); + Player::Inventory_t::Appraisal_t::readFromFile(); } void initGameDatafilesAsync(bool moddedReload) @@ -265,6 +267,32 @@ int initGame() FileIO::close(fp); } } + if ( PHYSFS_getRealDir("desertersanddisciples.key") != NULL ) //TODO: NX PORT: Update for the Switch? + { + std::string serial = PHYSFS_getRealDir("desertersanddisciples.key"); + serial.append(PHYSFS_getDirSeparator()).append("desertersanddisciples.key"); + // open the serial file + File* fp = nullptr; + if ( (fp = FileIO::open(serial.c_str(), "rb")) != NULL ) + { + char buf[64]; + size_t len = fp->read(&buf, sizeof(char), 32); + buf[len] = '\0'; + serial = buf; + // compute hash + size_t DLCHash = serialHash(serial); + if ( DLCHash == 121449 ) + { + printlog("[LICENSE]: Deserters and Disciples DLC license key found."); + enabledDLCPack3 = true; + } + else + { + printlog("[LICENSE]: DLC license key invalid."); + } + FileIO::close(fp); + } + } #endif // !NINTENDO #endif @@ -276,10 +304,16 @@ int initGame() { safePacketsReceivedMap[c].clear(); } - topscores.first = NULL; - topscores.last = NULL; - topscoresMultiplayer.first = NULL; - topscoresMultiplayer.last = NULL; + topscores_legacy.first = NULL; + topscores_legacy.last = NULL; + topscoresMultiplayer_legacy.first = NULL; + topscoresMultiplayer_legacy.last = NULL; + + topscores_json.first = nullptr; + topscores_json.last = nullptr; + topscoresMultiplayer_json.first = nullptr; + topscoresMultiplayer_json.last = nullptr; + messages.first = NULL; messages.last = NULL; for ( int i = 0; i < MAXPLAYERS; ++i ) @@ -320,11 +354,11 @@ int initGame() stats[c]->FOLLOWERS.last = nullptr; stats[c]->inventory.first = nullptr; stats[c]->inventory.last = nullptr; + stats[c]->void_chest_inventory.first = nullptr; + stats[c]->void_chest_inventory.last = nullptr; stats[c]->clearStats(); - entitiesToDelete[c].first = nullptr; - entitiesToDelete[c].last = nullptr; - initClass(c); GenericGUI[c].setPlayer(c); + initClass(c); FollowerMenu[c].setPlayer(c); CalloutMenu[c].setPlayer(c); cameras[c].winx = 0; @@ -447,6 +481,7 @@ void deinitGame() // destroy enemy hp bar textures EnemyHPDamageBarHandler::dumpCache(); + AOEIndicators_t::cleanup(); // send disconnect messages if (multiplayer != SINGLE) { @@ -502,8 +537,10 @@ void deinitGame() saveAllScores(SCORESFILE); saveAllScores(SCORESFILE_MULTIPLAYER); - list_FreeAll(&topscores); - list_FreeAll(&topscoresMultiplayer); + list_FreeAll(&topscores_json); + list_FreeAll(&topscoresMultiplayer_json); + list_FreeAll(&topscores_legacy); + list_FreeAll(&topscoresMultiplayer_legacy); for ( int i = 0; i < MAXPLAYERS; ++i ) { players[i]->messageZone.deleteAllNotificationMessages(); @@ -521,6 +558,7 @@ void deinitGame() players[c]->inventoryUI.appraisal.timer = 0; players[c]->inventoryUI.appraisal.current_item = 0; list_FreeAll(&stats[c]->inventory); + list_FreeAll(&stats[c]->void_chest_inventory); list_FreeAll(&stats[c]->FOLLOWERS); if ( multiplayer == CLIENT ) { @@ -542,7 +580,7 @@ void deinitGame() list_FreeAll(map.worldUI); //TODO: Need to do this? } list_FreeAll(&messages); - if ( multiplayer == SINGLE ) + /*if ( multiplayer == SINGLE ) { list_FreeAll(&channeledSpells[0]); } @@ -556,6 +594,10 @@ void deinitGame() { list_FreeAll(&channeledSpells[c]); } + }*/ + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + list_FreeAll(&channeledSpells[c]); } for ( int c = 0; c < MAXPLAYERS; c++ ) @@ -599,6 +641,9 @@ void deinitGame() tutorialmusic->release(); gameovermusic->release(); introstorymusic->release(); +#ifdef USE_FMOD + ensembleSounds.deinit(); +#endif for ( int c = 0; c < NUMMINESMUSIC; c++ ) { @@ -680,6 +725,14 @@ void deinitGame() { free(intromusic); } + for ( int c = 0; c < NUMFORTRESSMUSIC; c++ ) + { + fortressmusic[c]->release(); + } + if ( fortressmusic ) + { + free(fortressmusic); + } } #ifdef USE_OPENAL #undef FMOD_Channel_Stop @@ -790,6 +843,18 @@ void deinitGame() { free(CompendiumEntries.compendiumMap.tiles); } + + for ( auto it : allGameSpells ) + { + spell_t* spell = it.second; + list_RemoveNode(spell->sustain_node); + list_FreeAll(&spell->elements); + if ( spell->needsDataFreed ) + { + free(spell); + } + } + for (int i = 0; i < MAXPLAYERS; ++i) { delete players[i]; @@ -823,7 +888,7 @@ void loadAchievementData(const char* path) { return; } - char buf[120000]; + static char buf[120000]; int count = (int)fp->read(buf, sizeof(buf[0]), sizeof(buf)); buf[count] = '\0'; rapidjson::StringStream is(buf); @@ -911,6 +976,10 @@ void loadAchievementData(const char* path) { { achData.dlcType = Compendium_t::AchievementData_t::ACH_TYPE_DLC2; } + else if ( !strcmp(ach["dlc"].GetString(), "deserters_disciples") ) + { + achData.dlcType = Compendium_t::AchievementData_t::ACH_TYPE_DLC3; + } } else if ( ach["dlc"].IsArray() ) { @@ -940,6 +1009,17 @@ void loadAchievementData(const char* path) { achData.dlcType = Compendium_t::AchievementData_t::ACH_TYPE_DLC2; } } + else if ( !strcmp(it->GetString(), "deserters_disciples") ) + { + if ( achData.dlcType == Compendium_t::AchievementData_t::ACH_TYPE_DLC1_DLC2 ) + { + achData.dlcType = Compendium_t::AchievementData_t::ACH_TYPE_DLC1_DLC2_DLC3; + } + else + { + achData.dlcType = Compendium_t::AchievementData_t::ACH_TYPE_DLC3; + } + } } } } @@ -967,7 +1047,7 @@ void sortAchievementsForDisplay() #ifdef STEAMWORKS if ( Compendium_t::AchievementData_t::achievementsNeedFirstData ) { - if ( SteamUser()->BLoggedOn() ) + //if ( SteamUser()->BLoggedOn() ) { Compendium_t::AchievementData_t::achievementsNeedFirstData = false; diff --git a/src/interface/clickdescription.cpp b/src/interface/clickdescription.cpp index 79b24132a..b59f97716 100644 --- a/src/interface/clickdescription.cpp +++ b/src/interface/clickdescription.cpp @@ -111,6 +111,10 @@ void clickDescription(int player, Entity* entity) { messagePlayer(player, MESSAGE_INSPECTION, Language::get(256)); } + else if ( entity->behavior == &actIronDoor ) + { + messagePlayer(player, MESSAGE_INSPECTION, Language::get(6409)); + } else if ( entity->isDamageableCollider() ) { messagePlayer(player, MESSAGE_INSPECTION, Language::get(254), Language::get(entity->getColliderLangName())); @@ -143,6 +147,18 @@ void clickDescription(int player, Entity* entity) { messagePlayer(player, MESSAGE_INSPECTION, Language::get(260)); } + else if ( entity->behavior == &actCauldron ) + { + messagePlayer(player, MESSAGE_INSPECTION, Language::get(6976)); + } + else if ( entity->behavior == &actWorkbench ) + { + messagePlayer(player, MESSAGE_INSPECTION, Language::get(6980)); + } + else if ( entity->behavior == &actMailbox ) + { + messagePlayer(player, MESSAGE_INSPECTION, Language::get(6985)); + } else if ( entity->behavior == &actFountain) { messagePlayer(player, MESSAGE_INSPECTION, Language::get(262)); @@ -271,11 +287,40 @@ void clickDescription(int player, Entity* entity) (entity->sprite >= 278 && entity->sprite < 282) || (entity->sprite >= 614 && entity->sprite < 618) || (entity->sprite >= 992 && entity->sprite < 995) || - (entity->sprite == 620)) + (entity->sprite == 620) || entity->teleporterType == 3) { messagePlayer(player, MESSAGE_INSPECTION, Language::get(272)); } } + else if ( entity->behavior == &::actWallLock + || (entity->sprite >= 1585 && entity->sprite <= 1592) ) + { + int wallLockState = entity->wallLockState; + int wallLockMaterial = entity->wallLockMaterial; + if ( entity->sprite >= 1585 && entity->sprite <= 1592 ) + { + if ( Entity* parent = uidToEntity(entity->parent) ) + { + wallLockState = parent->wallLockState; + wallLockMaterial = parent->wallLockMaterial; + } + } + + if ( wallLockState == Entity::WallLockStates::LOCK_NO_KEY ) + { + messagePlayer(player, MESSAGE_INSPECTION, Language::get(6394), Language::get(6383 + wallLockMaterial)); + } + else + { + messagePlayer(player, MESSAGE_INSPECTION, Language::get(6395), Language::get(6383 + wallLockMaterial)); + } + } + else if ( entity->behavior == &::actWallButton + || entity->sprite == 1151 + || entity->sprite == 1152 ) + { + messagePlayer(player, MESSAGE_INSPECTION, Language::get(6396)); + } else if ( entity->behavior == &actFloorDecoration ) { if ( entity->sprite == 991 ) diff --git a/src/interface/consolecommand.cpp b/src/interface/consolecommand.cpp index 177d36a1c..c5e09eff4 100644 --- a/src/interface/consolecommand.cpp +++ b/src/interface/consolecommand.cpp @@ -9,10 +9,10 @@ -------------------------------------------------------------------------------*/ +#include "../main.hpp" #include "consolecommand.hpp" #include -#include "../main.hpp" #include "../files.hpp" #include "../game.hpp" #include "../stat.hpp" @@ -320,6 +320,14 @@ namespace ConsoleCommands { messagePlayer(clientnum, MESSAGE_MISC, "Type \"/listcmds %d\" for more", pagenum + 1); }); + static ConsoleCommand ccmd_listcmds_all("/listcmds_all", "list all console commands", []CCMD{ + auto & map = getConsoleCommands(); + for ( auto& pair : map ) { + auto& cmd = pair.second; + messagePlayer(clientnum, MESSAGE_MISC, "%s", cmd.name); + } + }); + static ConsoleCommand ccmd_mousecapture("/mousecapture", "toggle mouse capture enabled", []CCMD{ if (EnableMouseCapture == SDL_TRUE) { EnableMouseCapture = SDL_FALSE; @@ -421,6 +429,105 @@ namespace ConsoleCommands { lastname = name.c_str(); }); + static ConsoleCommand ccmd_spawntome("/spawntome", "spawn an item (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + if ( argc < 2 ) + { + return; + } + std::string name = argv[1]; + for ( int arg = 2; arg < argc; ++arg ) { + name.append(" "); + name.append(argv[arg]); + } + auto& rng = local_rng; + + int skillID = 0; + switch ( rng.rand() % 3 ) + { + case 0: + skillID = PRO_MYSTICISM; + break; + case 1: + skillID = PRO_SORCERY; + break; + case 2: + skillID = PRO_THAUMATURGY; + break; + default: + break; + } + if ( strstr("mysticism", name.c_str()) ) + { + skillID = PRO_MYSTICISM; + } + else if ( strstr("sorcery", name.c_str()) ) + { + skillID = PRO_SORCERY; + } + else if ( strstr("thaumaturgy", name.c_str()) ) + { + skillID = PRO_THAUMATURGY; + } + + int itemType = SPELLBOOK_FORCEBOLT; + std::vector> chances; + int minDifficulty = std::min(60, (currentlevel / 5) * 20); + for ( auto& def : allGameSpells ) + { + if ( auto spell = def.second ) + { + if ( spell->ID != SPELL_NONE && !spell->hide_from_ui && currentlevel >= spell->drop_table ) + { + if ( (spell->difficulty / 20) <= (1 + (currentlevel / 5)) + && (spell->difficulty >= minDifficulty) + && spell->skillID == skillID ) + { + chances.push_back(std::make_pair(spell->skillID, spell->ID)); + } + } + } + } + + if ( chances.size() ) + { + Uint32 appearance = 0; + int pick = rng.rand() % chances.size(); + int spellbookType = getSpellbookFromSpellID(chances[pick].second); + /*if ( items[spellbookType].category == SPELLBOOK ) + { + itemType = spellbookType; + } + else*/ + { + itemType = TOME_SORCERY; + appearance = spellTomeIDToAppearance[chances[pick].second]; + if ( chances[pick].first == PRO_MYSTICISM ) + { + itemType = TOME_MYSTICISM; + } + else if ( chances[pick].first == PRO_THAUMATURGY ) + { + itemType = TOME_THAUMATURGY; + } + } + bool identified = true; + if ( argc >= 3 ) + { + std::string str = argv[2]; + if ( str == "0" ) + { + identified = false; + } + } + dropItem(newItem(static_cast(itemType), EXCELLENT, 0, 1, appearance, identified, &stats[clientnum]->inventory), 0); + } + }); + static ConsoleCommand ccmd_spawnitem("/spawnitem", "spawn an item (cheat)", []CCMD{ if (!(svFlags & SV_FLAG_CHEATS)) { @@ -837,8 +944,8 @@ namespace ConsoleCommands { // this is definitely considered a cheat. // otherwise it's a major gameplay exploit. // do not disable this code block. - messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); - return; + //messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + //return; } if (players[clientnum] != nullptr && players[clientnum]->entity != nullptr) { @@ -1284,7 +1391,7 @@ namespace ConsoleCommands { Stat* tempStats = players[clientnum]->entity->getStats(); if (tempStats) { - tempStats->EFFECTS[EFF_POISONED] = true; + tempStats->setEffectActive(EFF_POISONED, 1); tempStats->EFFECTS_TIMERS[EFF_POISONED] = 600; } } @@ -1306,6 +1413,18 @@ namespace ConsoleCommands { skipintro = (skipintro == false); }); +#ifndef NDEBUG + static ConsoleCommand ccmd_test_story("/test_story", "test cutscene", []CCMD{ + if ( argc < 2 ) { + return; + } + int num = atoi(argv[1]); + MainMenu::beginFade((MainMenu::FadeDestination)num); + movie = true; + pauseGame(2, false); + }); +#endif + static ConsoleCommand ccmd_levelmagic("/levelmagic", "level up magic skills (cheat)", []CCMD{ if (!(svFlags & SV_FLAG_CHEATS)) { @@ -1318,8 +1437,9 @@ namespace ConsoleCommands { int i = 0; for (; i < 10; ++i) { - players[clientnum]->entity->increaseSkill(PRO_MAGIC); - players[clientnum]->entity->increaseSkill(PRO_SPELLCASTING); + players[clientnum]->entity->increaseSkill(PRO_SORCERY); + players[clientnum]->entity->increaseSkill(PRO_MYSTICISM); + players[clientnum]->entity->increaseSkill(PRO_THAUMATURGY); } } else @@ -1410,6 +1530,16 @@ namespace ConsoleCommands { if (entity->behavior == &actMonster) { entity->setHP(0); + if ( argc >= 2 ) + { + if ( atoi(argv[1]) == 1 ) + { + if ( players[clientnum]->entity ) + { + players[clientnum]->entity->awardXP(entity, true, true); + } + } + } c++; } } @@ -1506,7 +1636,7 @@ namespace ConsoleCommands { } // Attempt to set the Player on fire - players[clientnum]->entity->SetEntityOnFire(); + players[clientnum]->entity->SetEntityOnFire(nullptr); for (int c = 0; c < 100; c++) { @@ -1545,11 +1675,12 @@ namespace ConsoleCommands { if (!(c == EFF_VAMPIRICAURA && players[clientnum]->entity->getStats()->EFFECTS_TIMERS[c] == -2) && c != EFF_WITHDRAWAL && c != EFF_SHAPESHIFT) { - players[clientnum]->entity->getStats()->EFFECTS[c] = false; + players[clientnum]->entity->getStats()->clearEffect(c); players[clientnum]->entity->getStats()->EFFECTS_TIMERS[c] = 0; + players[clientnum]->entity->getStats()->EFFECTS_ACCRETION_TIME[c] = 0; } } - if (players[clientnum]->entity->getStats()->EFFECTS[EFF_WITHDRAWAL]) + if (players[clientnum]->entity->getStats()->getEffectActive(EFF_WITHDRAWAL)) { players[clientnum]->entity->setEffect(EFF_WITHDRAWAL, false, EFFECT_WITHDRAWAL_BASE_TIME, true); } @@ -1818,8 +1949,12 @@ namespace ConsoleCommands { } }); - static ConsoleCommand ccmd_mapseed("/mapseed", "display map seed", []CCMD{ - messagePlayer(clientnum, MESSAGE_MISC, "Mapseed: %d | Gamekey: %lu | Lobby: %lu", mapseed, uniqueGameKey, uniqueLobbyKey); + static ConsoleCommand ccmd_mapseed("/mapseed", "display game seed", []CCMD{ + messagePlayer(clientnum, MESSAGE_MISC, "Game seed: %lu", uniqueGameKey); + }); + + static ConsoleCommand ccmd_gameseeds("/mapseed2", "display game seeds", []CCMD{ + messagePlayer(clientnum, MESSAGE_MISC, "Mapseed: %d | Game seed: %lu | Lobby: %lu", mapseed, uniqueGameKey, uniqueLobbyKey); }); static ConsoleCommand ccmd_seedgame("/seedgame", "set custom seed", []CCMD{ @@ -1844,6 +1979,7 @@ namespace ConsoleCommands { static ConsoleCommand ccmd_reloadlang("/reloadlang", "reload language file", []CCMD{ Language::reloadLanguage(); + ItemTooltips.readItemLocalizationsFromFile(); }); static ConsoleCommand ccmd_disablemessages("/disablemessages", "disable all messages", []CCMD{ @@ -1978,25 +2114,35 @@ namespace ConsoleCommands { stats[i]->clearStats(); client_classes[i] = local_rng.rand() % (CLASS_MONK + 1);//NUMCLASSES; stats[i]->playerRace = RACE_HUMAN; - if (enabledDLCPack1 || enabledDLCPack2) + if (enabledDLCPack1 || enabledDLCPack2 || enabledDLCPack3) { - stats[i]->playerRace = local_rng.rand() % NUMPLAYABLERACES; - if (!enabledDLCPack1) + std::vector chances; + chances.resize(NUMRACES); + chances[RACE_HUMAN] = 1; + if ( enabledDLCPack1 ) { - while (stats[i]->playerRace == RACE_SKELETON || stats[i]->playerRace == RACE_VAMPIRE - || stats[i]->playerRace == RACE_SUCCUBUS || stats[i]->playerRace == RACE_GOATMAN) - { - stats[i]->playerRace = local_rng.rand() % NUMPLAYABLERACES; - } + chances[RACE_SKELETON] = 1; + chances[RACE_VAMPIRE] = 1; + chances[RACE_GOATMAN] = 1; + chances[RACE_SUCCUBUS] = 1; } - else if (!enabledDLCPack2) + if ( enabledDLCPack2 ) { - while (stats[i]->playerRace == RACE_AUTOMATON || stats[i]->playerRace == RACE_GOBLIN - || stats[i]->playerRace == RACE_INCUBUS || stats[i]->playerRace == RACE_INSECTOID) - { - stats[i]->playerRace = local_rng.rand() % NUMPLAYABLERACES; - } + chances[RACE_INSECTOID] = 1; + chances[RACE_INCUBUS] = 1; + chances[RACE_INSECTOID] = 1; + chances[RACE_AUTOMATON] = 1; + } + if ( enabledDLCPack3 ) + { + chances[RACE_GREMLIN] = 1; + chances[RACE_MYCONID] = 1; + chances[RACE_DRYAD] = 1; + chances[RACE_GNOME] = 1; + chances[RACE_SALAMANDER] = 1; } + + stats[i]->playerRace = local_rng.discrete(chances.data(), chances.size()); if (stats[i]->playerRace == RACE_INCUBUS) { stats[i]->sex = MALE; @@ -2009,21 +2155,15 @@ namespace ConsoleCommands { if (stats[i]->playerRace == RACE_HUMAN) { client_classes[i] = local_rng.rand() % (NUMCLASSES); - if (!enabledDLCPack1) + while ( + (!enabledDLCPack1 && (client_classes[i] == CLASS_CONJURER || client_classes[i] == CLASS_ACCURSED + || client_classes[i] == CLASS_MESMER || client_classes[i] == CLASS_BREWER)) + || (!enabledDLCPack2 && (client_classes[i] == CLASS_HUNTER || client_classes[i] == CLASS_SHAMAN + || client_classes[i] == CLASS_PUNISHER || client_classes[i] == CLASS_MACHINIST)) + || (!enabledDLCPack3 && (client_classes[i] == CLASS_BARD || client_classes[i] == CLASS_SAPPER + || client_classes[i] == CLASS_SCION || client_classes[i] == CLASS_HERMIT || client_classes[i] == CLASS_PALADIN)) ) { - while (client_classes[i] == CLASS_CONJURER || client_classes[i] == CLASS_ACCURSED - || client_classes[i] == CLASS_MESMER || client_classes[i] == CLASS_BREWER) - { - client_classes[i] = local_rng.rand() % (NUMCLASSES); - } - } - else if (!enabledDLCPack2) - { - while (client_classes[i] == CLASS_HUNTER || client_classes[i] == CLASS_SHAMAN - || client_classes[i] == CLASS_PUNISHER || client_classes[i] == CLASS_MACHINIST) - { - client_classes[i] = local_rng.rand() % (NUMCLASSES); - } + client_classes[i] = local_rng.rand() % (NUMCLASSES); } stats[i]->stat_appearance = local_rng.rand() % 18; } @@ -2280,6 +2420,7 @@ namespace ConsoleCommands { entity->flags[PASSABLE] = true; entity->flags[UPDATENEEDED] = true; entity->behavior = &actGoldBag; + entity->goldDroppedByPlayer = player + 1; } messagePlayer(player, MESSAGE_INVENTORY, Language::get(2594), amount); } @@ -2342,11 +2483,11 @@ namespace ConsoleCommands { messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); return; } - if (multiplayer != SINGLE) + /*if (multiplayer != SINGLE) { messagePlayer(clientnum, MESSAGE_MISC, Language::get(299)); return; - } + }*/ if (argc < 2) { @@ -2359,9 +2500,25 @@ namespace ConsoleCommands { } else { - for (int i = 0; i < 10; ++i) + if ( multiplayer == CLIENT ) { - players[clientnum]->entity->increaseSkill(skill); + for ( int i = 0; i < 10; ++i ) + { + strcpy((char*)net_packet->data, "CSKL"); + net_packet->data[4] = clientnum; + net_packet->data[5] = skill; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + } + else + { + for (int i = 0; i < 10; ++i) + { + players[clientnum]->entity->increaseSkill(skill); + } } } }); @@ -2380,7 +2537,7 @@ namespace ConsoleCommands { messagePlayer(clientnum, MESSAGE_MISC, Language::get(412)); - mapLevel(clientnum); + mapLevel(clientnum, 0, 0, 0, false); }); static ConsoleCommand ccmd_maplevel2("/maplevel2", "magic mapping for the level (cheat)", []CCMD{ @@ -2431,14 +2588,14 @@ namespace ConsoleCommands { return; } - if (!players[clientnum]->entity->getStats()->EFFECTS[EFF_DRUNK]) + if (!players[clientnum]->entity->getStats()->getEffectActive(EFF_DRUNK)) { - players[clientnum]->entity->getStats()->EFFECTS[EFF_DRUNK] = true; + players[clientnum]->entity->getStats()->setEffectActive(EFF_DRUNK, 1); players[clientnum]->entity->getStats()->EFFECTS_TIMERS[EFF_DRUNK] = -1; } else { - players[clientnum]->entity->getStats()->EFFECTS[EFF_DRUNK] = false; + players[clientnum]->entity->getStats()->clearEffect(EFF_DRUNK); players[clientnum]->entity->getStats()->EFFECTS_TIMERS[EFF_DRUNK] = 0; } }); @@ -2486,7 +2643,7 @@ namespace ConsoleCommands { for (int c = 1; c < NUMMONSTERS; c++) { // initialize all offsets to zero - for (x = 0; x < 20; x++) + for (x = 0; x < 30; x++) { limbs[c][x][0] = 0; limbs[c][x][1] = 0; @@ -2521,7 +2678,7 @@ namespace ConsoleCommands { } // process line - if (sscanf(data, "%d", &limb) != 1 || limb >= 20 || limb < 0) + if (sscanf(data, "%d", &limb) != 1 || limb >= 30 || limb < 0) { messagePlayer(clientnum, MESSAGE_MISC, "warning: syntax error in '%s':%d\n invalid limb index!", filename, line); printlog("warning: syntax error in '%s':%d\n invalid limb index!\n", filename, line); @@ -2722,6 +2879,27 @@ namespace ConsoleCommands { secretlevel = (secretlevel == false); }); + static ConsoleCommand ccmd_setlvl("/setlvl", "set character lvl", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + if ( multiplayer != SINGLE ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(299)); + return; + } + + if ( argc < 2 ) + { + return; + } + int lvl = atoi(argv[1]); + stats[clientnum]->LVL = lvl; + }); + static ConsoleCommand ccmd_seteffect("/seteffect", "give the player the specified effect (cheat)", []CCMD{ if (!(svFlags & SV_FLAG_CHEATS)) { @@ -2746,11 +2924,16 @@ namespace ConsoleCommands { else { int duration = 500; + Uint8 strength = 1; if ( argc >= 3 ) { duration = atoi(argv[2]); } - players[clientnum]->entity->setEffect(effect, true, duration, true); + if ( argc >= 4 ) + { + strength = atoi(argv[3]); + } + players[clientnum]->entity->setEffect(effect, strength, duration, true, true, true); } }); @@ -3062,8 +3245,8 @@ namespace ConsoleCommands { } messagePlayer(clientnum, MESSAGE_MISC, "Hungover Active: %d, Time to go: %d, Drunk Active: %d, Drunk time: %d", - stats[clientnum]->EFFECTS[EFF_WITHDRAWAL], stats[clientnum]->EFFECTS_TIMERS[EFF_WITHDRAWAL], - stats[clientnum]->EFFECTS[EFF_DRUNK], stats[clientnum]->EFFECTS_TIMERS[EFF_DRUNK]); + stats[clientnum]->getEffectActive(EFF_WITHDRAWAL), stats[clientnum]->EFFECTS_TIMERS[EFF_WITHDRAWAL], + stats[clientnum]->getEffectActive(EFF_DRUNK), stats[clientnum]->EFFECTS_TIMERS[EFF_DRUNK]); return; }); @@ -3095,6 +3278,30 @@ namespace ConsoleCommands { disableFPSLimitOnNetworkMessages = !disableFPSLimitOnNetworkMessages; }); + static ConsoleCommand ccmd_addspell("/addspell", "teach player some spells (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + if ( argc < 2 ) + { + return; + } + int spellID = atoi(argv[1]); + if ( allGameSpells.find(spellID) != allGameSpells.end() ) + { + if ( spell_t* spell = allGameSpells[spellID] ) + { + bool oldIntro = intro; + intro = true; + bool learned = addSpell(spell->ID, clientnum, true); + intro = oldIntro; + } + } + }); + static ConsoleCommand ccmd_allspells1("/allspells1", "teach player some spells (cheat)", []CCMD{ if (!(svFlags & SV_FLAG_CHEATS)) { @@ -3102,13 +3309,18 @@ namespace ConsoleCommands { return; } - for (auto it = allGameSpells.begin(); it != allGameSpells.begin() + 29; ++it) + for ( int i = SPELL_NONE + 1; i <= 29 && i < NUM_SPELLS; ++i ) { - spell_t* spell = *it; - bool oldIntro = intro; - intro = true; - bool learned = addSpell(spell->ID, clientnum, true); - intro = oldIntro; + if ( allGameSpells.find(i) != allGameSpells.end() ) + { + if ( spell_t* spell = allGameSpells[i] ) + { + bool oldIntro = intro; + intro = true; + bool learned = addSpell(spell->ID, clientnum, true); + intro = oldIntro; + } + } } return; }); @@ -3229,10 +3441,15 @@ namespace ConsoleCommands { messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); return; } + for (int i = 0; i < NUM_SPELLS; ++i) { + if ( i == SPELL_CRAB_FORM || i == SPELL_CRAB_WEB ) { continue; } int spellbook = getSpellbookFromSpellID(i); - dropItem(newItem(static_cast(spellbook), DECREPIT, -1, 1, local_rng.rand(), true, &stats[clientnum]->inventory), 0); + if ( spellbook >= 0 && spellbook < NUMITEMS && items[spellbook].category == SPELLBOOK ) + { + dropItem(newItem(static_cast(spellbook), DECREPIT, 0, 1, local_rng.rand(), true, &stats[clientnum]->inventory), 0); + } } }); @@ -3588,8 +3805,16 @@ namespace ConsoleCommands { { if (inputs.hasController(i)) { - inputs.getController(i)->addRumble(GameController::Haptic_t::RUMBLE_NORMAL, - 8000, 0, TICKS_PER_SECOND, 0); + if ( argc >= 4 ) + { + inputs.getController(i)->addRumble(GameController::Haptic_t::RUMBLE_SPELL, + atoi(argv[1]), atoi(argv[2]), atoi(argv[3]), 0); + } + else + { + inputs.getController(i)->addRumble(GameController::Haptic_t::RUMBLE_NORMAL, + 8000, 0, TICKS_PER_SECOND, 0); + } } } }); @@ -3782,9 +4007,109 @@ namespace ConsoleCommands { static ConsoleCommand ccmd_loaditems("/loaditems", "", []CCMD{ ItemTooltips.readItemsFromFile(); + setupSpells(); messagePlayer(clientnum, MESSAGE_MISC, "Reloaded items.json"); }); + static ConsoleCommand ccmd_reloadsound("/reloadsound", "reloads specific sounds.txt index", []CCMD{ + if ( argc > 1 ) + { + int soundIndex = atoi(argv[1]); + if ( soundIndex < numsounds ) + { + Mods::soundsListModifiedIndexes.push_back(soundIndex); + physfsReloadSounds(false); + } + } + }); + + static ConsoleCommand ccmd_reloadsprite("/reloadsprite", "reloads specific sprites.txt index", []CCMD{ + if ( argc > 1 ) + { + int reloadSpriteIndex = atoi(argv[1]); + + std::string spritesDirectory = PHYSFS_getRealDir("images/sprites.txt"); + spritesDirectory.append(PHYSFS_getDirSeparator()).append("images/sprites.txt"); + printlog("[PhysFS]: Loading sprites from directory %s...\n", spritesDirectory.c_str()); + File* fp = openDataFile(spritesDirectory.c_str(), "rb"); + char name[PATH_MAX]; + for ( int c = 0; !fp->eof(); ++c ) + { + fp->gets2(name, PATH_MAX); + if ( c != reloadSpriteIndex ) { continue; } + if ( PHYSFS_getRealDir(name) != nullptr ) + { + std::string spriteFile = PHYSFS_getRealDir(name); + spriteFile.append(PHYSFS_getDirSeparator()).append(name); + if ( sprites[c] ) + { + SDL_FreeSurface(sprites[c]); + } + char fullname[PATH_MAX]; + strncpy(fullname, spriteFile.c_str(), PATH_MAX - 1); + sprites[c] = loadImage(fullname); + if ( nullptr != sprites[c] ) + { + //Whee + } + else + { + printlog("warning: failed to load '%s' listed at line %d in %s\n", name, c + 1, spritesDirectory.c_str()); + if ( 0 == c ) + { + printlog("sprite 0 cannot be NULL!\n"); + FileIO::close(fp); + return; + } + } + } + break; + } + FileIO::close(fp); + } + }); + + static ConsoleCommand ccmd_reloadspells("/reloadspells", "reload spells definitions and player owned spells (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + consoleCommand("/loaditems"); + node_t* nextnode = nullptr; + players[clientnum]->magic.clearSelectedSpells(); + std::vector relearnSpells; + for ( auto node = stats[clientnum]->inventory.first; node; node = nextnode ) + { + nextnode = node->next; + if ( Item* item = (Item*)node->element ) + { + if ( getSpellFromItem(clientnum, item, true) ) + { + list_RemoveNode(item->node); + } + } + } + + for ( auto node = players[clientnum]->magic.spellList.first; node; node = nextnode ) + { + nextnode = node->next; + int spellID = ((spell_t*)(node->element))->ID; + relearnSpells.push_back(spellID); + list_RemoveNode(node); + } + + + for ( auto spellID : relearnSpells ) + { + bool oldIntro = intro; + intro = true; + bool learned = addSpell(spellID, clientnum, true); + intro = oldIntro; + } + }); + static ConsoleCommand ccmd_gimmeallpotions("/gimmeallpotions", "give all potions (cheat)", []CCMD{ if (!(svFlags & SV_FLAG_CHEATS)) { @@ -3858,13 +4183,18 @@ namespace ConsoleCommands { return; } - for (auto it = allGameSpells.begin() + 29; it != allGameSpells.end(); ++it) + for ( int i = 30; i < NUM_SPELLS; ++i ) { - spell_t* spell = *it; - bool oldIntro = intro; - intro = true; - bool learned = addSpell(spell->ID, clientnum, true); - intro = oldIntro; + if ( allGameSpells.find(i) != allGameSpells.end() ) + { + if ( spell_t* spell = allGameSpells[i] ) + { + bool oldIntro = intro; + intro = true; + bool learned = addSpell(spell->ID, clientnum, true); + intro = oldIntro; + } + } } return; }); @@ -3876,49 +4206,327 @@ namespace ConsoleCommands { return; } - for (auto it = allGameSpells.begin(); it != allGameSpells.end(); ++it) + for ( int i = SPELL_NONE + 1; i < NUM_SPELLS; ++i ) { - spell_t* spell = *it; - bool oldIntro = intro; - intro = true; - bool learned = addSpell(spell->ID, clientnum, true); - intro = oldIntro; + if ( allGameSpells.find(i) != allGameSpells.end() ) + { + if ( spell_t* spell = allGameSpells[i] ) + { + bool oldIntro = intro; + intro = true; + bool learned = addSpell(spell->ID, clientnum, true); + intro = oldIntro; + } + } } return; }); - static ConsoleCommand ccmd_gimmexp("/gimmexp", "give the player some XP (cheat)", []CCMD{ - if (!(svFlags & SV_FLAG_CHEATS)) + static ConsoleCommand ccmd_allspells4("/allspells4", "teach the player some spells (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) { messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); return; } - if (players[clientnum] && players[clientnum]->entity) + /*rapidjson::Document d; + d.SetObject(); + CustomHelpers::addMemberToRoot(d, "item_names", rapidjson::Value(rapidjson::kObjectType));*/ + + for ( int i = SPELL_NONE + 50; i < NUM_SPELLS; ++i ) { - players[clientnum]->entity->getStats()->EXP += 1 + local_rng.rand() % 50; - } - }); + if ( allGameSpells.find(i) != allGameSpells.end() ) + { + if ( spell_t* spell = allGameSpells[i] ) + { + bool oldIntro = intro; + intro = true; + bool learned = addSpell(spell->ID, clientnum, true); + intro = oldIntro; - static ConsoleCommand ccmd_loadhudsettings("/loadhudsettings", "", []CCMD{ - loadHUDSettingsJSON(); - messagePlayer(clientnum, MESSAGE_MISC, "Reloaded HUD_settings.json"); - }); + /*int spellbookId = getSpellbookFromSpellID(spell->ID); + if ( items[spellbookId].category == SPELLBOOK ) + { + d["item_names"].AddMember(rapidjson::Value(ItemTooltips.tmpItems[spellbookId].internalName.c_str(), d.GetAllocator()), rapidjson::Value(rapidjson::kObjectType), + d.GetAllocator()); + std::string str = "spellbook of "; + str += spell->getSpellName(true); + d["item_names"][ItemTooltips.tmpItems[spellbookId].internalName.c_str()].AddMember("name_identified", rapidjson::Value(str.c_str(), d.GetAllocator()), d.GetAllocator()); + d["item_names"][ItemTooltips.tmpItems[spellbookId].internalName.c_str()].AddMember("name_unidentified", rapidjson::Value("spellbook", d.GetAllocator()), d.GetAllocator()); + }*/ + } + } + } - static ConsoleCommand ccmd_loadskillsheet("/loadskillsheet", "", []CCMD{ - Player::SkillSheet_t::loadSkillSheetJSON(); - messagePlayer(clientnum, MESSAGE_MISC, "Reloaded skillsheet_entries.json"); - }); + /*File* fp = FileIO::open("lang/stuff.json", "wb"); + if ( !fp ) + { + return; + } + rapidjson::StringBuffer os; + rapidjson::PrettyWriter writer(os); + d.Accept(writer); + fp->write(os.GetString(), sizeof(char), os.GetSize()); + FileIO::close(fp);*/ - static ConsoleCommand ccmd_loadcharsheet("/loadcharsheet", "", []CCMD{ - Player::CharacterSheet_t::loadCharacterSheetJSON(); - messagePlayer(clientnum, MESSAGE_MISC, "Reloaded charsheet_settings.json"); + return; }); - static ConsoleCommand ccmd_loadfollowerwheel("/loadfollowerwheel", "", []CCMD{ - FollowerRadialMenu::loadFollowerJSON(); - messagePlayer(clientnum, MESSAGE_MISC, "Reloaded follower_wheel.json"); - }); + static ConsoleCommand ccmd_levelspells_sorcery("/levelspells_sorcery", "teach the player some spells (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + std::vector>> orderedSpells; + + for ( int i = 0; i < NUM_SPELLS; ++i ) + { + if ( allGameSpells.find(i) != allGameSpells.end() ) + { + if ( spell_t* spell = allGameSpells[i] ) + { + bool oldIntro = intro; + intro = true; + if ( spell->skillID == PRO_SORCERY + && spell->difficulty <= stats[clientnum]->getProficiency(PRO_SORCERY) + && !spell->hide_from_ui && spell->drop_table >= 0 ) + { + bool learned = addSpell(spell->ID, clientnum, true); + if ( argc >= 2 ) + { + orderedSpells.push_back(std::make_pair(spell->ID, std::make_pair(spell->difficulty, spell->spell_internal_name))); + } + } + intro = oldIntro; + } + } + } + + for ( auto& spell : orderedSpells ) + { + printlog("%d %d %s", spell.first, spell.second.first, spell.second.second); + } + + return; + }); + + static ConsoleCommand ccmd_levelspells_mysticism("/levelspells_mysticism", "teach the player some spells (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + std::vector>> orderedSpells; + + for ( int i = 0; i < NUM_SPELLS; ++i ) + { + if ( allGameSpells.find(i) != allGameSpells.end() ) + { + if ( spell_t* spell = allGameSpells[i] ) + { + bool oldIntro = intro; + intro = true; + if ( spell->skillID == PRO_MYSTICISM + && spell->difficulty <= stats[clientnum]->getProficiency(PRO_MYSTICISM) + && !spell->hide_from_ui && spell->drop_table >= 0 ) + { + bool learned = addSpell(spell->ID, clientnum, true); + if ( argc >= 2 ) + { + orderedSpells.push_back(std::make_pair(spell->ID, std::make_pair(spell->difficulty, spell->spell_internal_name))); + } + } + intro = oldIntro; + } + } + } + + for ( auto& spell : orderedSpells ) + { + printlog("%d %d %s", spell.first, spell.second.first, spell.second.second); + } + + return; + }); + + static ConsoleCommand ccmd_collect_spellbooks("/collect_spellbooks", "collect some spells (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + node_t* nextnode = nullptr; + for ( auto node = map.entities->first; node; node = nextnode ) + { + nextnode = node->next; + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->behavior == &actItem && (items[entity->skill[10]].category == SPELLBOOK || items[entity->skill[10]].category == TOME_SPELL) ) + { + Entity* oldSelected = selectedEntity[0]; + selectedEntity[0] = entity; + bool oldInRange = inrange[0]; + inrange[0] = true; + actItem(entity); + inrange[0] = oldInRange; + selectedEntity[0] = oldSelected; + } + } + } + }); + + static ConsoleCommand ccmd_levelspells_thaumaturgy("/levelspells_thaumaturgy", "teach the player some spells (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + std::vector>> orderedSpells; + + for ( int i = 0; i < NUM_SPELLS; ++i ) + { + if ( allGameSpells.find(i) != allGameSpells.end() ) + { + if ( spell_t* spell = allGameSpells[i] ) + { + bool oldIntro = intro; + intro = true; + if ( spell->skillID == PRO_THAUMATURGY + && spell->difficulty <= stats[clientnum]->getProficiency(PRO_THAUMATURGY) + && !spell->hide_from_ui && spell->drop_table >= 0 ) + { + bool learned = addSpell(spell->ID, clientnum, true); + if ( argc >= 2 ) + { + orderedSpells.push_back(std::make_pair(spell->ID, std::make_pair(spell->difficulty, spell->spell_internal_name))); + } + } + intro = oldIntro; + } + } + } + + for ( auto& spell : orderedSpells ) + { + printlog("%d %d %s", spell.first, spell.second.first, spell.second.second); + } + return; + }); + + static ConsoleCommand ccmd_levelspells_cast("/levelspells_cast", "teach the player some spells (cheat)", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + if ( argc > 1 ) + { + int type = atoi(argv[1]); + int spellID = SPELL_FORCEBOLT; + if ( type == 1 ) + { + spellID = SPELL_LIGHTNING; + } + else if ( type == 2 ) + { + spellID = SPELL_FIREBALL; + } + else if ( type == 3 ) + { + spellID = SPELL_COLD; + } + else if ( type == 4 ) + { + spellID = SPELL_MAGICMISSILE; + } + else if ( type == 5 ) + { + spellID = SPELL_METEOR; + } + + static int mana = 0; + static int rolls = 0; + static int longestDryStreak = 0; + static int currentStreak = 0; + messagePlayer(0, MESSAGE_DEBUG, "Mana: %d | Rolls: %d | Longest Streak: %d", mana, rolls, longestDryStreak); + if ( type == -1 ) + { + mana = 0; + rolls = 0; + longestDryStreak = 0; + currentStreak = 0; + stats[clientnum]->setProficiencyUnsafe(getSpellFromID(spellID)->skillID, 0); + return; + } + stats[clientnum]->setProficiencyUnsafe(getSpellFromID(spellID)->skillID, + std::max(getSpellFromID(spellID)->difficulty, stats[clientnum]->getProficiency(getSpellFromID(spellID)->skillID))); + for ( int i = 0; i < 10; ++i ) + { + int baseMana = getSpellFromID(spellID)->mana; + if ( argc > 2 ) + { + baseMana = atoi(argv[2]); + } + players[clientnum]->mechanics.baseSpellIncrementMP(baseMana, getSpellFromID(spellID)->skillID); + mana += baseMana; + //castSpell(players[clientnum]->entity->getUID(), getSpellFromID(spellID), false, false); + int prevLvl = stats[clientnum]->getProficiency(getSpellFromID(spellID)->skillID); + magicOnSpellCastEvent(players[clientnum]->entity, nullptr, nullptr, spellID, spell_t::SPELL_LEVEL_EVENT_DMG, 1); + if ( stats[clientnum]->getProficiency(getSpellFromID(spellID)->skillID) == prevLvl ) + { + ++currentStreak; + } + else + { + currentStreak = 0; + } + if ( currentStreak > longestDryStreak ) + { + longestDryStreak = currentStreak; + } + ++rolls; + } + } + }); + + static ConsoleCommand ccmd_gimmexp("/gimmexp", "give the player some XP (cheat)", []CCMD{ + if (!(svFlags & SV_FLAG_CHEATS)) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + if (players[clientnum] && players[clientnum]->entity) + { + players[clientnum]->entity->getStats()->EXP += 1 + local_rng.rand() % 50; + } + }); + + static ConsoleCommand ccmd_loadhudsettings("/loadhudsettings", "", []CCMD{ + loadHUDSettingsJSON(); + messagePlayer(clientnum, MESSAGE_MISC, "Reloaded HUD_settings.json"); + }); + + static ConsoleCommand ccmd_loadskillsheet("/loadskillsheet", "", []CCMD{ + Player::SkillSheet_t::loadSkillSheetJSON(); + messagePlayer(clientnum, MESSAGE_MISC, "Reloaded skillsheet_entries.json"); + }); + + static ConsoleCommand ccmd_loadcharsheet("/loadcharsheet", "", []CCMD{ + Player::CharacterSheet_t::loadCharacterSheetJSON(); + messagePlayer(clientnum, MESSAGE_MISC, "Reloaded charsheet_settings.json"); + }); + + static ConsoleCommand ccmd_loadfollowerwheel("/loadfollowerwheel", "", []CCMD{ + FollowerRadialMenu::loadFollowerJSON(); + messagePlayer(clientnum, MESSAGE_MISC, "Reloaded follower_wheel.json"); + }); static ConsoleCommand ccmd_loadcalloutwheel("/loadcalloutwheel", "", []CCMD{ CalloutRadialMenu::loadCalloutJSON(); @@ -3941,10 +4549,22 @@ namespace ConsoleCommands { messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); return; } +#ifndef NDEBUG if (players[clientnum]->entity) { - spellEffectPolymorph(players[clientnum]->entity, players[clientnum]->entity, true, TICKS_PER_SECOND * 60 * 2); + spellEffectPolymorph(players[clientnum]->entity, players[clientnum]->entity, true, TICKS_PER_SECOND * 60 * 5); + if ( argc > 1 ) + { + int type = std::min(NUMMONSTERS - 1, std::max((int)HUMAN, atoi(argv[1]))); + players[clientnum]->entity->effectPolymorph = type; + } + } +#else + if ( players[clientnum]->entity ) + { + spellEffectPolymorph(players[clientnum]->entity, players[clientnum]->entity, true, TICKS_PER_SECOND * 60 * 1); } +#endif }); static ConsoleCommand ccmd_sexchange("/sexchange", "fix yourself (cheat)", []CCMD{ @@ -4310,9 +4930,11 @@ namespace ConsoleCommands { } int cat = atoi(argv[1]); - cat = std::min(std::max(0, cat), NUMCATEGORIES - 1); + cat = std::min(std::max(0, cat), Category::CATEGORY_MAX - 2); ItemType type = itemLevelCurve((Category)cat, 0, currentlevel, local_rng); - dropItem(newItem(type, EXCELLENT, 0, 1, local_rng.rand(), true, &stats[clientnum]->inventory), 0); + Item* item = newItem(type, EXCELLENT, 0, 1, local_rng.rand(), true, &stats[clientnum]->inventory); + itemLevelCurvePostProcess(nullptr, item, local_rng); + dropItem(item, 0); }); static ConsoleCommand ccmd_spawnitem2("/spawnitem2", "spawn an item with beatitude and status (/spawnitem -2 5 wooden shield) (cheat)", []CCMD{ @@ -4566,99 +5188,360 @@ namespace ConsoleCommands { #endif }); - static ConsoleCommand ccmd_exportitemlang("/exportitemlang", "", []CCMD{ -#ifndef EDITOR -#ifndef NINTENDO - /*rapidjson::Document d; - d.SetObject(); - CustomHelpers::addMemberToRoot(d, "version", rapidjson::Value(1)); - CustomHelpers::addMemberToRoot(d, "items", rapidjson::Value(rapidjson::kObjectType)); - for ( int i = 0; i < NUMITEMS; ++i ) - { - d["item_names"].AddMember(rapidjson::Value(ItemTooltips.tmpItems[i].itemName.c_str(), d.GetAllocator()), rapidjson::Value(rapidjson::kObjectType), - d.GetAllocator()); - d["item_names"][ItemTooltips.tmpItems[i].itemName.c_str()].AddMember("name_identified", rapidjson::Value(items[i].name_identified, d.GetAllocator()), d.GetAllocator()); - d["item_names"][ItemTooltips.tmpItems[i].itemName.c_str()].AddMember("name_unidentified", rapidjson::Value(items[i].name_unidentified, d.GetAllocator()), d.GetAllocator()); - } - File* fp = FileIO::open("lang/item_names.json", "wb"); - if ( !fp ) + static ConsoleCommand ccmd_map_debug_stations("/map_debug_stations", "", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); return; } - rapidjson::StringBuffer os; - rapidjson::PrettyWriter writer(os); - d.Accept(writer); - fp->write(os.GetString(), sizeof(char), os.GetSize()); - FileIO::close(fp);*/ -#endif -#endif - }); - static ConsoleCommand ccmd_exportspelllang("/exportspelllang", "", []CCMD{ -#ifndef EDITOR -#ifndef NINTENDO - /*rapidjson::Document d; - d.SetObject(); - CustomHelpers::addMemberToRoot(d, "version", rapidjson::Value(1)); - CustomHelpers::addMemberToRoot(d, "spells", rapidjson::Value(rapidjson::kObjectType)); - for ( int i = 0; i < NUM_SPELLS; ++i ) + for ( int j = 0; j < 2; ++j ) + { + auto& floors_stations = (j == 0) ? treasure_room_generator.station_floors : treasure_room_generator.station_secret_floors; + for ( int i = 0; i <= 35; ++i ) { - if ( spell_t* spell = getSpellFromID(i) ) + if ( floors_stations.find(i) != floors_stations.end() ) { - d["spell_names"].AddMember(rapidjson::Value(ItemTooltips.spellItems[i].internalName.c_str(), d.GetAllocator()), rapidjson::Value(rapidjson::kObjectType), - d.GetAllocator()); - d["spell_names"][ItemTooltips.spellItems[i].internalName.c_str()].AddMember("name", rapidjson::Value(spell->getSpellName(), d.GetAllocator()), d.GetAllocator()); + messagePlayer(clientnum, MESSAGE_DEBUG, "[STATIONS]: [%d]: %s", i, floors_stations[i].c_str()); } } - File* fp = FileIO::open("lang/spell_names.json", "wb"); - if ( !fp ) - { - return; - } - rapidjson::StringBuffer os; - rapidjson::PrettyWriter writer(os); - d.Accept(writer); - fp->write(os.GetString(), sizeof(char), os.GetSize()); - FileIO::close(fp);*/ -#endif -#endif + } }); - static ConsoleCommand ccmd_spawndummy("/spawndummy", "", []CCMD{ + static ConsoleCommand ccmd_map_debug_door("/map_debug_door", "", []CCMD{ +#ifndef NINTENDO if ( !(svFlags & SV_FLAG_CHEATS) ) { messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); return; } - if ( multiplayer == CLIENT ) - { - messagePlayer(clientnum, MESSAGE_MISC, Language::get(284)); - return; - } - if ( players[clientnum]->entity ) + for ( auto f : directoryContents(".\\maps\\", false, true) ) { - if ( Entity* monster = summonMonster(DUMMYBOT, players[clientnum]->entity->x, players[clientnum]->entity->y) ) + std::string mapPath = "maps/"; + mapPath += f; + bool foundNumber = std::find_if(f.begin(), f.end(), ::isdigit) != f.end(); + if ( foundNumber && PHYSFS_getRealDir(mapPath.c_str()) ) { - if ( Stat* stat = monster->getStats() ) + int maphash = 0; + std::string fullMapPath = PHYSFS_getRealDir(mapPath.c_str()); + fullMapPath += PHYSFS_getDirSeparator(); + fullMapPath += mapPath; + loadMap(fullMapPath.c_str(), &map, map.entities, map.creatures, nullptr); + for ( node_t* node = map.entities->first; node; node = node->next ) { - stat->HP = 5000; - stat->MAXHP = 5000; - stat->CON = 0; - stat->LVL = 50; - stat->monsterForceAllegiance = Stat::MONSTER_FORCE_PLAYER_ENEMY; - serverUpdateEntityStatFlag(monster, 20); + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->sprite == 217 || entity->sprite == 218 ) + { + printlog("Iron door: Lockpick state: %d, opening: %d", entity->doorDisableLockpicks, entity->doorDisableOpening); + } + } } + // will crash the game but will show results of every map load :) } } - }); +#endif + }); - static ConsoleCommand ccmd_spawndummyhuman("/spawndummyhuman", "", []CCMD{ + static ConsoleCommand ccmd_map_debug_floor_interact("/map_debug_floor_interact", "", []CCMD{ +#ifndef NINTENDO if ( !(svFlags & SV_FLAG_CHEATS) ) { messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); return; } - if ( multiplayer == CLIENT ) + for ( auto f : directoryContents(".\\maps\\", false, true) ) + { + std::string mapPath = "maps/"; + mapPath += f; + /*bool foundNumber = std::find_if(f.begin(), f.end(), ::isdigit) != f.end();*/ + if ( /*foundNumber &&*/ PHYSFS_getRealDir(mapPath.c_str()) ) + { + int maphash = 0; + std::string fullMapPath = PHYSFS_getRealDir(mapPath.c_str()); + fullMapPath += PHYSFS_getDirSeparator(); + fullMapPath += mapPath; + loadMap(fullMapPath.c_str(), &map, map.entities, map.creatures, nullptr); + for ( node_t* node = map.entities->first; node; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->sprite == 127 && entity->floorDecorationInteractText1 != 0 ) + { + char buf[256] = ""; + int totalChars = 0; + for ( int i = 8; i < 60; ++i ) + { + if ( i == 28 ) // circuit_status + { + continue; + } + if ( entity->skill[i] != 0 ) + { + for ( int c = 0; c < 4; ++c ) + { + buf[totalChars] = static_cast((entity->skill[i] >> (c * 8)) & 0xFF); + //messagePlayer(0, "%d %d", i, c); + ++totalChars; + } + } + } + if ( buf[totalChars] != '\0' ) + { + buf[totalChars] = '\0'; + } + std::string output = buf; + printlog("Map: %s, floor interaction: %s", f.c_str(), output.c_str()); + } + } + } + // will crash the game but will show results of every map load :) + } + } +#endif + }); + + static ConsoleCommand ccmd_map_debug_treasure("/map_debug_treasure", "", []CCMD{ +#ifndef NINTENDO + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + std::map> treasureLoot; + for ( auto f : directoryContents(".\\maps\\", false, true) ) + { + std::string mapPath = "maps/"; + mapPath += f; + if ( mapPath.find("_lock") != std::string::npos && PHYSFS_getRealDir(mapPath.c_str()) ) + { + int maphash = 0; + std::string fullMapPath = PHYSFS_getRealDir(mapPath.c_str()); + fullMapPath += PHYSFS_getDirSeparator(); + fullMapPath += mapPath; + loadMap(fullMapPath.c_str(), &map, map.entities, map.creatures, nullptr); + auto& loot = treasureLoot[mapPath]; + for ( node_t* node = map.entities->first; node; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + int x = entity->x / 16; + int y = entity->y / 16; + std::string coord = "@[x:"; + coord += (std::to_string(x)); + coord += (",y:"); + coord += (std::to_string(y)); + coord += ("], "); + + if ( entity->sprite == 9 ) + { + loot.push_back(coord + "gold"); + } + else if ( entity->sprite == 8 ) + { + { + std::string item = "item, [x"; + item += std::to_string(std::max(1, entity->skill[13])); + item += "], "; + item += "("; + if ( entity->skill[12] == 10 ) + { + item += "+?"; + } + else + { + item += entity->skill[12] >= 0 ? "+" : ""; + item += std::to_string(entity->skill[12]); + } + item += "), "; + if ( entity->skill[11] == 0 ) + { + item += "[rand]"; + } + else + { + switch ( (Status)(entity->skill[11]) ) + { + case BROKEN: + item += "[broken]"; + break; + case DECREPIT: + item += "[decrepit]"; + break; + case WORN: + item += "[worn]"; + break; + case SERVICABLE: + item += "[serviceable]"; + break; + case EXCELLENT: + item += "[excellent]"; + break; + default: + item += "[?]"; + } + } + item += ", "; + if ( entity->skill[10] == 0 ) + { + item += "\"random item\""; + } + else if ( entity->skill[10] == 1 ) + { + if ( entity->skill[16] > 0 && entity->skill[16] <= 13 ) + { + static char itemCategoryNames[17][32] = + { + "random", + "weapon", + "armor", + "amulet", + "potion", + "scroll", + "magicstaff", + "ring", + "spellbook", + "gem", + "thrown", + "tool", + "food", + "book", + "equipment", + "jewelry", + "magical" + }; + item += std::string("\"category: ") + std::string(itemCategoryNames[entity->skill[16]]) + "\""; + } + else + { + item += std::string("\"random category\""); + } + } + else + { + item += "\""; + item += items[entity->skill[10] - 2].getIdentifiedName(); + item += "\""; + } + item += ", ("; + item += entity->skill[15] == 1 ? "id'd)" : "unid'd)"; + if ( entity->skill[10] > 1 ) + { + item += ", [value: " + std::to_string(items[entity->skill[10] - 2].gold_value) + "G]"; + } + else + { + item += ", [value: ?G]"; + } + loot.push_back(coord + item); + } + } + } + } + // will crash the game but will show results of every map load :) + for ( auto& s : loot ) + { + printlog("[%s], %s", mapPath.c_str(), s.c_str()); + } + } + } +#endif + }); + + static ConsoleCommand ccmd_exportitemlang("/exportitemlang", "", []CCMD{ +#ifndef EDITOR +#ifndef NINTENDO + /*rapidjson::Document d; + d.SetObject(); + CustomHelpers::addMemberToRoot(d, "version", rapidjson::Value(1)); + CustomHelpers::addMemberToRoot(d, "items", rapidjson::Value(rapidjson::kObjectType)); + for ( int i = 0; i < NUMITEMS; ++i ) + { + d["item_names"].AddMember(rapidjson::Value(ItemTooltips.tmpItems[i].itemName.c_str(), d.GetAllocator()), rapidjson::Value(rapidjson::kObjectType), + d.GetAllocator()); + d["item_names"][ItemTooltips.tmpItems[i].itemName.c_str()].AddMember("name_identified", rapidjson::Value(items[i].name_identified, d.GetAllocator()), d.GetAllocator()); + d["item_names"][ItemTooltips.tmpItems[i].itemName.c_str()].AddMember("name_unidentified", rapidjson::Value(items[i].name_unidentified, d.GetAllocator()), d.GetAllocator()); + } + File* fp = FileIO::open("lang/item_names.json", "wb"); + if ( !fp ) + { + return; + } + rapidjson::StringBuffer os; + rapidjson::PrettyWriter writer(os); + d.Accept(writer); + fp->write(os.GetString(), sizeof(char), os.GetSize()); + FileIO::close(fp);*/ +#endif +#endif + }); + + static ConsoleCommand ccmd_exportspelllang("/exportspelllang", "", []CCMD{ +#ifndef EDITOR +#ifndef NINTENDO + /*rapidjson::Document d; + d.SetObject(); + CustomHelpers::addMemberToRoot(d, "version", rapidjson::Value(1)); + CustomHelpers::addMemberToRoot(d, "spells", rapidjson::Value(rapidjson::kObjectType)); + for ( int i = 0; i < NUM_SPELLS; ++i ) + { + if ( spell_t* spell = getSpellFromID(i) ) + { + d["spell_names"].AddMember(rapidjson::Value(ItemTooltips.spellItems[i].internalName.c_str(), d.GetAllocator()), rapidjson::Value(rapidjson::kObjectType), + d.GetAllocator()); + d["spell_names"][ItemTooltips.spellItems[i].internalName.c_str()].AddMember("name", rapidjson::Value(spell->getSpellName(), d.GetAllocator()), d.GetAllocator()); + } + } + File* fp = FileIO::open("lang/spell_names.json", "wb"); + if ( !fp ) + { + return; + } + rapidjson::StringBuffer os; + rapidjson::PrettyWriter writer(os); + d.Accept(writer); + fp->write(os.GetString(), sizeof(char), os.GetSize()); + FileIO::close(fp);*/ +#endif +#endif + }); + + static ConsoleCommand ccmd_spawndummy("/spawndummy", "", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + if ( multiplayer == CLIENT ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(284)); + return; + } + if ( players[clientnum]->entity ) + { + if ( Entity* monster = summonMonster(DUMMYBOT, players[clientnum]->entity->x, players[clientnum]->entity->y) ) + { + if ( Stat* stat = monster->getStats() ) + { + stat->HP = 5000; + stat->MAXHP = 5000; + stat->CON = 0; + stat->LVL = 50; + stat->setAttribute("dummy_target", "0"); + stat->monsterForceAllegiance = Stat::MONSTER_FORCE_PLAYER_ENEMY; + serverUpdateEntityStatFlag(monster, 20); + } + } + } + }); + + static ConsoleCommand ccmd_spawndummyhuman("/spawndummyhuman", "", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + if ( multiplayer == CLIENT ) { messagePlayer(clientnum, MESSAGE_MISC, Language::get(284)); return; @@ -4674,7 +5557,8 @@ namespace ConsoleCommands { stat->CON = 0; stat->RANDOM_CON = 0; stat->LVL = 50; - stat->EFFECTS[EFF_STUNNED] = true; + stat->setEffectActive(EFF_STUNNED, 1); + stat->setAttribute("dummy_target", "0"); stat->monsterForceAllegiance = Stat::MONSTER_FORCE_PLAYER_ENEMY; serverUpdateEntityStatFlag(monster, 20); stat->EDITOR_ITEMS[ITEM_SLOT_HELM] = 0; @@ -4695,6 +5579,11 @@ namespace ConsoleCommands { static ConsoleCommand ccmd_mesh_collider_debug("/mesh_collider_debug", "", []CCMD{ node_t* tmpNode = NULL; Entity* tmpEnt = NULL; + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } for ( tmpNode = map.entities->first; tmpNode != NULL; tmpNode = tmpNode->next ) { tmpEnt = (Entity*)tmpNode->element; @@ -4709,8 +5598,241 @@ namespace ConsoleCommands { } }); + static ConsoleCommand ccmd_debug_claim_items("/debug_claim_items", "", []CCMD{ + node_t * tmpNode = NULL; + Entity* tmpEnt = NULL; + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + if ( multiplayer != CLIENT ) + { + node_t* nextnode = nullptr; + for ( tmpNode = map.entities->first; tmpNode != NULL; tmpNode = nextnode ) + { + nextnode = tmpNode->next; + tmpEnt = (Entity*)tmpNode->element; + if ( tmpEnt->behavior == &actItem ) + { + Item* item2 = newItemFromEntity(tmpEnt); + int pickedUpCount = item2->count; + Item* item = itemPickup(clientnum, item2); + if ( item ) + { + if ( players[clientnum]->isLocalPlayer() ) + { + // item is the new inventory stack for server, free the picked up items + free(item2); + } + list_RemoveNode(tmpEnt->mynode); + } + } + } + } + }); + + static ConsoleCommand ccmd_debug_heal_items("/debug_heal_items", "", []CCMD{ + node_t * tmpNode = NULL; + Entity* tmpEnt = NULL; + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + int healing = 0; + int mana = 0; + int items = 0; + int monsterinv = 0; + int monsterinvheal = 0; + int monsterinvmana = 0; + int chestinv = 0; + int chestheal = 0; + int chestmana = 0; + int monsterShopPrivStock = 0; + int monsterShopPrivStockHeal = 0; + int monsterShopPrivStockMana = 0; + int magicScrap = 0; + std::map allitems; + for ( tmpNode = map.entities->first; tmpNode != NULL; tmpNode = tmpNode->next ) + { + tmpEnt = (Entity*)tmpNode->element; + if ( tmpEnt->behavior == &actItem ) + { + if ( Item* item = newItemFromEntity(tmpEnt) ) + { + int metal = 0; + int magic = 0; + GenericGUIMenu::tinkeringGetItemValue(item, &metal, &magic); + magicScrap += magic; + allitems[item->type] += item->count; + if ( int heal = item->potionGetEffectHealth(players[clientnum]->entity, stats[clientnum]) ) + { + if ( item->type == POTION_EXTRAHEALING ) + { + if ( statGetCON(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += 4 * statGetCON(stats[clientnum], players[clientnum]->entity); + } + } + if ( item->type == POTION_HEALING ) + { + if ( statGetCON(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += 2 * statGetCON(stats[clientnum], players[clientnum]->entity); + } + } + + if ( item->type == POTION_RESTOREMAGIC ) + { + if ( statGetINT(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += std::min(30, 2 * statGetINT(stats[clientnum], players[clientnum]->entity)); // extra mana scaling from 1 to 15 INT, capped at +30 MP + } + mana += heal * item->count; + } + else + { + healing += heal * item->count; + } + items += item->count; + } + free(item); + } + } + else if ( tmpEnt->behavior == &actMonster ) + { + list_t* inventory = &tmpEnt->getStats()->inventory; + if ( inventory ) + { + for ( node_t* node = inventory->first; node; node = node->next ) + { + if ( Item* item = (Item*)node->element ) + { + int metal = 0; + int magic = 0; + GenericGUIMenu::tinkeringGetItemValue(item, &metal, &magic); + magicScrap += magic; + allitems[item->type] += item->count; + if ( int heal = item->potionGetEffectHealth(players[clientnum]->entity, stats[clientnum]) ) + { + if ( item->type == POTION_EXTRAHEALING ) + { + if ( statGetCON(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += 4 * statGetCON(stats[clientnum], players[clientnum]->entity); + } + } + if ( item->type == POTION_HEALING ) + { + if ( statGetCON(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += 2 * statGetCON(stats[clientnum], players[clientnum]->entity); + } + } + + if ( item->type == POTION_RESTOREMAGIC ) + { + if ( statGetINT(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += std::min(30, 2 * statGetINT(stats[clientnum], players[clientnum]->entity)); // extra mana scaling from 1 to 15 INT, capped at +30 MP + } + mana += heal * item->count; + monsterinvmana = heal * item->count; + if ( item->itemSpecialShopConsumable ) + { + monsterShopPrivStockMana += heal * item->count; + monsterShopPrivStock += item->count; + } + } + else + { + healing += heal * item->count; + monsterinvheal += heal * item->count; + if ( item->itemSpecialShopConsumable ) + { + monsterShopPrivStockHeal += heal * item->count; + monsterShopPrivStock += item->count; + } + } + items += item->count; + monsterinv += item->count; + } + } + } + } + } + else if ( tmpEnt->behavior == &actChest ) + { + list_t* inventory = tmpEnt->getChestInventoryList(); + if ( inventory ) + { + for ( node_t* node = inventory->first; node; node = node->next ) + { + if ( Item* item = (Item*)node->element ) + { + int metal = 0; + int magic = 0; + GenericGUIMenu::tinkeringGetItemValue(item, &metal, &magic); + magicScrap += magic; + allitems[item->type] += item->count; + if ( int heal = item->potionGetEffectHealth(players[clientnum]->entity, stats[clientnum]) ) + { + if ( item->type == POTION_EXTRAHEALING ) + { + if ( statGetCON(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += 4 * statGetCON(stats[clientnum], players[clientnum]->entity); + } + } + if ( item->type == POTION_HEALING ) + { + if ( statGetCON(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += 2 * statGetCON(stats[clientnum], players[clientnum]->entity); + } + } + + if ( item->type == POTION_RESTOREMAGIC ) + { + if ( statGetINT(stats[clientnum], players[clientnum]->entity) > 0 ) + { + heal += std::min(30, 2 * statGetINT(stats[clientnum], players[clientnum]->entity)); // extra mana scaling from 1 to 15 INT, capped at +30 MP + } + mana += heal * item->count; + chestmana += heal * item->count; + } + else + { + healing += heal * item->count; + chestheal += heal * item->count; + } + items += item->count; + chestinv += item->count; + } + } + } + } + } + } + messagePlayer(clientnum, MESSAGE_MISC, "Total Items: %d | heal: %d | mana: %d", items, healing, mana); + messagePlayer(clientnum, MESSAGE_MISC, "Monsters: %d | heal: %d | mana: %d", monsterinv, monsterinvheal, monsterinvmana); + messagePlayer(clientnum, MESSAGE_MISC, "Shop Specials: %d | heal: %d | mana: %d", monsterShopPrivStock, monsterShopPrivStockHeal, monsterShopPrivStockMana); + messagePlayer(clientnum, MESSAGE_MISC, "Chests: %d | heal: %d | mana: %d", chestinv, chestheal, chestmana); + for ( auto& i : allitems ) + { + messagePlayer(clientnum, MESSAGE_MISC, "[Gen]: %s : %d", ::items[i.first].getIdentifiedName(), i.second); + } + }); + static ConsoleCommand ccmd_mesh_collider_verify_and_crash_game("/mesh_collider_verify_and_crash_game", "", []CCMD{ #ifndef NINTENDO + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } for ( auto f : directoryContents(".\\maps\\", false, true) ) { std::string mapPath = "maps/"; @@ -4777,7 +5899,7 @@ namespace ConsoleCommands { const auto r = argc >= 2 ? strtof(argv[1], nullptr) : 1.f; const auto g = argc >= 3 ? strtof(argv[2], nullptr) : 1.f; const auto b = argc >= 4 ? strtof(argv[3], nullptr) : 1.f; - (void)lightSphereShadow(0, cameras[0].x, cameras[0].y, 4, r, g, b, 0.5f); + (void)lightSphereShadow(0, cameras[0].x, cameras[0].y, 4, r, g, b, 0.f, 0.5f); }); static ConsoleCommand ccmd_test_model("/test_model", "spawn an entity using a specific model", []CCMD{ @@ -4928,59 +6050,236 @@ namespace ConsoleCommands { } }); - static ConsoleCommand ccmd_reloadtiles("/reloadtiles", "reloads tile textures", []CCMD{ - generateTileTextures(); - }); - - static ConsoleCommand ccmd_spawnghost2("/respawnasghost2", "respawn", []CCMD{ + static ConsoleCommand ccmd_reloadtiles("/reloadtiles", "reloads tile textures", []CCMD{ + generateTileTextures(); + }); + + static ConsoleCommand ccmd_spawnghost2("/respawnasghost2", "respawn", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + if ( players[clientnum]->entity ) + { + return; + } + + players[clientnum]->ghost.respawn(); + }); + + static ConsoleCommand ccmd_spawnghost3("/respawnasghost3", "respawn as a ghost", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + /*if ( players[clientnum]->ghost.my ) + { + players[clientnum]->ghost.setActive(!players[clientnum]->ghost.isActive()); + return; + } + + if ( stats[clientnum]->HP > 0 ) + { + stats[clientnum]->HP = 0; + }*/ + + if ( players[clientnum]->entity ) + { + players[clientnum]->ghost.initTeleportLocations(players[clientnum]->entity->x / 16, players[clientnum]->entity->y / 16); + players[clientnum]->entity->skill[3] = 2; + players[clientnum]->entity->setEffect(EFF_PROJECT_SPIRIT, true, 30 * TICKS_PER_SECOND, false); + players[clientnum]->ghost.spawnGhost(); + } + }); + + static ConsoleCommand ccmd_spawnghost("/respawnasghost", "respawn as a ghost", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + if ( players[clientnum]->ghost.my ) + { + players[clientnum]->ghost.setActive(!players[clientnum]->ghost.isActive()); + return; + } + + if ( stats[clientnum]->HP > 0 ) + { + stats[clientnum]->HP = 0; + } + + if ( players[clientnum]->entity ) + { + players[clientnum]->ghost.initTeleportLocations(players[clientnum]->entity->x / 16, players[clientnum]->entity->y / 16); + } + else + { + players[clientnum]->ghost.initTeleportLocations(players[clientnum]->ghost.startRoomX, players[clientnum]->ghost.startRoomY); + } + players[clientnum]->ghost.spawnGhost(); + }); + + static ConsoleCommand ccmd_reloadequipmentoffsets("/reloadequipmentoffsets", "reloads equipment model offsets", []CCMD{ + EquipmentModelOffsets.readFromFile(monstertypename[stats[clientnum]->type]); + EquipmentModelOffsets.readBaseItemsFromFile(); + }); + + static ConsoleCommand ccmd_classstatrolls("/classstatrolls", "debug class stats", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + + if ( multiplayer != SINGLE ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(299)); + return; + } + if ( !players[clientnum]->entity ) { return; } + + for ( int i = 0; i < NUMCLASSES; ++i ) + { + stats[clientnum]->STR = 0; + stats[clientnum]->DEX = 0; + stats[clientnum]->CON = 0; + stats[clientnum]->INT = 0; + stats[clientnum]->PER = 0; + stats[clientnum]->CHR = 0; + for ( int lv = 0; lv < 50; ++lv ) + { + int increasestat[3] = { 0, 0, 0 }; + players[clientnum]->entity->playerStatIncrease(i, increasestat); + for ( int i = 0; i < 3; i++ ) + { + switch ( increasestat[i] ) + { + case STAT_STR: + stats[clientnum]->STR++; + break; + case STAT_DEX: + stats[clientnum]->DEX++; + break; + case STAT_CON: + stats[clientnum]->CON++; + break; + case STAT_INT: + stats[clientnum]->INT++; + break; + case STAT_PER: + stats[clientnum]->PER++; + break; + case STAT_CHR: + stats[clientnum]->CHR++; + break; + default: + break; + } + } + } + printlog("%d: %d %d %d %d %d %d", + i, + stats[clientnum]->STR, + stats[clientnum]->DEX, + stats[clientnum]->CON, + stats[clientnum]->INT, + stats[clientnum]->PER, + stats[clientnum]->CHR); + } + stats[clientnum]->STR = 0; + stats[clientnum]->DEX = 0; + stats[clientnum]->CON = 0; + stats[clientnum]->INT = 0; + stats[clientnum]->PER = 0; + stats[clientnum]->CHR = 0; + }); + + static ConsoleCommand ccmd_classstatrolls2("/classstatrolls2", "debug current class stats", []CCMD{ if ( !(svFlags & SV_FLAG_CHEATS) ) { messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); return; } - if ( players[clientnum]->entity ) - { - return; - } - - players[clientnum]->ghost.respawn(); - }); - - static ConsoleCommand ccmd_spawnghost("/respawnasghost", "respawn as a ghost", []CCMD{ - if ( !(svFlags & SV_FLAG_CHEATS) ) - { - messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); - return; - } - - if ( players[clientnum]->ghost.my ) + if ( multiplayer != SINGLE ) { - players[clientnum]->ghost.setActive(!players[clientnum]->ghost.isActive()); + messagePlayer(clientnum, MESSAGE_MISC, Language::get(299)); return; } + if ( !players[clientnum]->entity ) { return; } - if ( stats[clientnum]->HP > 0 ) - { - stats[clientnum]->HP = 0; - } - - if ( players[clientnum]->entity ) - { - players[clientnum]->ghost.initTeleportLocations(players[clientnum]->entity->x / 16, players[clientnum]->entity->y / 16); - } - else + //for ( int i = 0; i < NUMCLASSES; ++i ) { - players[clientnum]->ghost.initTeleportLocations(players[clientnum]->ghost.startRoomX, players[clientnum]->ghost.startRoomY); + stats[clientnum]->STR = 0; + stats[clientnum]->DEX = 0; + stats[clientnum]->CON = 0; + stats[clientnum]->INT = 0; + stats[clientnum]->PER = 0; + stats[clientnum]->CHR = 0; + for ( int lv = 0; lv < 50; ++lv ) + { + int increasestat[3] = { 0, 0, 0 }; + players[clientnum]->entity->playerStatIncrease(client_classes[clientnum], increasestat); + for ( int i = 0; i < 3; i++ ) + { + switch ( increasestat[i] ) + { + case STAT_STR: + stats[clientnum]->STR++; + break; + case STAT_DEX: + stats[clientnum]->DEX++; + break; + case STAT_CON: + stats[clientnum]->CON++; + break; + case STAT_INT: + stats[clientnum]->INT++; + break; + case STAT_PER: + stats[clientnum]->PER++; + break; + case STAT_CHR: + stats[clientnum]->CHR++; + break; + default: + break; + } + } + } + printlog("%d: %d %d %d %d %d %d", + client_classes[clientnum], + stats[clientnum]->STR, + stats[clientnum]->DEX, + stats[clientnum]->CON, + stats[clientnum]->INT, + stats[clientnum]->PER, + stats[clientnum]->CHR); + char buf[128]; + snprintf(buf, sizeof(buf), "%d %d %d %d %d %d", + stats[clientnum]->STR, + stats[clientnum]->DEX, + stats[clientnum]->CON, + stats[clientnum]->INT, + stats[clientnum]->PER, + stats[clientnum]->CHR); + SDL_SetClipboardText(buf); } - players[clientnum]->ghost.spawnGhost(); - }); - - static ConsoleCommand ccmd_reloadequipmentoffsets("/reloadequipmentoffsets", "reloads equipment model offsets", []CCMD{ - EquipmentModelOffsets.readFromFile(monstertypename[stats[clientnum]->type]); + stats[clientnum]->STR = 0; + stats[clientnum]->DEX = 0; + stats[clientnum]->CON = 0; + stats[clientnum]->INT = 0; + stats[clientnum]->PER = 0; + stats[clientnum]->CHR = 0; }); - static ConsoleCommand ccmd_classstatrolls("/classstatrolls", "debug class stats", []CCMD{ + static ConsoleCommand ccmd_classstatrolls3("/classstatrolls3", "debug current class stats", []CCMD{ if ( !(svFlags & SV_FLAG_CHEATS) ) { messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); @@ -4994,7 +6293,11 @@ namespace ConsoleCommands { } if ( !players[clientnum]->entity ) { return; } - for ( int i = 0; i < NUMCLASSES; ++i ) + int numStatRolls = 0; + int attr[6] = { 0, 0, 0, 0, 0, 0 }; + int attr2[6] = { 0, 0, 0, 0, 0, 0 }; + Monster prevType = stats[clientnum]->type; + for ( int i = 0; i < 1000; ++i ) { stats[clientnum]->STR = 0; stats[clientnum]->DEX = 0; @@ -5005,7 +6308,40 @@ namespace ConsoleCommands { for ( int lv = 0; lv < 50; ++lv ) { int increasestat[3] = { 0, 0, 0 }; - players[clientnum]->entity->playerStatIncrease(i, increasestat); + stats[clientnum]->type = prevType; + players[clientnum]->entity->playerStatIncrease(client_classes[clientnum], increasestat); + for ( int i = 0; i < 3; i++ ) + { + switch ( increasestat[i] ) + { + case STAT_STR: + stats[clientnum]->STR++; + break; + case STAT_DEX: + stats[clientnum]->DEX++; + break; + case STAT_CON: + stats[clientnum]->CON++; + break; + case STAT_INT: + stats[clientnum]->INT++; + break; + case STAT_PER: + stats[clientnum]->PER++; + break; + case STAT_CHR: + stats[clientnum]->CHR++; + break; + default: + break; + } + ++attr[increasestat[i]]; + ++numStatRolls; + } + + increasestat[0] = 0; increasestat[1] = 0; increasestat[2] = 0; + stats[clientnum]->type = HUMAN; + players[clientnum]->entity->playerStatIncrease(client_classes[clientnum], increasestat); for ( int i = 0; i < 3; i++ ) { switch ( increasestat[i] ) @@ -5031,17 +6367,40 @@ namespace ConsoleCommands { default: break; } + ++attr2[increasestat[i]]; + //++numStatRolls; } } - printlog("%d: %d %d %d %d %d %d", - i, - stats[clientnum]->STR, - stats[clientnum]->DEX, - stats[clientnum]->CON, - stats[clientnum]->INT, - stats[clientnum]->PER, - stats[clientnum]->CHR); } + char buf[128]; + snprintf(buf, sizeof(buf), "%.2f %.2f %.2f %.2f %.2f %.2f", + 3 * attr[0] / float(numStatRolls), + 3 * attr[1] / float(numStatRolls), + 3 * attr[2] / float(numStatRolls), + 3 * attr[3] / float(numStatRolls), + 3 * attr[4] / float(numStatRolls), + 3 * attr[5] / float(numStatRolls)); + messagePlayer(clientnum, MESSAGE_STATUS, "%s", buf); + + snprintf(buf, sizeof(buf), "%.2f %.2f %.2f %.2f %.2f %.2f", + 3 * attr2[0] / float(numStatRolls), + 3 * attr2[1] / float(numStatRolls), + 3 * attr2[2] / float(numStatRolls), + 3 * attr2[3] / float(numStatRolls), + 3 * attr2[4] / float(numStatRolls), + 3 * attr2[5] / float(numStatRolls)); + messagePlayer(clientnum, MESSAGE_STATUS, "%s", buf); + + snprintf(buf, sizeof(buf), "%.2f%% %.2f%% %.2f%% %.2f%% %.2f%% %.2f%%", + 100.0 * 3 * (attr[0] - attr2[0]) / float(numStatRolls), + 100.0 * 3 * (attr[1] - attr2[1]) / float(numStatRolls), + 100.0 * 3 * (attr[2] - attr2[2]) / float(numStatRolls), + 100.0 * 3 * (attr[3] - attr2[3]) / float(numStatRolls), + 100.0 * 3 * (attr[4] - attr2[4]) / float(numStatRolls), + 100.0 * 3 * (attr[5] - attr2[5]) / float(numStatRolls)); + messagePlayer(clientnum, MESSAGE_STATUS, "%s", buf); + + SDL_SetClipboardText(buf); stats[clientnum]->STR = 0; stats[clientnum]->DEX = 0; stats[clientnum]->CON = 0; @@ -5165,6 +6524,12 @@ namespace ConsoleCommands { case 166: monsterType = GYROBOT; break; case 188: monsterType = BAT_SMALL; break; case 189: monsterType = BUGBEAR; break; + case 204: monsterType = DRYAD; break; + case 205: monsterType = MYCONID; break; + case 206: monsterType = SALAMANDER; break; + case 207: monsterType = GREMLIN; break; + case 246: monsterType = REVENANT_SKULL; break; + case 247: monsterType = MONSTER_ADORCISED_WEAPON; break; default: break; } @@ -5179,5 +6544,260 @@ namespace ConsoleCommands { } #endif }); + + static ConsoleCommand ccmd_shader_test("/shader_test", "", []CCMD{ + { + std::string filePath = "/data/shaders/"; + filePath.append("sprite"); + if ( filePath.find(".json") == std::string::npos ) + { + filePath.append(".json"); + } + if ( PHYSFS_getRealDir(filePath.c_str()) ) + { + std::string inputPath = PHYSFS_getRealDir(filePath.c_str()); + inputPath.append(filePath); + + File* fp = FileIO::open(inputPath.c_str(), "rb"); + if ( !fp ) + { + printlog("[JSON]: Error: Could not locate json file %s", inputPath.c_str()); + return; + } + + char buf[65536]; + int count = fp->read(buf, sizeof(buf[0]), sizeof(buf)); + buf[count] = '\0'; + rapidjson::StringStream is(buf); + FileIO::close(fp); + + rapidjson::Document d; + d.ParseStream(is); + + if ( !d.IsObject() || !d.HasMember("version") ) + { + printlog("[JSON]: Error: No 'version' value in json file, or JSON syntax incorrect! %s", inputPath.c_str()); + return; + } + + std::string vertex; + std::string fragment; + + for ( auto itr = d["vertex"].Begin(); itr != d["vertex"].End(); ++itr ) + { + if ( vertex.size() != 0 ) + { + vertex += '\n'; + } + vertex += itr->GetString(); + } + for ( auto itr = d["fragment"].Begin(); itr != d["fragment"].End(); ++itr ) + { + if ( fragment.size() != 0 ) + { + fragment += '\n'; + } + fragment += itr->GetString(); + } + + auto& shader = spriteBrightShader; + shader.destroy(); + shader.init("spriteBrightShader"); + + shader.compile(vertex.c_str(), vertex.size(), Shader::Type::Vertex); + shader.compile(fragment.c_str(), fragment.size(), Shader::Type::Fragment); + shader.bindAttribLocation("iPosition", 0); + shader.bindAttribLocation("iTexCoord", 1); + shader.bindAttribLocation("iColor", 2); + shader.link(); + shader.bind(); + GL_CHECK_ERR(glUniform1i(shader.uniform("uTexture"), 0)); + GL_CHECK_ERR(glUniform1i(shader.uniform("uLightmap"), 1)); + } + } + { + std::string filePath = "/data/shaders/"; + filePath.append("voxel"); + if ( filePath.find(".json") == std::string::npos ) + { + filePath.append(".json"); + } + if ( PHYSFS_getRealDir(filePath.c_str()) ) + { + std::string inputPath = PHYSFS_getRealDir(filePath.c_str()); + inputPath.append(filePath); + + File* fp = FileIO::open(inputPath.c_str(), "rb"); + if ( !fp ) + { + printlog("[JSON]: Error: Could not locate json file %s", inputPath.c_str()); + return; + } + + char buf[65536]; + int count = fp->read(buf, sizeof(buf[0]), sizeof(buf)); + buf[count] = '\0'; + rapidjson::StringStream is(buf); + FileIO::close(fp); + + rapidjson::Document d; + d.ParseStream(is); + + if ( !d.IsObject() || !d.HasMember("version") ) + { + printlog("[JSON]: Error: No 'version' value in json file, or JSON syntax incorrect! %s", inputPath.c_str()); + return; + } + + std::string vertex; + std::string fragment; + + for ( auto itr = d["vertex"].Begin(); itr != d["vertex"].End(); ++itr ) + { + if ( vertex.size() != 0 ) + { + vertex += '\n'; + } + vertex += itr->GetString(); + } + for ( auto itr = d["fragment"].Begin(); itr != d["fragment"].End(); ++itr ) + { + if ( fragment.size() != 0 ) + { + fragment += '\n'; + } + fragment += itr->GetString(); + } + + auto& shader = voxelShader; + shader.destroy(); + shader.init("voxelShader"); + + shader.compile(vertex.c_str(), vertex.size(), Shader::Type::Vertex); + shader.compile(fragment.c_str(), fragment.size(), Shader::Type::Fragment); + shader.bindAttribLocation("iPosition", 0); + shader.bindAttribLocation("iColor", 1); + shader.bindAttribLocation("iNormal", 2); + shader.link(); + shader.bind(); + GL_CHECK_ERR(glUniform1i(shader.uniform("uLightmap"), 1)); + GL_CHECK_ERR(glUniform1i(shader.uniform("uTexturemap"), 2)); + } + } + }); + + real_t testPFromC(real_t c, int iterations, int chance) + { + int rollsRequired = 0; + int max_n = 0; + std::map buckets; + for ( int i = 0; i < iterations; ++i ) + { + int n = 1; + while ( c * n < 1.0 ) + { + real_t roll = (local_rng.rand() % 100) / 100.0; + if ( roll <= (c * n) || n >= 20 ) + { + break; + } + ++n; + } + max_n = std::max(max_n, n); + buckets[n / 5]++; + rollsRequired += n; + } + real_t res = rollsRequired / (real_t)iterations; + messagePlayer(0, MESSAGE_DEBUG, "Rolls avg: %.2f%%, max_n: %d", 100.0 / (rollsRequired / (real_t)iterations), max_n); + for ( int i = 0; i < buckets.size(); ++i ) + { + messagePlayer(0, MESSAGE_DEBUG, "Bucket: %d-%d: %.2f", i, i + 4, buckets[i] / (real_t)iterations); + } + + rollsRequired = 0; + int n = 1; + max_n = 0; + for ( int i = 0; i < iterations; ++i ) + { + bool success = players[0]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_DEFAULT, chance); + if ( success ) + { + ++rollsRequired; + max_n = std::max(max_n, n); + n = 0; + } + else + { + ++n; + } + } + + messagePlayer(0, MESSAGE_DEBUG, "Rolls new: %.2f%%, max_n: %d", (100.0 * rollsRequired / (real_t)iterations), max_n); + + return res; + } + + static ConsoleCommand ccmd_test_rand_prng("/test_rand_prng", "", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + if ( argc < 2 ) + { + return; + } + std::string str_c = argv[1]; + real_t c = std::stof(str_c); + int chance = 5; + if ( argc >= 3 ) + { + chance = atoi(argv[2]); + } + testPFromC(c, 100000, chance); + }); + + static ConsoleCommand ccmd_appraisal_table("/appraisal_table", "", []CCMD{ + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(277)); + return; + } + std::vector> appraisalValues; + std::set sortedItems; + Item* item = newItem(WOODEN_SHIELD, EXCELLENT, 0, 0, 0, false, nullptr); + for ( int i = 0; i < NUMITEMS; ++i ) + { + item->type = (ItemType)i; + for ( int skill = 0; skill <= 100; ++skill ) + { + stats[0]->setProficiencyUnsafe(PRO_APPRAISAL, 0); + stats[0]->PER = skill; + if ( players[0]->inventoryUI.appraisal.appraisalPossible(item) ) + { + appraisalValues.push_back(std::make_tuple(skill, players[0]->inventoryUI.appraisal.getAppraisalTime(item), i)); + break; + } + } + } + + std::sort(appraisalValues.begin(), appraisalValues.end()); + for ( auto& pair : appraisalValues ) + { + printlog("%d, %.2f, %d, %d, \"%s\"", std::get<0>(pair), std::get<1>(pair) / (real_t)TICKS_PER_SECOND, items[std::get<2>(pair)].gold_value, (int)items[std::get<2>(pair)].category, items[std::get<2>(pair)].getIdentifiedName()); + } + + /*for ( auto& pair : allGameSpells ) + { + printlog("\"template_icon_%s\": [\"%%+d Magic Damage\"],", pair.second->spell_internal_name); + printlog("\"template_desc_%s\": [\"Press[Cast] while equipped\", \"to cast the[Projectile].\"],", pair.second->spell_internal_name); + }*/ + + free(item); + }); + + static ConsoleCommand ccmd_reloadappraisal("/reloadappraisal", "reloads appraisal entries", []CCMD{ + Player::Inventory_t::Appraisal_t::readFromFile(); + }); } diff --git a/src/interface/drawminimap.cpp b/src/interface/drawminimap.cpp index 39d22b217..af4a0fd95 100644 --- a/src/interface/drawminimap.cpp +++ b/src/interface/drawminimap.cpp @@ -144,6 +144,13 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) for ( node_t* node = map.entities->first; node != NULL; node = node->next ) { Entity* entity = (Entity*)node->element; + if ( entity->flags[SPRITE] ) + { + if ( entity->getEntityShowOnMapDuration() == 0 ) + { + continue; + } + } if ( entity->sprite == 161 || (entity->sprite >= 254 && entity->sprite < 258) || entity->behavior == &actCustomPortal ) // ladder or portal models { @@ -205,16 +212,16 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) { entityPointsOfInterest.push_back(entity); } - else if ( entity->entityShowOnMap > 0 ) + else if ( entity->getEntityShowOnMapDuration() > 0 ) { entityPointsOfInterest.push_back(entity); } } - if ( entity->entityShowOnMap > 0 && lastMapTick != ticks ) + if ( entity->getEntityShowOnMapDuration() > 0 && lastMapTick != ticks ) { // only decrease the entities' shown duration when the global game timer passes a tick // (drawMinimap doesn't follow game tick intervals) - --entity->entityShowOnMap; + entity->entityShowOnMapTickDuration(); } } @@ -499,7 +506,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) { int x = floor(entity->x / 16); int y = floor(entity->y / 16); - if ( minimap[y][x] || (entity->entityShowOnMap > 0 && !(entity->behavior == &actCustomPortal)) ) + if ( minimap[y][x] || (entity->getEntityShowOnMapDuration() > 0 && !(entity->behavior == &actCustomPortal)) ) { if ( ticks % 40 - ticks % 20 ) { @@ -572,7 +579,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) if ( (players[i] && players[i]->entity && players[i]->entity->creatureShadowTaggedThisUid == entity->getUID()) - || (entity->getStats() && entity->getStats()->EFFECTS[EFF_SHADOW_TAGGED]) ) + || (entity->getStats() && entity->getStats()->getEffectActive(EFF_SHADOW_TAGGED)) ) { warningEffect = true; int x = std::min(std::max(0, entity->x / 16), map.width - 1); @@ -587,7 +594,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) const int i = player; if ( (players[i] && players[i]->entity && players[i]->entity->creatureShadowTaggedThisUid == entity->getUID()) - || (entity->getStats() && entity->getStats()->EFFECTS[EFF_SHADOW_TAGGED]) ) + || (entity->getStats() && entity->getStats()->getEffectActive(EFF_SHADOW_TAGGED)) ) { warningEffect = true; int x = std::min(std::max(0, entity->x / 16), map.width - 1); @@ -605,7 +612,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) if ( !players[i]->isLocalPlayer() || client_disconnected[i] ) { continue; } if ( (stats[i]->ring && stats[i]->ring->type == RING_WARNING) - || (entity->entityShowOnMap > 0) ) + || (entity->getEntityShowOnMapDuration() > 0) ) { int beatitude = 0; if ( stats[i]->ring && stats[i]->ring->type == RING_WARNING ) @@ -619,7 +626,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) } bool doEffect = false; - if ( entity->entityShowOnMap > 0 ) + if ( entity->getEntityShowOnMapDuration() > 0 ) { doEffect = true; } @@ -632,7 +639,24 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) { int x = std::min(std::max(0, entity->x / 16), map.width - 1); int y = std::min(std::max(0, entity->y / 16), map.height - 1); - drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(191, 127, 191, 255)); + if ( entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_SCRY ) + { + if ( ticks % 40 - ticks % 20 ) + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(200, 200, 255, 255)); + } + } + else + { + if ( entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_PINPOINT ) + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(240, 228, 66, 255)); + } + else + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(191, 127, 191, 255)); + } + } warningEffect = true; break; } @@ -643,7 +667,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) { const int i = player; if ( (stats[i]->ring && stats[i]->ring->type == RING_WARNING) - || (entity->entityShowOnMap > 0) ) + || (entity->getEntityShowOnMapDuration() > 0) ) { int beatitude = 0; if ( stats[i]->ring && stats[i]->ring->type == RING_WARNING ) @@ -657,7 +681,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) } bool doEffect = false; - if ( entity->entityShowOnMap > 0 ) + if ( entity->getEntityShowOnMapDuration() > 0 ) { doEffect = true; } @@ -670,7 +694,24 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) { int x = std::min(std::max(0, entity->x / 16), map.width - 1); int y = std::min(std::max(0, entity->y / 16), map.height - 1); - drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(191, 127, 191, 255)); + if ( entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_SCRY ) + { + if ( ticks % 40 - ticks % 20 ) + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(200, 200, 255, 255)); + } + } + else + { + if ( entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_PINPOINT ) + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(240, 228, 66, 255)); + } + else + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(191, 127, 191, 255)); + } + } warningEffect = true; } } @@ -693,7 +734,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) && players[i] && players[i]->entity && entityDist(players[i]->entity, entity) < 16.0 * 20 ) { - entity->entityShowOnMap = std::max(entity->entityShowOnMap, TICKS_PER_SECOND * 5); + entity->setEntityShowOnMap(Entity::SHOW_MAP_DEFAULT, std::max(entity->getEntityShowOnMapDuration(), TICKS_PER_SECOND * 5)); int x = std::min(std::max(0, entity->x / 16), map.width - 1); int y = std::min(std::max(0, entity->y / 16), map.height - 1); drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(191, 127, 191, 255)); @@ -716,7 +757,7 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) && players[i] && players[i]->entity && entityDist(players[i]->entity, entity) < 16.0 * 20 ) { - entity->entityShowOnMap = std::max(entity->entityShowOnMap, TICKS_PER_SECOND * 5); + entity->setEntityShowOnMap(Entity::SHOW_MAP_DEFAULT, std::max(entity->getEntityShowOnMapDuration(), TICKS_PER_SECOND * 5)); int x = std::min(std::max(0, entity->x / 16), map.width - 1); int y = std::min(std::max(0, entity->y / 16), map.height - 1); drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(191, 127, 191, 255)); @@ -779,13 +820,32 @@ void drawMinimap(const int player, SDL_Rect rect, bool drawingSharedMap) drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(240, 228, 66, 255)); } } - else if ( entity->entityShowOnMap > 0 ) + else if ( entity->getEntityShowOnMapDuration() > 0 ) { int x = std::min(std::max(0, entity->x / 16), map.width - 1); int y = std::min(std::max(0, entity->y / 16), map.height - 1); - if ( ticks % 40 - ticks % 20 ) + if ( entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_DETECT_MONSTER ) { - drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(255, 168, 200, 255)); + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(191, 127, 191, 255)); + } + else if ( entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_PINPOINT ) + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(240, 228, 66, 255)); + } + else if ( (ticks % 40 - ticks % 20) ) + { + if ( entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_SCRY ) + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(200, 200, 255, 255)); + } + else if ( entity->getEntityShowOnMapSource() == Entity::SHOW_MAP_DONATION ) + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(240, 228, 66, 255)); + } + else + { + drawCircleMesh((real_t)x + 0.5, (real_t)y + 0.5, (real_t)1.0, rect, makeColor(255, 168, 200, 255)); + } } } } diff --git a/src/interface/drawstatus.cpp b/src/interface/drawstatus.cpp index 3445af95a..565cdf335 100644 --- a/src/interface/drawstatus.cpp +++ b/src/interface/drawstatus.cpp @@ -57,8 +57,31 @@ void updateEnemyBar(Entity* source, Entity* target, const char* name, Sint32 hp, if ( player == -1 ) { - if ( source->behavior == &actMonster && source->monsterAllySummonRank != 0 - && (target->behavior == &actMonster || target->behavior == &actPlayer) ) + if ( target->behavior == &actMonster ) + { + if ( Stat* stats = target->getStats() ) + { + if ( Uint8 effectStrength = stats->getEffectActive(EFF_DETECT_ENEMY) ) + { + if ( effectStrength >= 1 && effectStrength < 1 + MAXPLAYERS ) + { + player = effectStrength - 1; + } + } + } + } + if ( source->behavior == &actMonster && source->getStats() && (source->getStats()->type == DUCK_SMALL || (source->getStats()->type == MONSTER_ADORCISED_WEAPON + && source->getStats()->getAttribute("spirit_weapon") != "")) ) // special non-follower list followers + { + Entity* parent = uidToEntity(source->parent); + if ( parent && parent->behavior == &actPlayer && parent != target ) + { + player = parent->skill[2]; // don't update enemy bar if attacking leader. + } + } + else if ( source->behavior == &actMonster && source->monsterAllySummonRank != 0 + && (target->behavior == &actMonster || target->behavior == &actPlayer || target->behavior == &actDoor || target->behavior == &actIronDoor + || target->behavior == &actChest || target->behavior == &actFurniture || target->behavior == &actColliderDecoration) ) { player = source->monsterAllyIndex; if ( source->monsterAllyGetPlayerLeader() && source->monsterAllyGetPlayerLeader() == target ) @@ -75,7 +98,8 @@ void updateEnemyBar(Entity* source, Entity* target, const char* name, Sint32 hp, } } else if ( source->behavior == &actMonster && source->monsterAllyIndex >= 0/*monsterIsImmobileTurret(source, nullptr)*/ - && (target->behavior == &actMonster || target->behavior == &actPlayer || target->behavior == &actDoor) ) + && (target->behavior == &actMonster || target->behavior == &actPlayer || target->behavior == &actDoor || target->behavior == &actIronDoor + || target->behavior == &actChest || target->behavior == &actFurniture || target->behavior == &actColliderDecoration) ) { player = source->monsterAllyIndex; if ( source->monsterAllyGetPlayerLeader() && source->monsterAllyGetPlayerLeader() == target ) @@ -116,6 +140,11 @@ void updateEnemyBar(Entity* source, Entity* target, const char* name, Sint32 hp, } } + if ( player >= 0 && target == source ) + { + return; + } + int oldhp = 0; if ( stats ) { @@ -123,7 +152,7 @@ void updateEnemyBar(Entity* source, Entity* target, const char* name, Sint32 hp, } else { - if ( target->behavior == &actDoor ) + if ( target->behavior == &actDoor || target->behavior == &actIronDoor ) { oldhp = target->doorOldHealth; } @@ -145,2043 +174,1814 @@ void updateEnemyBar(Entity* source, Entity* target, const char* name, Sint32 hp, } } - if ( !EnemyHPDamageBarHandler::bDamageGibTypesEnabled ) - { - gibType = DamageGib::DMG_DEFAULT; - } - - EnemyHPDamageBarHandler::EnemyHPDetails* details = nullptr; - if ( player >= 0 /*&& players[player]->isLocalPlayer()*/ ) - { - // add enemy bar to the server - int p = player; - if ( !players[player]->isLocalPlayer() ) - { - p = clientnum; // remote clients, add it to the local list. - } - if ( splitscreen ) // check if anyone else has this enemy bar on their screen - { - for ( int i = 0; i < MAXPLAYERS; ++i ) - { - if ( players[i]->isLocalPlayer() ) - { - if ( enemyHPDamageBarHandler[i].HPBars.find(target->getUID()) != enemyHPDamageBarHandler[i].HPBars.end() ) - { - p = i; - break; - } - } - } - } - if ( stats ) - { - details = enemyHPDamageBarHandler[p].addEnemyToList(hp, maxhp, oldhp, target->getUID(), name, lowPriorityTick, gibType); - } - else - { - details = enemyHPDamageBarHandler[p].addEnemyToList(hp, maxhp, oldhp, target->getUID(), name, lowPriorityTick, gibType); - } - } - - if ( player >= 0 && multiplayer == SERVER ) - { - // send to all remote players - for ( int p = 1; p < MAXPLAYERS; ++p ) - { - if ( !players[p]->isLocalPlayer() ) - { - if ( p == playertarget ) - { - continue; - } - strcpy((char*)net_packet->data, "ENHP"); - SDLNet_Write16(static_cast(hp), &net_packet->data[4]); - SDLNet_Write16(static_cast(maxhp), &net_packet->data[6]); - if ( stats ) - { - SDLNet_Write16(static_cast(oldhp), &net_packet->data[8]); - } - else - { - SDLNet_Write16(static_cast(oldhp), &net_packet->data[8]); - } - SDLNet_Write32(target->getUID(), &net_packet->data[10]); - net_packet->data[14] = lowPriorityTick ? 1 : 0; // 1 == true - if ( EnemyHPDamageBarHandler::bDamageGibTypesEnabled ) - { - net_packet->data[14] |= (gibType << 1) & 0xFE; - } - if ( stats && details ) - { - SDLNet_Write32(details->enemy_statusEffects1, &net_packet->data[15]); - SDLNet_Write32(details->enemy_statusEffects2, &net_packet->data[19]); - SDLNet_Write32(details->enemy_statusEffectsLowDuration1, &net_packet->data[23]); - SDLNet_Write32(details->enemy_statusEffectsLowDuration2, &net_packet->data[27]); - } - else - { - SDLNet_Write32(0, &net_packet->data[15]); - SDLNet_Write32(0, &net_packet->data[19]); - SDLNet_Write32(0, &net_packet->data[23]); - SDLNet_Write32(0, &net_packet->data[27]); - } - strcpy((char*)(&net_packet->data[31]), name); - net_packet->data[31 + strlen(name)] = 0; - net_packet->address.host = net_clients[p - 1].host; - net_packet->address.port = net_clients[p - 1].port; - net_packet->len = 31 + strlen(name) + 1; - sendPacketSafe(net_sock, -1, net_packet, p - 1); - - } - } - } -} - -/*------------------------------------------------------------------------------- - - drawStatus - - Draws various status bar elements, such as textbox, health, magic, - and the hotbar - --------------------------------------------------------------------------------*/ - -bool mouseInBoundsRealtimeCoords(int, int, int, int, int); //Defined in playerinventory.cpp. Dirty hack, you should be ashamed of yourself. - -bool warpMouseToSelectedHotbarSlot(const int player) -{ - if ( players[player]->shootmode == true) - { - return false; - } - - if ( auto hotbarSlotFrame = players[player]->hotbar.getHotbarSlotFrame(players[player]->hotbar.current_hotbar) ) - { - if ( !players[player]->hotbar.isInteractable ) - { - players[player]->inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_HOTBAR; - players[player]->inventoryUI.cursor.queuedFrameToWarpTo = hotbarSlotFrame; - return false; - } - else - { - //messagePlayer(0, "[Debug]: select item warped"); - players[player]->inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_NONE; - players[player]->inventoryUI.cursor.queuedFrameToWarpTo = nullptr; - hotbarSlotFrame->warpMouseToFrame(player, (Inputs::SET_CONTROLLER)); - } - return true; - } - - return false; -} - -void drawHPMPBars(int player) -{ - const int x1 = players[player]->camera_x1(); - const int x2 = players[player]->camera_x2(); - const int y1 = players[player]->camera_y1(); - const int y2 = players[player]->camera_y2(); - const int hpmpbarOffsets = 0; - - int playerStatusBarWidth = 38 * uiscale_playerbars; - int playerStatusBarHeight = 156 * uiscale_playerbars; - - SDL_Rect pos; - - // PLAYER HEALTH BAR - // Display Health bar border - pos.x = x1 + 38 + 38 * uiscale_playerbars; - pos.w = playerStatusBarWidth; - pos.h = playerStatusBarHeight; - pos.y = y2 - (playerStatusBarHeight + 12) - hpmpbarOffsets; - drawTooltip(&pos); - if ( stats[player] && stats[player]->HP > 0 - && stats[player]->EFFECTS[EFF_HP_REGEN] ) - { - bool lowDurationFlash = !((ticks % 50) - (ticks % 25)); - bool lowDuration = stats[player]->EFFECTS_TIMERS[EFF_HP_REGEN] > 0 && - (stats[player]->EFFECTS_TIMERS[EFF_HP_REGEN] < TICKS_PER_SECOND * 5); - if ( (lowDuration && !lowDurationFlash) || !lowDuration ) - { - if ( colorblind ) - { - drawTooltip(&pos, makeColorRGB(0, 255, 255)); // blue - } - else - { - drawTooltip(&pos, makeColorRGB(0, 255, 0)); // green - } - } - } - - // Display "HP" at top of Health bar - ttfPrintText(ttf12, pos.x + (playerStatusBarWidth / 2 - 10), pos.y + 6, Language::get(306)); - - // Display border between actual Health bar and "HP" - //pos.x = 76; - pos.w = playerStatusBarWidth; - pos.h = 0; - pos.y = y2 - (playerStatusBarHeight - 9) - hpmpbarOffsets; - drawTooltip(&pos); - if ( stats[player] && stats[player]->HP > 0 - && stats[player]->EFFECTS[EFF_HP_REGEN] ) - { - bool lowDurationFlash = !((ticks % 50) - (ticks % 25)); - bool lowDuration = stats[player]->EFFECTS_TIMERS[EFF_HP_REGEN] > 0 && - (stats[player]->EFFECTS_TIMERS[EFF_HP_REGEN] < TICKS_PER_SECOND * 5); - if ( (lowDuration && !lowDurationFlash) || !lowDuration ) - { - if ( colorblind ) - { - drawTooltip(&pos, makeColorRGB(0, 255, 255)); // blue - } - else - { - drawTooltip(&pos, makeColorRGB(0, 255, 0)); // green - } - } - } - - // Display the actual Health bar's faint background - pos.x = x1 + 42 + 38 * uiscale_playerbars; - pos.w = playerStatusBarWidth - 5; - pos.h = playerStatusBarHeight - 27; - pos.y = y2 - 15 - pos.h - hpmpbarOffsets; - - // Change the color depending on if you are poisoned - Uint32 color = 0; - if ( stats[player] && stats[player]->EFFECTS[EFF_POISONED] ) - { - if ( colorblind ) - { - color = makeColorRGB(0, 0, 48); // Display blue - } - else - { - color = makeColorRGB(0, 48, 0); // Display green - } - } - else - { - color = makeColorRGB(48, 0, 0); // Display red - } - - // Draw the actual Health bar's faint background with specified color - drawRect(&pos, color, 255); - - // If the Player is alive, base the size of the actual Health bar off remaining HP - if ( stats[player] && stats[player]->HP > 0 ) - { - //pos.x = 80; - pos.w = playerStatusBarWidth - 5; - pos.h = (playerStatusBarHeight - 27) * (static_cast(stats[player]->HP) / stats[player]->MAXHP); - pos.y = y2 - 15 - pos.h - hpmpbarOffsets; - - if ( stats[player]->EFFECTS[EFF_POISONED] ) - { - if ( !colorblind ) - { - color = makeColorRGB(0, 128, 0); - } - else - { - color = makeColorRGB(0, 0, 128); - } - } - else - { - color = makeColorRGB(128, 0, 0); - } - - // Only draw the actual Health bar if the Player is alive - drawRect(&pos, color, 255); - } - - // Print out the amount of HP the Player currently has - if ( stats[player] ) - { - snprintf(tempstr, 4, "%d", stats[player]->HP); - } - else - { - snprintf(tempstr, 4, "%d", 0); - } - if ( uiscale_playerbars >= 1.5 ) - { - pos.x += uiscale_playerbars * 2; - } - printTextFormatted(font12x12_bmp, pos.x + 16 * uiscale_playerbars - strlen(tempstr) * 6, y2 - (playerStatusBarHeight / 2 + 8) - hpmpbarOffsets, tempstr); - int xoffset = pos.x; - - // PLAYER MAGIC BAR - // Display the Magic bar border - pos.x = x1 + 12 * uiscale_playerbars; - pos.w = playerStatusBarWidth; - pos.h = playerStatusBarHeight; - pos.y = y2 - (playerStatusBarHeight + 12) - hpmpbarOffsets; - drawTooltip(&pos); - if ( stats[player] && stats[player]->HP > 0 - && stats[player]->EFFECTS[EFF_MP_REGEN] ) - { - bool lowDurationFlash = !((ticks % 50) - (ticks % 25)); - bool lowDuration = stats[player]->EFFECTS_TIMERS[EFF_MP_REGEN] > 0 && - (stats[player]->EFFECTS_TIMERS[EFF_MP_REGEN] < TICKS_PER_SECOND * 5); - if ( (lowDuration && !lowDurationFlash) || !lowDuration ) - { - if ( colorblind ) - { - drawTooltip(&pos, makeColorRGB(0, 255, 255)); // blue - } - else - { - drawTooltip(&pos, makeColorRGB(0, 255, 0)); // green - } - } - } - Uint32 mpColorBG = makeColorRGB(0, 0, 48); - Uint32 mpColorFG = makeColorRGB(0, 24, 128); - if ( stats[player] && stats[player]->playerRace == RACE_INSECTOID && stats[player]->stat_appearance == 0 ) - { - ttfPrintText(ttf12, pos.x + (playerStatusBarWidth / 2 - 10), pos.y + 6, Language::get(3768)); - mpColorBG = makeColorRGB(32, 48, 0); - mpColorFG = makeColorRGB(92, 192, 0); - } - else if ( stats[player] && stats[player]->type == AUTOMATON ) - { - ttfPrintText(ttf12, pos.x + (playerStatusBarWidth / 2 - 10), pos.y + 6, Language::get(3474)); - mpColorBG = makeColorRGB(64, 32, 0); - mpColorFG = makeColorRGB(192, 92, 0); - } - else - { - // Display "MP" at the top of Magic bar - ttfPrintText(ttf12, pos.x + (playerStatusBarWidth / 2 - 10), pos.y + 6, Language::get(307)); - } - - // Display border between actual Magic bar and "MP" - //pos.x = 12; - pos.w = playerStatusBarWidth; - pos.h = 0; - pos.y = y2 - (playerStatusBarHeight - 9) - hpmpbarOffsets; - drawTooltip(&pos); - if ( stats[player] && stats[player]->HP > 0 - && stats[player]->EFFECTS[EFF_MP_REGEN] ) - { - bool lowDurationFlash = !((ticks % 50) - (ticks % 25)); - bool lowDuration = stats[player]->EFFECTS_TIMERS[EFF_MP_REGEN] > 0 && - (stats[player]->EFFECTS_TIMERS[EFF_MP_REGEN] < TICKS_PER_SECOND * 5); - if ( (lowDuration && !lowDurationFlash) || !lowDuration ) - { - if ( colorblind ) - { - drawTooltip(&pos, makeColorRGB(0, 255, 255)); // blue - } - else - { - drawTooltip(&pos, makeColorRGB(0, 255, 0)); // green - } - } - } - - // Display the actual Magic bar's faint background - pos.x = x1 + 4 + 12 * uiscale_playerbars; - pos.w = playerStatusBarWidth - 5; - pos.h = playerStatusBarHeight - 27; - pos.y = y2 - 15 - pos.h - hpmpbarOffsets; - - // Draw the actual Magic bar's faint background - drawRect(&pos, mpColorBG, 255); // Display blue - - // If the Player has MP, base the size of the actual Magic bar off remaining MP - if ( stats[player] && stats[player]->MP > 0 ) - { - //pos.x = 16; - pos.w = playerStatusBarWidth - 5; - pos.h = (playerStatusBarHeight - 27) * (static_cast(stats[player]->MP) / stats[player]->MAXMP); - pos.y = y2 - 15 - pos.h - hpmpbarOffsets; - - // Only draw the actual Magic bar if the Player has MP - drawRect(&pos, mpColorFG, 255); // Display blue - } - - // Print out the amount of MP the Player currently has - if ( stats[player] ) - { - snprintf(tempstr, 4, "%d", stats[player]->MP); - } - else - { - snprintf(tempstr, 4, "%d", 0); - } - printTextFormatted(font12x12_bmp, x1 + 32 * uiscale_playerbars - strlen(tempstr) * 6, y2 - (playerStatusBarHeight / 2 + 8) - hpmpbarOffsets, tempstr); - -} - -void drawStatus(int player) -{ - SDL_Rect pos, initial_position; - Sint32 x, y, z, c, i; - node_t* node; - string_t* string; - - const int x1 = players[player]->camera_x1(); - const int x2 = players[player]->camera_x2(); - const int y1 = players[player]->camera_y1(); - const int y2 = players[player]->camera_y2(); - - const Sint32 mousex = inputs.getMouse(player, Inputs::X); - const Sint32 mousey = inputs.getMouse(player, Inputs::Y); - const Sint32 omousex = inputs.getMouse(player, Inputs::OX); - const Sint32 omousey = inputs.getMouse(player, Inputs::OY); - const Sint32 mousexrel = inputs.getMouse(player, Inputs::XREL); - const Sint32 mouseyrel = inputs.getMouse(player, Inputs::YREL); - - //pos.x = players[player]->statusBarUI.getStartX(); - auto& hotbar_t = players[player]->hotbar; - auto& hotbar = hotbar_t.slots(); - - int gui_mode = players[player]->gui_mode; - bool shootmode = players[player]->shootmode; - - if ( !hide_statusbar ) - { - //pos.y = players[player]->statusBarUI.getStartY(); - } - else - { - pos.y = y2 - 16; - } - //To garner the position of the hotbar. - initial_position.x = 0; /*hotbar_t.getStartX();*/ - initial_position.y = pos.y; - initial_position.w = 0; - initial_position.h = 0; - //pos.w = status_bmp->w * uiscale_chatlog; - //pos.h = status_bmp->h * uiscale_chatlog; - //if ( !hide_statusbar ) - //{ - // drawImageScaled(status_bmp, NULL, &pos); - //} - - ////players[player]->statusBarUI.messageStatusBarBox.x = pos.x; - ////players[player]->statusBarUI.messageStatusBarBox.y = pos.y; - ////players[player]->statusBarUI.messageStatusBarBox.w = pos.w; - ////players[player]->statusBarUI.messageStatusBarBox.h = pos.h; - - //// enemy health - //enemyHPDamageBarHandler[player].displayCurrentHPBar(player); - - //// messages - //if ( !hide_statusbar ) - //{ - // x = players[player]->statusBarUI.getStartX() + 24 * uiscale_chatlog; - // y = players[player]->camera_y2(); - // textscroll = std::min(list_Size(&messages) - 3, textscroll); - // c = 0; - // for ( node = messages.last; node != NULL; node = node->prev ) - // { - // c++; - // if ( c <= textscroll ) - // { - // continue; - // } - // string = (string_t*)node->element; - // if ( uiscale_chatlog >= 1.5 ) - // { - // y -= TTF16_HEIGHT * string->lines; - // if ( y < y2 - (status_bmp->h * uiscale_chatlog) + 8 * uiscale_chatlog ) - // { - // break; - // } - // } - // else if ( uiscale_chatlog != 1.f ) - // { - // y -= TTF12_HEIGHT * string->lines; - // if ( y < y2 - status_bmp->h * 1.1 + 4 ) - // { - // break; - // } - // } - // else - // { - // y -= TTF12_HEIGHT * string->lines; - // if ( y < y2 - status_bmp->h + 4 ) - // { - // break; - // } - // } - // z = 0; - // for ( i = 0; i < strlen(string->data); i++ ) - // { - // if ( string->data[i] != 10 ) // newline - // { - // z++; - // } - // else - // { - // z = 0; - // } - // if ( z == 65 ) - // { - // if ( string->data[i] != 10 ) - // { - // char* tempString = (char*)malloc(sizeof(char) * (strlen(string->data) + 2)); - // strcpy(tempString, string->data); - // strcpy((char*)(tempString + i + 1), (char*)(string->data + i)); - // tempString[i] = 10; - // free(string->data); - // string->data = tempString; - // string->lines++; - // } - // z = 0; - // } - // } - // Uint32 color = makeColor( 0, 0, 0, 255); // black color - // if ( uiscale_chatlog >= 1.5 ) - // { - // ttfPrintTextColor(ttf16, x, y, color, false, string->data); - // } - // else - // { - // ttfPrintTextColor(ttf12, x, y, color, false, string->data); - // } - // } - // if ( inputs.bMouseLeft(player) ) - // { - // if ( omousey >= y2 - status_bmp->h * uiscale_chatlog + 7 && omousey < y2 - status_bmp->h * uiscale_chatlog + (7 + 27) * uiscale_chatlog ) - // { - // if ( omousex >= players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog - // && omousex < players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog + 11 * uiscale_chatlog ) - // { - // // text scroll up - // buttonclick = 3; - // textscroll++; - // inputs.mouseClearLeft(player); - // } - // } - // else if ( omousey >= y2 - status_bmp->h * uiscale_chatlog + 34 && omousey < y2 - status_bmp->h * uiscale_chatlog + (34 + 28) * uiscale_chatlog ) - // { - // if ( omousex >= players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog - // && omousex < players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog + 11 * uiscale_chatlog ) - // { - // // text scroll down - // buttonclick = 12; - // textscroll--; - // if ( textscroll < 0 ) - // { - // textscroll = 0; - // } - // inputs.mouseClearLeft(player); - // } - // } - // else if ( omousey >= y2 - status_bmp->h * uiscale_chatlog + 62 && omousey < y2 - status_bmp->h * uiscale_chatlog + (62 + 31) * uiscale_chatlog ) - // { - // if ( omousex >= players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog - // && omousex < players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog + 11 * uiscale_chatlog ) - // { - // // text scroll down all the way - // buttonclick = 4; - // textscroll = 0; - // inputs.mouseClearLeft(player); - // } - // } - // /*else if( omousey>=y2-status_bmp->h+8 && omouseyh+8+30 ) { - // if( omousex>=players[player]->statusBarUI.getStartX()+618 && omousexstatusBarUI.getStartX()+618+11 ) { - // // text scroll up all the way - // buttonclick=13; - // textscroll=list_Size(&messages)-4; - // mousestatus[SDL_BUTTON_LEFT]=0; - // } - // }*/ - // } - - // // mouse wheel - // if ( !shootmode ) - // { - // if ( mousex >= players[player]->statusBarUI.getStartX() && mousex < players[player]->statusBarUI.getStartX() + status_bmp->w * uiscale_chatlog ) - // { - // if ( mousey >= initial_position.y && mousey < initial_position.y + status_bmp->h * uiscale_chatlog ) - // { - // if ( mousestatus[SDL_BUTTON_WHEELDOWN] ) - // { - // mousestatus[SDL_BUTTON_WHEELDOWN] = 0; - // textscroll--; - // if ( textscroll < 0 ) - // { - // textscroll = 0; - // } - // } - // else if ( mousestatus[SDL_BUTTON_WHEELUP] ) - // { - // mousestatus[SDL_BUTTON_WHEELUP] = 0; - // textscroll++; - // } - // } - // } - // } - // if (showfirst) - // { - // textscroll = list_Size(&messages) - 3; - // } - - - // //Text scroll up button. - // if ( buttonclick == 3 ) - // { - // pos.x = players[player]->statusBarUI.getStartX() + 617 * uiscale_chatlog; - // pos.y = y2 - status_bmp->h * uiscale_chatlog + 7 * uiscale_chatlog; - // pos.w = 11 * uiscale_chatlog; - // pos.h = 27 * uiscale_chatlog; - // drawRect(&pos, makeColorRGB(255, 255, 255), 80); - // //drawImage(textup_bmp, NULL, &pos); - // } - // //Text scroll down all the way button. - // if ( buttonclick == 4 ) - // { - // pos.x = players[player]->statusBarUI.getStartX() + 617 * uiscale_chatlog; - // pos.y = y2 - status_bmp->h * uiscale_chatlog + 62 * uiscale_chatlog; - // pos.w = 11 * uiscale_chatlog; - // pos.h = 31 * uiscale_chatlog; - // drawRect(&pos, makeColorRGB(255, 255, 255), 80); - // //drawImage(textdown_bmp, NULL, &pos); - // } - // //Text scroll down button. - // if ( buttonclick == 12 ) - // { - // pos.x = players[player]->statusBarUI.getStartX() + 617 * uiscale_chatlog; - // pos.y = y2 - status_bmp->h * uiscale_chatlog + 34 * uiscale_chatlog; - // pos.w = 11 * uiscale_chatlog; - // pos.h = 28 * uiscale_chatlog; - // drawRect(&pos, makeColorRGB(255, 255, 255), 80); - // //drawImage(textup_bmp, NULL, &pos); - // } - // //Text scroll up all the way button. - // /*if( buttonclick==13 ) { - // pos.x=players[player]->statusBarUI.getStartX()+617; pos.y=y2-status_bmp->h+8; - // pos.w=11; pos.h=30; - // drawRect(&pos,0xffffffff,80); - // //drawImage(textdown_bmp, NULL, &pos); - // }*/ - //} - - //int playerStatusBarWidth = 38 * uiscale_playerbars; - //int playerStatusBarHeight = 156 * uiscale_playerbars; - - //if ( !players[player]->hud.hpFrame ) - //{ - // drawHPMPBars(player); - //} - // - //// hunger icon - //if ( stats[player] && stats[player]->type != AUTOMATON - // && (svFlags & SV_FLAG_HUNGER) && stats[player]->HUNGER <= 250 && (ticks % 50) - (ticks % 25) ) - //{ - // pos.x = /*xoffset*/ + playerStatusBarWidth + 10; // was pos.x = 128; - // pos.y = y2 - 160; - // pos.w = 64; - // pos.h = 64; - // if ( playerRequiresBloodToSustain(player) ) - // { - // drawImageScaled(hunger_blood_bmp, NULL, &pos); - // } - // else - // { - // drawImageScaled(hunger_bmp, NULL, &pos); - // } - //} - - //if ( stats[player] && stats[player]->type == AUTOMATON ) - //{ - // if ( stats[player]->HUNGER > 300 || (ticks % 50) - (ticks % 25) ) - // { - // pos.x = /*xoffset*/ + playerStatusBarWidth + 10; // was pos.x = 128; - // pos.y = y2 - 160; - // pos.w = 64; - // pos.h = 64; - // if ( stats[player]->HUNGER > 1200 ) - // { - // drawImageScaled(hunger_boiler_hotflame_bmp, nullptr, &pos); - // } - // else - // { - // if ( stats[player]->HUNGER > 600 ) - // { - // drawImageScaledPartial(hunger_boiler_flame_bmp, nullptr, &pos, 1.f); - // } - // else - // { - // float percent = (stats[player]->HUNGER - 300) / 300.f; // always show a little bit more at the bottom (10-20%) - // drawImageScaledPartial(hunger_boiler_flame_bmp, nullptr, &pos, percent); - // } - // } - // drawImageScaled(hunger_boiler_bmp, nullptr, &pos); - // } - //} - - //// minotaur icon - //if ( minotaurlevel && (ticks % 50) - (ticks % 25) ) - //{ - // pos.x = /*xoffset*/ + playerStatusBarWidth + 10; // was pos.x = 128; - // pos.y = y2 - 160 + 64 + 2; - // pos.w = 64; - // pos.h = 64; - // drawImageScaled(minotaur_bmp, nullptr, &pos); - //} - - // draw action prompts. - /*if ( players[player]->hud.bShowActionPrompts ) - { - int skill = (ticks / 100) % 16; - int iconSize = 48; - SDL_Rect skillPos{0, 0, iconSize, iconSize }; - skillPos.x = hotbar_t.hotbarBox.x - 3.5 * iconSize; - skillPos.y = players[player]->camera_y2() - 106 - iconSize - 16; - std::string promptString; - players[player]->hud.drawActionIcon(skillPos, players[player]->hud.getActionIconForPlayer(Player::HUD_t::ACTION_PROMPT_OFFHAND, promptString)); - players[player]->hud.drawActionGlyph(skillPos, Player::HUD_t::ACTION_PROMPT_OFFHAND); - - skillPos.x = hotbar_t.hotbarBox.x - 2.5 * iconSize; - players[player]->hud.drawActionIcon(skillPos, players[player]->hud.getActionIconForPlayer(Player::HUD_t::ACTION_PROMPT_MAINHAND, promptString)); - players[player]->hud.drawActionGlyph(skillPos, Player::HUD_t::ACTION_PROMPT_MAINHAND); - - skillPos.x = hotbar_t.hotbarBox.x + hotbar_t.hotbarBox.w + 0.5 * iconSize; - players[player]->hud.drawActionIcon(skillPos, players[player]->hud.getActionIconForPlayer(Player::HUD_t::ACTION_PROMPT_MAGIC, promptString)); - players[player]->hud.drawActionGlyph(skillPos, Player::HUD_t::ACTION_PROMPT_MAGIC); - }*/ - - Item* item = nullptr; - //Now the hotbar. - int num = 0; - //Reset the position to the top left corner of the status bar to draw the hotbar slots.. - //pos.x = initial_position.x; - pos.x = 0; /*hotbar_t.getStartX();*/ - pos.y = initial_position.y - hotbar_t.getSlotSize(); - - hotbar_t.hotbarBox.x = pos.x; - hotbar_t.hotbarBox.y = pos.y; - hotbar_t.hotbarBox.w = NUM_HOTBAR_SLOTS * hotbar_t.getSlotSize(); - hotbar_t.hotbarBox.h = hotbar_t.getSlotSize(); - - if ( players[player]->hotbar.useHotbarFaceMenu ) - { - players[player]->hotbar.initFaceButtonHotbar(); - } - - for ( num = 0; num < NUM_HOTBAR_SLOTS; ++num ) - { - Uint32 color; - if ( players[player]->hotbar.current_hotbar == num && !openedChest[player] ) - { - color = makeColor( 255, 255, 0, 255); //Draw gold border around currently selected hotbar. - } - else - { - color = makeColor( 255, 255, 255, 60); //Draw normal grey border. - } - pos.w = hotbar_t.getSlotSize(); - pos.h = hotbar_t.getSlotSize(); - - if ( players[player]->hotbar.useHotbarFaceMenu ) - { - pos.x = players[player]->hotbar.faceButtonPositions[num].x; - pos.y = players[player]->hotbar.faceButtonPositions[num].y; - } - - //drawImageScaledColor(hotbar_img, NULL, &pos, color); - item = uidToItem(hotbar[num].item); - if ( item ) - { - if ( item->type == BOOMERANG ) - { - hotbar_t.magicBoomerangHotbarSlot = num; - } - bool used = false; - pos.w = hotbar_t.getSlotSize(); - pos.h = hotbar_t.getSlotSize(); - - SDL_Rect highlightBox; - highlightBox.x = pos.x + 2; - highlightBox.y = pos.y + 2; - highlightBox.w = 60 * uiscale_hotbar; - highlightBox.h = 60 * uiscale_hotbar; - - if ( !item->identified ) - { - // give it a yellow background if it is unidentified - drawRect(&highlightBox, makeColorRGB(128, 128, 0), 64); //31875 - } - else if ( item->beatitude < 0 ) - { - // give it a red background if cursed - drawRect(&highlightBox, makeColorRGB(128, 0, 0), 64); - } - else if ( item->beatitude > 0 ) - { - // give it a green background if blessed (light blue if colorblind mode) - if ( colorblind ) - { - drawRect(&highlightBox, makeColorRGB(50, 128, 128), 64); - } - else - { - drawRect(&highlightBox, makeColorRGB(0, 128, 0), 64); - } - } - if ( item->status == BROKEN ) - { - drawRect(&highlightBox, makeColorRGB(64, 64, 64), 125); - } - - Uint32 itemColor = makeColor( 255, 255, 255, 255); - if ( hotbar_t.useHotbarFaceMenu && hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_NONE ) - { - bool dimColor = false; - if ( hotbar_t.faceMenuButtonHeld != hotbar_t.getFaceMenuGroupForSlot(num) ) - { - dimColor = true; - } - if ( dimColor ) - { - itemColor = makeColor( 255, 255, 255, 128); - } - } - drawImageScaledColor(itemSprite(item), NULL, &pos, itemColor); - - bool disableItemUsage = false; - - if ( players[player] && players[player]->entity && players[player]->entity->effectShapeshift != NOTHING ) - { - // shape shifted, disable some items - if ( !item->usableWhileShapeshifted(stats[player]) ) - { - disableItemUsage = true; - drawRect(&highlightBox, makeColorRGB(64, 64, 64), 144); - } - } - if ( client_classes[player] == CLASS_SHAMAN ) - { - if ( item->type == SPELL_ITEM && !(playerUnlockedShamanSpell(player, item)) ) - { - disableItemUsage = true; - drawRect(&highlightBox, makeColorRGB(64, 64, 64), 144); - } - } - - if ( stats[player] && stats[player]->HP > 0 ) - { - Item*& selectedItem = inputs.getUIInteraction(player)->selectedItem; - - if ( !shootmode && mouseInBounds(player, pos.x, pos.x + hotbar_t.getSlotSize(), pos.y, pos.y + hotbar_t.getSlotSize()) ) - { - if ( (inputs.bMouseLeft(player) - || (inputs.bControllerInputPressed(player, INJOY_MENU_LEFT_CLICK) - && !openedChest[player] - && gui_mode != (GUI_MODE_SHOP) - && !GenericGUI[player].isGUIOpen())) - && !selectedItem ) - { - inputs.getUIInteraction(player)->toggleclick = false; - if ( keystatus[SDLK_LSHIFT] || keystatus[SDLK_RSHIFT] ) - { - hotbar[num].item = 0; - hotbar[num].resetLastItem(); - } - else - { - //Remove the item if left clicked. - selectedItem = item; - if ( selectedItem ) - { - playSound(139, 64); // click sound - } - hotbar[num].item = 0; - hotbar[num].resetLastItem(); - - if ( inputs.bControllerInputPressed(player, INJOY_MENU_LEFT_CLICK) && !openedChest[player] && gui_mode != (GUI_MODE_SHOP) ) - { - inputs.controllerClearInput(player, INJOY_MENU_LEFT_CLICK); - inputs.getUIInteraction(player)->toggleclick = true; - inputs.getUIInteraction(player)->selectedItemFromHotbar = num; - //TODO: Change the mouse cursor to THE HAND. - } - } - } - if ( inputs.bMouseRight(player) - || (inputs.bControllerInputPressed(player, INJOY_MENU_USE) - && !openedChest[player] - && gui_mode != (GUI_MODE_SHOP) - && !GenericGUI[player].isGUIOpen()) ) - { - //Use the item if right clicked. - inputs.mouseClearRight(player); - inputs.controllerClearInput(player, INJOY_MENU_USE); - bool badpotion = false; - bool learnedSpell = false; - - if ( itemCategory(item) == POTION && item->identified ) - { - badpotion = isPotionBad(*item); //So that you wield empty potions be default. - } - if ( item->type == POTION_EMPTY ) - { - badpotion = true; - } - if ( itemCategory(item) == SPELLBOOK && (item->identified || itemIsEquipped(item, player)) ) - { - // equipped spellbook will unequip on use. - learnedSpell = (playerLearnedSpellbook(player, item) || itemIsEquipped(item, player)); - } - - if ( keystatus[SDLK_LSHIFT] || keystatus[SDLK_RSHIFT] ) - { - players[player]->inventoryUI.appraisal.appraiseItem(item); - } - else - { - if ( (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK || item->type == FOOD_CREAMPIE ) - && (keystatus[SDLK_LALT] || keystatus[SDLK_RALT]) ) - { - badpotion = true; - learnedSpell = true; - } - - if ( !learnedSpell && item->identified - && itemCategory(item) == SPELLBOOK && players[player] && players[player]->entity ) - { - learnedSpell = true; // let's always equip/unequip spellbooks from the hotbar? - spell_t* currentSpell = getSpellFromID(getSpellIDFromSpellbook(item->type)); - if ( currentSpell ) - { - int skillLVL = stats[player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[player], players[player]->entity); - if ( stats[player]->getModifiedProficiency(PRO_MAGIC) >= 100 ) - { - skillLVL = 100; - } - if ( skillLVL >= currentSpell->difficulty ) - { - // can learn spell, try that instead. - learnedSpell = false; - } - } - } - - if ( itemCategory(item) == SPELLBOOK && stats[player] - && (stats[player]->type == GOBLIN - || (stats[player]->playerRace == RACE_GOBLIN && stats[player]->stat_appearance == 0)) ) - { - learnedSpell = true; // goblinos can't learn spells but always equip books. - } - - if ( !badpotion && !learnedSpell ) - { - if ( !(isItemEquippableInShieldSlot(item) && cast_animation[player].active_spellbook) ) - { - if ( !disableItemUsage ) - { - if ( stats[player] && stats[player]->type == AUTOMATON - && (item->type == TOOL_METAL_SCRAP || item->type == TOOL_MAGIC_SCRAP) ) - { - // consume item - if ( multiplayer == CLIENT ) - { - strcpy((char*)net_packet->data, "FODA"); - SDLNet_Write32((Uint32)item->type, &net_packet->data[4]); - SDLNet_Write32((Uint32)item->status, &net_packet->data[8]); - SDLNet_Write32((Uint32)item->beatitude, &net_packet->data[12]); - SDLNet_Write32((Uint32)item->count, &net_packet->data[16]); - SDLNet_Write32((Uint32)item->appearance, &net_packet->data[20]); - net_packet->data[24] = item->identified; - net_packet->data[25] = player; - net_packet->address.host = net_server.host; - net_packet->address.port = net_server.port; - net_packet->len = 26; - sendPacketSafe(net_sock, -1, net_packet, 0); - } - item_FoodAutomaton(item, player); - } - else - { - useItem(item, player); - } - } - else - { - if ( client_classes[player] == CLASS_SHAMAN && item->type == SPELL_ITEM ) - { - messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3488)); // unable to use with current level. - } - else - { - messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3432)); // unable to use in current form message. - } - playSoundPlayer(player, 90, 64); - } - } - } - else - { - if ( !disableItemUsage ) - { - playerTryEquipItemAndUpdateServer(player, item, false); - } - else - { - if ( client_classes[player] == CLASS_SHAMAN && item->type == SPELL_ITEM ) - { - messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3488)); // unable to use with current level. - } - else - { - messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3432)); // unable to use in current form message. - } - playSoundPlayer(player, 90, 64); - } - } - used = true; - if ( disableItemUsage ) - { - used = false; - } - } - } - } - } - - // item count - if ( !used ) - { - if ( item->count > 1 ) - { - int digits = numdigits_sint16(item->count); - SDL_Surface* digitFont = font12x12_bmp; - if ( uiscale_hotbar >= 1.5 ) - { - digitFont = font16x16_bmp; - printTextFormatted(digitFont, pos.x + hotbar_t.getSlotSize() - (24 * digits), pos.y + hotbar_t.getSlotSize() - 24, "%d", item->count); - } - else - { - printTextFormatted(digitFont, pos.x + hotbar_t.getSlotSize() - (14 * digits), pos.y + hotbar_t.getSlotSize() - 14, "%d", item->count); - } - } - - SDL_Rect src; - src.x = pos.x + 2; - src.h = 16 * uiscale_hotbar; - src.y = pos.y + hotbar_t.getSlotSize() - src.h - 2; - src.w = 16 * uiscale_hotbar; - - // item equipped - if ( itemCategory(item) != SPELL_CAT ) - { - if ( itemIsEquipped(item, player) ) - { - //drawImageScaled(equipped_bmp, NULL, &src); - } - else if ( item->status == BROKEN ) - { - //drawImageScaled(itembroken_bmp, NULL, &src); - } - } - else - { - spell_t* spell = getSpellFromItem(player, item, false); - if ( players[player]->magic.selectedSpell() == spell - && (players[player]->magic.selected_spell_last_appearance == item->appearance || players[player]->magic.selected_spell_last_appearance == -1 ) ) - { - //drawImageScaled(equipped_bmp, NULL, &src); - } - } - } - } - - // draw hotbar slot 'numbers' or glyphs - if ( players[player]->hotbar.useHotbarFaceMenu ) - { - if ( players[player]->hotbar.faceMenuAlternateLayout ) - { - SDL_Rect tmp{ pos.x, pos.y, pos.w, pos.h }; - if ( num == 1 ) - { - tmp.y = pos.y + .75 * pos.h - 4; - tmp.x = pos.x + pos.w / 2 + 16; - tmp.x += pos.w; - if ( !(hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_NONE && hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_LEFT) ) - { - //players[player]->hotbar.drawFaceButtonGlyph(num, tmp); - } - } - else if ( num == 4 ) - { - tmp.y = pos.y + pos.h + 24; - if ( !(hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_NONE && hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_MIDDLE) ) - { - //players[player]->hotbar.drawFaceButtonGlyph(num, tmp); - } - } - else if ( num == 7 ) - { - tmp.y = pos.y + .75 * pos.h - 4; - tmp.x = pos.x - (pos.w / 2 + 16); - tmp.x -= pos.w; - - if ( !(hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_NONE && hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_RIGHT) ) - { - //players[player]->hotbar.drawFaceButtonGlyph(num, tmp); - } - } - else - { - //players[player]->hotbar.drawFaceButtonGlyph(num, tmp); - } - } - else - { - //players[player]->hotbar.drawFaceButtonGlyph(num, pos); - } - } - else if ( uiscale_hotbar >= 1.5 ) - { - printTextFormatted(font16x16_bmp, pos.x + 2, pos.y + 2, "%d", (num + 1) % 10); // slot number - } - else - { - printTextFormatted(font12x12_bmp, pos.x + 2, pos.y + 2, "%d", (num + 1) % 10); // slot number - } - pos.x += hotbar_t.getSlotSize(); - } - - bool drawHotBarTooltipOnCycle = false; - if ( !intro ) - { - if ( hotbar_t.hotbarTooltipLastGameTick != 0 && (ticks - hotbar_t.hotbarTooltipLastGameTick) < TICKS_PER_SECOND * 2 ) - { - drawHotBarTooltipOnCycle = true; - } - else if ( players[player]->hotbar.useHotbarFaceMenu && players[player]->hotbar.faceMenuButtonHeld != Player::Hotbar_t::GROUP_NONE ) - { - drawHotBarTooltipOnCycle = true; - } - } - - if ( !shootmode || drawHotBarTooltipOnCycle ) - { - pos.x = initial_position.x; - - //Go back through all of the hotbar slots and draw the tooltips. - for ( num = 0; num < NUM_HOTBAR_SLOTS; ++num ) - { - if ( players[player]->hotbar.useHotbarFaceMenu ) - { - pos.x = players[player]->hotbar.faceButtonPositions[num].x; - pos.y = players[player]->hotbar.faceButtonPositions[num].y; - } - item = uidToItem(hotbar[num].item); - if ( item ) - { - bool drawTooltipOnSlot = !shootmode && mouseInBounds(player, pos.x, pos.x + hotbar_t.getSlotSize(), pos.y, pos.y + hotbar_t.getSlotSize()); - if ( !drawTooltipOnSlot ) - { - if ( drawHotBarTooltipOnCycle && players[player]->hotbar.current_hotbar == num ) - { - drawTooltipOnSlot = true; - } - } - else - { - if ( !shootmode ) - { - // reset timer. - hotbar_t.hotbarTooltipLastGameTick = 0; - drawHotBarTooltipOnCycle = false; - } - else - { - if ( drawHotBarTooltipOnCycle ) - { - drawTooltipOnSlot = false; - } - } - } - - if ( drawTooltipOnSlot ) - { - //Tooltip - SDL_Rect src; - src.x = mousex + 16; - src.y = mousey + 8; - - if ( drawHotBarTooltipOnCycle ) - { - if ( players[player]->hotbar.useHotbarFaceMenu ) - { - src.x = pos.x + hotbar_t.getSlotSize() / 2; - src.y = pos.y - 32; - } - else - { - src.x = pos.x + hotbar_t.getSlotSize(); - src.y = pos.y + hotbar_t.getSlotSize(); - src.y -= 16; - } - } - - if ( itemCategory(item) == SPELL_CAT ) - { - spell_t* spell = getSpellFromItem(player, item, false); - if ( drawHotBarTooltipOnCycle ) - { - //drawSpellTooltip(player, spell, item, &src); - } - else - { - //drawSpellTooltip(player, spell, item, nullptr); - } - } - else - { - src.w = std::max(13, longestline(item->description())) * TTF12_WIDTH + 8; - src.h = TTF12_HEIGHT * 4 + 8; - char spellEffectText[256] = ""; - if ( item->identified ) - { - bool learnedSpellbook = false; - if ( itemCategory(item) == SPELLBOOK ) - { - learnedSpellbook = playerLearnedSpellbook(player, item); - if ( !learnedSpellbook && stats[player] && players[player] && players[player]->entity ) - { - // spellbook tooltip shows if you have the magic requirement as well (for goblins) - int skillLVL = stats[player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[player], players[player]->entity); - spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item->type)); - if ( spell && skillLVL >= spell->difficulty ) - { - learnedSpellbook = true; - } - } - } - - if ( itemCategory(item) == WEAPON || itemCategory(item) == ARMOR || itemCategory(item) == THROWN - || itemTypeIsQuiver(item->type) ) - { - src.h += TTF12_HEIGHT; - } - else if ( itemCategory(item) == SCROLL && item->identified ) - { - src.h += TTF12_HEIGHT; - src.w = std::max((2 + longestline(Language::get(3862)) + longestline(item->getScrollLabel())) * TTF12_WIDTH + 8, src.w); - } - else if ( itemCategory(item) == SPELLBOOK && learnedSpellbook ) - { - int height = 1; - char effectType[32] = ""; - int spellID = getSpellIDFromSpellbook(item->type); - int damage = 0;;// drawSpellTooltip(player, getSpellFromID(spellID), item, nullptr); - real_t dummy = 0.f; - //getSpellEffectString(spellID, spellEffectText, effectType, damage, &height, &dummy); - int width = longestline(spellEffectText) * TTF12_WIDTH + 8; - if ( width > src.w ) - { - src.w = width; - } - src.h += height * TTF12_HEIGHT; - } - else if ( item->type == TOOL_GYROBOT || item->type == TOOL_DUMMYBOT - || item->type == TOOL_SENTRYBOT || item->type == TOOL_SPELLBOT - || (item->type == ENCHANTED_FEATHER && item->identified) ) - { - src.w += 7 * TTF12_WIDTH; - } - } - - int furthestX = players[player]->camera_x2(); - /*if ( players[player]->characterSheet.proficienciesPage == 0 ) - { - if ( src.y < players[player]->characterSheet.skillsSheetBox.y + players[player]->characterSheet.skillsSheetBox.h ) - { - furthestX = players[player]->camera_x2() - players[player]->characterSheet.skillsSheetBox.w; - } - } - else - { - if ( src.y < players[player]->characterSheet.partySheetBox.y + players[player]->characterSheet.partySheetBox.h ) - { - furthestX = players[player]->camera_x2() - players[player]->characterSheet.partySheetBox.w; - } - }*/ - - if ( drawHotBarTooltipOnCycle && players[player]->hotbar.useHotbarFaceMenu ) - { - // draw centred. - src.x -= src.w / 2; - src.y -= src.h; - } - - if ( src.x + src.w + 16 > furthestX ) // overflow right side of screen - { - src.x -= (src.w + 32); - } - if ( src.y + src.h + 16 > y2 ) // overflow bottom of screen - { - src.y -= (src.y + src.h + 16 - y2); - } - - if ( drawHotBarTooltipOnCycle ) - { - drawTooltip(&src); - } - else - { - drawTooltip(&src); - } - - Uint32 color = 0xFFFFFFFF; - if ( !item->identified ) - { - color = makeColorRGB(255, 255, 0); - ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT, color, Language::get(309)); - } - else - { - if ( item->beatitude < 0 ) - { - color = makeColorRGB(255, 0, 0); - ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT, color, Language::get(310)); - } - else if ( item->beatitude == 0 ) - { - color = 0xFFFFFFFF; - ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT, color, Language::get(311)); - } - else - { - color = makeColorRGB(0, 255, 0); - ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT, color, Language::get(312)); - } - } - if ( item->beatitude == 0 || !item->identified ) - { - color = 0xFFFFFFFF; - } - - if ( item->type == TOOL_GYROBOT || item->type == TOOL_DUMMYBOT - || item->type == TOOL_SENTRYBOT || item->type == TOOL_SPELLBOT ) - { - int health = 100; - if ( !item->tinkeringBotIsMaxHealth() ) - { - health = 25 * (item->appearance % 10); - if ( health == 0 && item->status != BROKEN ) - { - health = 5; - } - } - ttfPrintTextFormattedColor(ttf12, src.x + 4, src.y + 4, color, "%s (%d%%)", item->description(), health); - } - else if ( item->type == ENCHANTED_FEATHER && item->identified ) - { - ttfPrintTextFormattedColor(ttf12, src.x + 4, src.y + 4, color, "%s (%d%%)", item->description(), item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY); - } - else - { - ttfPrintTextFormattedColor(ttf12, src.x + 4, src.y + 4, color, "%s", item->description()); - } - int itemWeight = item->getWeight(); - ttfPrintTextFormatted(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 2, Language::get(313), itemWeight); - ttfPrintTextFormatted(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 3, Language::get(314), item->sellValue(player)); - if ( strcmp(spellEffectText, "") ) - { - ttfPrintTextFormattedColor(ttf12, src.x + 4, src.y + 4 + TTF12_HEIGHT * 4, makeColorRGB(0, 255, 255), spellEffectText); - } - - if ( item->identified && stats[player] ) - { - if ( itemCategory(item) == WEAPON || itemCategory(item) == THROWN - || itemTypeIsQuiver(item->type) ) - { - Monster tmpRace = stats[player]->type; - if ( stats[player]->type == TROLL - || stats[player]->type == RAT - || stats[player]->type == SPIDER - || stats[player]->type == CREATURE_IMP ) - { - // these monsters have 0 bonus from weapons, but want the tooltip to say the normal amount. - stats[player]->type = HUMAN; - } - - if ( item->weaponGetAttack(stats[player]) >= 0 ) - { - color = makeColorRGB(0, 255, 255); - } - else - { - color = makeColorRGB(255, 0, 0); - } - if ( stats[player]->type != tmpRace ) - { - color = makeColorRGB(127, 127, 127); // grey out the text if monster doesn't benefit. - } - - ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 4, color, Language::get(315), item->weaponGetAttack(stats[player])); - stats[player]->type = tmpRace; - } - else if ( itemCategory(item) == ARMOR ) - { - Monster tmpRace = stats[player]->type; - if ( stats[player]->type == TROLL - || stats[player]->type == RAT - || stats[player]->type == SPIDER - || stats[player]->type == CREATURE_IMP ) - { - // these monsters have 0 bonus from armor, but want the tooltip to say the normal amount. - stats[player]->type = HUMAN; - } - - if ( item->armorGetAC(stats[player]) >= 0 ) - { - color = makeColorRGB(0, 255, 255); - } - else - { - color = makeColorRGB(255, 0, 0); - } - if ( stats[player]->type != tmpRace ) - { - color = makeColorRGB(127, 127, 127); // grey out the text if monster doesn't benefit. - } - - ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 4, color, Language::get(316), item->armorGetAC(stats[player])); - stats[player]->type = tmpRace; - } - else if ( itemCategory(item) == SCROLL ) - { - color = makeColorRGB(0, 255, 255); - ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 4, color, "%s%s", Language::get(3862), item->getScrollLabel()); - } - } - } - if ( !drawHotBarTooltipOnCycle && playerSettings[multiplayer ? 0 : player].hotbar_numkey_quick_add && inputs.bPlayerUsingKeyboardControl(player) ) - { - Uint32 swapItem = 0; - if ( keystatus[SDLK_1] ) - { - keystatus[SDLK_1] = 0; - swapItem = hotbar[0].item; - hotbar[0].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_2] ) - { - keystatus[SDLK_2] = 0; - swapItem = hotbar[1].item; - hotbar[1].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_3] ) - { - keystatus[SDLK_3] = 0; - swapItem = hotbar[2].item; - hotbar[2].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_4] ) - { - keystatus[SDLK_4] = 0; - swapItem = hotbar[3].item; - hotbar[3].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_5] ) - { - keystatus[SDLK_5] = 0; - swapItem = hotbar[4].item; - hotbar[4].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_6] ) - { - keystatus[SDLK_6] = 0; - swapItem = hotbar[5].item; - hotbar[5].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_7] ) - { - keystatus[SDLK_7] = 0; - swapItem = hotbar[6].item; - hotbar[6].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_8] ) - { - keystatus[SDLK_8] = 0; - swapItem = hotbar[7].item; - hotbar[7].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_9] ) - { - keystatus[SDLK_9] = 0; - swapItem = hotbar[8].item; - hotbar[8].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - if ( keystatus[SDLK_0] ) - { - keystatus[SDLK_0] = 0; - swapItem = hotbar[9].item; - hotbar[9].item = hotbar[num].item; - hotbar[num].item = swapItem; - } - } - } - } - pos.x += hotbar_t.getSlotSize(); - } - } - - //NOTE: If you change the number of hotbar slots, you *MUST* change this. - if ( !command && stats[player] && stats[player]->HP > 0 ) - { - Item* item = NULL; - const auto& inventoryUI = players[multiplayer ? 0 : player]->inventoryUI; - if ( !(!shootmode && playerSettings[multiplayer ? 0 : player].hotbar_numkey_quick_add - /*&& ( - (omousex >= inventoryUI.getStartX() - && omousex <= inventoryUI.getStartX() + inventoryUI.getSizeX() * inventoryUI.getSlotSize() - && omousey >= inventoryUI.getStartY() - && omousey <= inventoryUI.getStartY() + inventoryUI.getSizeY() * inventoryUI.getSlotSize() - ) - || - (omousex >= initial_position.x - && omousex <= initial_position.x + hotbar_t.getSlotSize() * NUM_HOTBAR_SLOTS - && omousey >= initial_position.y - hotbar_t.getSlotSize() - && omousey <= initial_position.y - ) - )*/ - ) ) - { - // if hotbar_numkey_quick_add is enabled, then the number keys won't do the default equip function - // skips equipping items if the mouse is in the hotbar or inventory area. otherwise the below code runs. - if ( inputs.bPlayerUsingKeyboardControl(player) && !StatueManager.activeEditing ) - { - if ( keystatus[SDLK_1] ) - { - keystatus[SDLK_1] = 0; - item = uidToItem(hotbar[0].item); - hotbar_t.current_hotbar = 0; - } - if ( keystatus[SDLK_2] ) - { - keystatus[SDLK_2] = 0; - item = uidToItem(hotbar[1].item); - hotbar_t.current_hotbar = 1; - } - if ( keystatus[SDLK_3] ) - { - keystatus[SDLK_3] = 0; - item = uidToItem(hotbar[2].item); - hotbar_t.current_hotbar = 2; - } - if ( keystatus[SDLK_4] ) - { - keystatus[SDLK_4] = 0; - item = uidToItem(hotbar[3].item); - hotbar_t.current_hotbar = 3; - } - if ( keystatus[SDLK_5] ) - { - keystatus[SDLK_5] = 0; - item = uidToItem(hotbar[4].item); - hotbar_t.current_hotbar = 4; - } - if ( keystatus[SDLK_6] ) - { - keystatus[SDLK_6] = 0; - item = uidToItem(hotbar[5].item); - hotbar_t.current_hotbar = 5; - } - if ( keystatus[SDLK_7] ) - { - keystatus[SDLK_7] = 0; - item = uidToItem(hotbar[6].item); - hotbar_t.current_hotbar = 6; - } - if ( keystatus[SDLK_8] ) - { - keystatus[SDLK_8] = 0; - item = uidToItem(hotbar[7].item); - hotbar_t.current_hotbar = 7; - } - if ( keystatus[SDLK_9] ) - { - keystatus[SDLK_9] = 0; - item = uidToItem(hotbar[8].item); - hotbar_t.current_hotbar = 8; - } - if ( keystatus[SDLK_0] ) - { - keystatus[SDLK_0] = 0; - item = uidToItem(hotbar[9].item); - hotbar_t.current_hotbar = 9; - } - } - if ( players[player]->hotbar.useHotbarFaceMenu - && !openedChest[player] - && gui_mode != (GUI_MODE_SHOP) - && !GenericGUI[player].isGUIOpen() - && !inputs.getUIInteraction(player)->selectedItem - && shootmode ) - { - Player::Hotbar_t::FaceMenuGroup pressed = Player::Hotbar_t::GROUP_NONE; - - for ( int i = 0; i < 3; ++i ) - { - int button = SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B + i; - if ( inputs.bControllerRawInputPressed(player, 301 + button) ) - { - if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN) ) - { - inputs.controllerClearRawInput(player, 301 + button); - inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN); - inputs.controllerClearRawInputRelease(player, 301 + button); - break; - } - - switch ( button ) - { - case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B: - pressed = Player::Hotbar_t::GROUP_RIGHT; - break; - case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_Y: - pressed = Player::Hotbar_t::GROUP_MIDDLE; - break; - case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X: - pressed = Player::Hotbar_t::GROUP_LEFT; - break; - default: - break; - } - - std::array slotOrder = { 0, 1, 2 }; - int centerSlot = 1; - if ( hotbar_t.faceMenuAlternateLayout ) - { - slotOrder = { 0, 2, 1 }; - } - if ( button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B ) - { - centerSlot = 7; - if ( hotbar_t.faceMenuAlternateLayout ) - { - slotOrder = { 7, 6, 8 }; - } - else - { - slotOrder = { 6, 7, 8 }; - } - } - else if ( button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_Y ) - { - centerSlot = 4; - slotOrder = { 3, 4, 5 }; - } - - if ( hotbar_t.faceMenuAlternateLayout ) - { - if ( true ) - { - // temp test - if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ) - { - hotbar_t.selectHotbarSlot(slotOrder[0]); - } - else if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ) - { - hotbar_t.selectHotbarSlot(slotOrder[2]); - } - else if ( players[player]->hotbar.faceMenuButtonHeld == Player::Hotbar_t::GROUP_NONE ) - { - hotbar_t.selectHotbarSlot(slotOrder[1]); - } - } - else - { - if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ) - { - hotbar_t.selectHotbarSlot(std::max(centerSlot - 1, hotbar_t.current_hotbar - 1)); - inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER); - } - else if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ) - { - hotbar_t.selectHotbarSlot(std::min(centerSlot + 1, hotbar_t.current_hotbar + 1)); - inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER); - } - else if ( players[player]->hotbar.faceMenuButtonHeld == Player::Hotbar_t::GROUP_NONE ) - { - hotbar_t.selectHotbarSlot(slotOrder[1]); - } - } - } - else - { - if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ) - { - hotbar_t.selectHotbarSlot(std::max(centerSlot - 1, hotbar_t.current_hotbar - 1)); - inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER); - } - else if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ) - { - hotbar_t.selectHotbarSlot(std::min(centerSlot + 1, hotbar_t.current_hotbar + 1)); - inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER); - } - else if ( players[player]->hotbar.faceMenuButtonHeld == Player::Hotbar_t::GROUP_NONE ) - { - hotbar_t.selectHotbarSlot(slotOrder[1]); - } - } - break; - } - else if ( inputs.bControllerRawInputReleased(player, 301 + button) ) - { - item = uidToItem(players[player]->hotbar.slots()[hotbar_t.current_hotbar].item); - inputs.controllerClearRawInputRelease(player, 301 + button); - break; - } - } - - players[player]->hotbar.faceMenuButtonHeld = pressed; - - if ( pressed != Player::Hotbar_t::GROUP_NONE - && players[player]->hotbar.faceMenuQuickCastEnabled && item && itemCategory(item) == SPELL_CAT ) - { - spell_t* spell = getSpellFromItem(player, item, false); - if ( spell && players[player]->magic.selectedSpell() == spell ) - { - players[player]->hotbar.faceMenuQuickCast = true; - } - } - } - } - - //Moving the cursor changes the currently selected hotbar slot. - if ( (mousexrel || mouseyrel) && !shootmode ) - { - pos.x = initial_position.x; - pos.y = initial_position.y - hotbar_t.getSlotSize(); - for ( c = 0; c < NUM_HOTBAR_SLOTS; ++c, pos.x += hotbar_t.getSlotSize() ) - { - if ( players[player]->hotbar.useHotbarFaceMenu ) - { - pos.x = players[player]->hotbar.faceButtonPositions[c].x; - pos.y = players[player]->hotbar.faceButtonPositions[c].y; - } - if ( mouseInBoundsRealtimeCoords(player, pos.x, pos.x + hotbar_t.getSlotSize(), pos.y, pos.y + hotbar_t.getSlotSize()) ) - { - players[player]->hotbar.selectHotbarSlot(c); - } - } - } - - bool bumper_moved = false; - - Input& input = Input::inputs[player]; - - if ( !players[player]->hotbar.useHotbarFaceMenu && input.consumeBinaryToggle("Hotbar Right") - && players[player]->bControlEnabled && !gamePaused && !players[player]->usingCommand() ) - { - if ( shootmode && !inputs.getUIInteraction(player)->itemMenuOpen && !openedChest[player] - && gui_mode != (GUI_MODE_SHOP) && !players[player]->bookGUI.bBookOpen - && !GenericGUI[player].isGUIOpen() ) - { - bumper_moved = true; - hotbar_t.hotbarTooltipLastGameTick = ticks; - players[player]->hotbar.selectHotbarSlot(players[player]->hotbar.current_hotbar + 1); - } - else - { - hotbar_t.hotbarTooltipLastGameTick = 0; - } - } - if ( !players[player]->hotbar.useHotbarFaceMenu && input.consumeBinaryToggle("Hotbar Left") - && players[player]->bControlEnabled && !gamePaused && !players[player]->usingCommand() ) - { - if ( shootmode && !inputs.getUIInteraction(player)->itemMenuOpen && !openedChest[player] - && gui_mode != (GUI_MODE_SHOP) && !players[player]->bookGUI.bBookOpen - && !GenericGUI[player].isGUIOpen() ) - { - bumper_moved = true; - hotbar_t.hotbarTooltipLastGameTick = ticks; - hotbar_t.selectHotbarSlot(hotbar_t.current_hotbar - 1); - } - else - { - hotbar_t.hotbarTooltipLastGameTick = 0; - } - } - - if ( bumper_moved && !inputs.getUIInteraction(player)->itemMenuOpen - && !openedChest[player] && gui_mode != (GUI_MODE_SHOP) - && !players[player]->bookGUI.bBookOpen - && !GenericGUI[player].isGUIOpen() ) - { - warpMouseToSelectedHotbarSlot(player); - } - - if ( !inputs.getUIInteraction(player)->itemMenuOpen && !inputs.getUIInteraction(player)->selectedItem && !openedChest[player] && gui_mode != (GUI_MODE_SHOP) ) - { - if ( shootmode && input.consumeBinaryToggle("Hotbar Up / Select") - && (!hotbar_t.useHotbarFaceMenu || (hotbar_t.useHotbarFaceMenu && !inputs.hasController(player))) - && players[player]->bControlEnabled && !gamePaused - && !players[player]->usingCommand() - && !openedChest[player] && gui_mode != (GUI_MODE_SHOP) - && !players[player]->bookGUI.bBookOpen - && !GenericGUI[player].isGUIOpen() ) - { - //Show a tooltip - hotbar_t.hotbarTooltipLastGameTick = std::max(ticks - TICKS_PER_SECOND, ticks - hotbar_t.hotbarTooltipLastGameTick); - - //Activate a hotbar slot if in-game. - item = uidToItem(hotbar[hotbar_t.current_hotbar].item); - } - - // We don't have a hotbar clear binding, but if we need one, feel free to add it - // in MainMenu.cpp - - /*if ( !shootmode && inputs.bControllerInputPressed(player, INJOY_MENU_HOTBAR_CLEAR) && !players[player]->bookGUI.bBookOpen ) //TODO: Don't activate if any of the previous if statement's conditions are true? - { - //Clear a hotbar slot if in-inventory. - inputs.controllerClearInput(player, INJOY_MENU_HOTBAR_CLEAR); - - hotbar[hotbar_t.current_hotbar].item = 0; - }*/ + if ( !EnemyHPDamageBarHandler::bDamageGibTypesEnabled ) + { + gibType = DamageGib::DMG_DEFAULT; + } - pos.x = initial_position.x + (hotbar_t.current_hotbar * hotbar_t.getSlotSize()); - pos.y = initial_position.y - hotbar_t.getSlotSize(); - //if ( !shootmode && !players[player]->bookGUI.bBookOpen && !openedChest[player] && inputs.bControllerInputPressed(player, INJOY_MENU_DROP_ITEM) - // && mouseInBounds(player, pos.x, pos.x + hotbar_img->w * uiscale_hotbar, pos.y, pos.y + hotbar_img->h * uiscale_hotbar) ) - //{ - // //Drop item if this hotbar is currently active & the player pressed the cancel button on the gamepad (typically "b"). - // inputs.controllerClearInput(player, INJOY_MENU_DROP_ITEM); - // Item* itemToDrop = uidToItem(hotbar[hotbar_t.current_hotbar].item); - // if ( itemToDrop ) - // { - // dropItem(itemToDrop, player); - // } - //} + EnemyHPDamageBarHandler::EnemyHPDetails* details = nullptr; + if ( player >= 0 /*&& players[player]->isLocalPlayer()*/ ) + { + // add enemy bar to the server + int p = player; + if ( !players[player]->isLocalPlayer() ) + { + p = clientnum; // remote clients, add it to the local list. } - - if ( item ) + if ( splitscreen ) // check if anyone else has this enemy bar on their screen { - bool badpotion = false; - bool learnedSpell = false; - if ( itemCategory(item) == POTION ) - { - badpotion = isPotionBad(*item); - } - - if ( itemCategory(item) == SPELLBOOK && (item->identified || itemIsEquipped(item, player)) ) - { - // equipped spellbook will unequip on use. - learnedSpell = (playerLearnedSpellbook(player, item) || itemIsEquipped(item, player)); - } - - if ( (keystatus[SDLK_LALT] || keystatus[SDLK_RALT]) - && (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK || item->type == FOOD_CREAMPIE) ) - { - badpotion = true; - learnedSpell = true; - } - - if ( !learnedSpell && item->identified - && itemCategory(item) == SPELLBOOK && players[player] && players[player]->entity ) + for ( int i = 0; i < MAXPLAYERS; ++i ) { - learnedSpell = true; // let's always equip/unequip spellbooks from the hotbar? - spell_t* currentSpell = getSpellFromID(getSpellIDFromSpellbook(item->type)); - if ( currentSpell && stats[player] ) + if ( players[i]->isLocalPlayer() ) { - int skillLVL = stats[player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[player], players[player]->entity); - if ( stats[player]->getModifiedProficiency(PRO_MAGIC) >= 100 ) - { - skillLVL = 100; - } - if ( skillLVL >= currentSpell->difficulty ) + if ( enemyHPDamageBarHandler[i].HPBars.find(target->getUID()) != enemyHPDamageBarHandler[i].HPBars.end() ) { - // can learn spell, try that instead. - learnedSpell = false; + p = i; + break; } } } - - if ( itemCategory(item) == SPELLBOOK && stats[player] ) + } + if ( stats ) + { + details = enemyHPDamageBarHandler[p].addEnemyToList(hp, maxhp, oldhp, target->getUID(), name, lowPriorityTick, gibType); + } + else + { + details = enemyHPDamageBarHandler[p].addEnemyToList(hp, maxhp, oldhp, target->getUID(), name, lowPriorityTick, gibType); + } + } + + if ( player >= 0 && multiplayer == SERVER ) + { + // send to all remote players + for ( int p = 1; p < MAXPLAYERS; ++p ) + { + if ( !players[p]->isLocalPlayer() ) { - if ( stats[player]->type == GOBLIN || stats[player]->type == CREATURE_IMP - || (stats[player]->playerRace == RACE_GOBLIN && stats[player]->stat_appearance == 0) ) + if ( p == playertarget ) { - learnedSpell = true; // goblinos can't learn spells but always equip books. + continue; } - } - - bool disableItemUsage = false; - if ( players[player] && players[player]->entity ) - { - if ( players[player]->entity->effectShapeshift != NOTHING ) + strcpy((char*)net_packet->data, "ENHP"); + SDLNet_Write16(static_cast(hp), &net_packet->data[4]); + SDLNet_Write16(static_cast(maxhp), &net_packet->data[6]); + if ( stats ) { - if ( !item->usableWhileShapeshifted(stats[player]) ) - { - disableItemUsage = true; - } + SDLNet_Write16(static_cast(oldhp), &net_packet->data[8]); } else { - if ( itemCategory(item) == SPELL_CAT && item->appearance >= 1000 ) - { - if ( canUseShapeshiftSpellInCurrentForm(player, *item) != 1 ) - { - disableItemUsage = true; - } - } - } - } - if ( client_classes[player] == CLASS_SHAMAN ) - { - if ( item->type == SPELL_ITEM && !(playerUnlockedShamanSpell(player, item)) ) - { - disableItemUsage = true; - } - } - - if ( !disableItemUsage ) - { - if ( !badpotion && !learnedSpell ) - { - if ( !(isItemEquippableInShieldSlot(item) && cast_animation[player].active_spellbook) ) - { - if ( stats[player] && stats[player]->type == AUTOMATON - && (item->type == TOOL_METAL_SCRAP || item->type == TOOL_MAGIC_SCRAP) ) - { - // consume item - if ( multiplayer == CLIENT ) - { - strcpy((char*)net_packet->data, "FODA"); - SDLNet_Write32((Uint32)item->type, &net_packet->data[4]); - SDLNet_Write32((Uint32)item->status, &net_packet->data[8]); - SDLNet_Write32((Uint32)item->beatitude, &net_packet->data[12]); - SDLNet_Write32((Uint32)item->count, &net_packet->data[16]); - SDLNet_Write32((Uint32)item->appearance, &net_packet->data[20]); - net_packet->data[24] = item->identified; - net_packet->data[25] = player; - net_packet->address.host = net_server.host; - net_packet->address.port = net_server.port; - net_packet->len = 26; - sendPacketSafe(net_sock, -1, net_packet, 0); - } - item_FoodAutomaton(item, player); - } - else - { - useItem(item, player); - } - } + SDLNet_Write16(static_cast(oldhp), &net_packet->data[8]); } - else + SDLNet_Write32(target->getUID(), &net_packet->data[10]); + net_packet->data[14] = lowPriorityTick ? 1 : 0; // 1 == true + if ( EnemyHPDamageBarHandler::bDamageGibTypesEnabled ) { - playerTryEquipItemAndUpdateServer(player, item, false); + net_packet->data[14] |= (gibType << 1) & 0xFE; } - } - else - { - if ( client_classes[player] == CLASS_SHAMAN && item->type == SPELL_ITEM ) + if ( stats && details ) { - messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3488)); // unable to use with current level. + SDLNet_Write32(details->enemy_statusEffects1, &net_packet->data[15]); + SDLNet_Write32(details->enemy_statusEffects2, &net_packet->data[19]); + SDLNet_Write32(details->enemy_statusEffects3, &net_packet->data[23]); + SDLNet_Write32(details->enemy_statusEffects4, &net_packet->data[27]); + SDLNet_Write32(details->enemy_statusEffects5, &net_packet->data[31]); + SDLNet_Write32(details->enemy_statusEffectsLowDuration1, &net_packet->data[35]); + SDLNet_Write32(details->enemy_statusEffectsLowDuration2, &net_packet->data[39]); + SDLNet_Write32(details->enemy_statusEffectsLowDuration3, &net_packet->data[43]); + SDLNet_Write32(details->enemy_statusEffectsLowDuration4, &net_packet->data[47]); + SDLNet_Write32(details->enemy_statusEffectsLowDuration5, &net_packet->data[51]); } else { - messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3432)); // unable to use in current form message. - } - playSoundPlayer(player, 90, 64); + SDLNet_Write32(0, &net_packet->data[15]); + SDLNet_Write32(0, &net_packet->data[19]); + SDLNet_Write32(0, &net_packet->data[23]); + SDLNet_Write32(0, &net_packet->data[27]); + SDLNet_Write32(0, &net_packet->data[31]); + SDLNet_Write32(0, &net_packet->data[35]); + SDLNet_Write32(0, &net_packet->data[39]); + SDLNet_Write32(0, &net_packet->data[43]); + SDLNet_Write32(0, &net_packet->data[47]); + SDLNet_Write32(0, &net_packet->data[51]); + } + strcpy((char*)(&net_packet->data[55]), name); + net_packet->data[55 + strlen(name)] = 0; + net_packet->address.host = net_clients[p - 1].host; + net_packet->address.port = net_clients[p - 1].port; + net_packet->len = 55 + strlen(name) + 1; + sendPacketSafe(net_sock, -1, net_packet, p - 1); + } } } +} - FollowerMenu[player].drawFollowerMenu(); +/*------------------------------------------------------------------------------- - //// stat increase icons - //pos.w = 64; - //pos.h = 64; - //pos.x = players[player]->camera_x2() - pos.w * 3 - 9; - //pos.y = players[player]->characterSheet.skillsSheetBox.h + (32 + pos.h * 2 + 3); // 131px from end of prof window. + drawStatus - //if ( (!shootmode || players[player]->characterSheet.lock_right_sidebar) && players[player]->characterSheet.proficienciesPage == 1 - // && pos.y < (players[player]->characterSheet.partySheetBox.y + players[player]->characterSheet.partySheetBox.h + 16) ) - //{ - // pos.y = players[player]->characterSheet.partySheetBox.y + players[player]->characterSheet.partySheetBox.h + 16; - //} + Draws various status bar elements, such as textbox, health, magic, + and the hotbar - //if ( splitscreen ) - //{ - // // todo - adjust position. - // pos.w = 48; - // pos.h = 48; - // pos.x = players[player]->camera_x2() - pos.w * 3 - 9; - // pos.y = players[player]->characterSheet.skillsSheetBox.h + (16 + pos.h * 2 + 3); - //} - //else - //{ - // if ( pos.y + pos.h > (players[player]->camera_y2() - minimaps[player].y - minimaps[player].h) ) // check if overlapping minimap - // { - // pos.y = (players[player]->camera_y2() - minimaps[player].y - minimaps[player].h) - (64 + 3); // align above minimap - // } - //} - - SDL_Surface *tmp_bmp = NULL; +-------------------------------------------------------------------------------*/ - for ( i = 0; i < NUMSTATS; i++ ) - { - if ( stats[player] && stats[player]->PLAYER_LVL_STAT_TIMER[i] > 0 && ((ticks % 50) - (ticks % 10)) ) - { - stats[player]->PLAYER_LVL_STAT_TIMER[i]--; +bool mouseInBoundsRealtimeCoords(int, int, int, int, int); //Defined in playerinventory.cpp. Dirty hack, you should be ashamed of yourself. - //switch ( i ) - //{ - // // prepare the stat image. - // case STAT_STR: - // tmp_bmp = str_bmp64u; - // break; - // case STAT_DEX: - // tmp_bmp = dex_bmp64u; - // break; - // case STAT_CON: - // tmp_bmp = con_bmp64u; - // break; - // case STAT_INT: - // tmp_bmp = int_bmp64u; - // break; - // case STAT_PER: - // tmp_bmp = per_bmp64u; - // break; - // case STAT_CHR: - // tmp_bmp = chr_bmp64u; - // break; - // default: - // break; - //} - drawImageScaled(tmp_bmp, NULL, &pos); - if ( stats[player]->PLAYER_LVL_STAT_TIMER[i + NUMSTATS] > 0 ) - { - // bonus stat acheived, draw additional stat icon above. - pos.y -= 64 + 3; - drawImageScaled(tmp_bmp, NULL, &pos); - pos.y += 64 + 3; - stats[player]->PLAYER_LVL_STAT_TIMER[i + NUMSTATS]--; - } +bool warpMouseToSelectedHotbarSlot(const int player) +{ + if ( players[player]->shootmode == true) + { + return false; + } - pos.x += pos.h + 3; + if ( auto hotbarSlotFrame = players[player]->hotbar.getHotbarSlotFrame(players[player]->hotbar.current_hotbar) ) + { + if ( !players[player]->hotbar.isInteractable ) + { + players[player]->inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_HOTBAR; + players[player]->inventoryUI.cursor.queuedFrameToWarpTo = hotbarSlotFrame; + return false; + } + else + { + //messagePlayer(0, "[Debug]: select item warped"); + players[player]->inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_NONE; + players[player]->inventoryUI.cursor.queuedFrameToWarpTo = nullptr; + hotbarSlotFrame->warpMouseToFrame(player, (Inputs::SET_CONTROLLER)); } + return true; } + + return false; } + +//void drawStatus(int player) +//{ +// SDL_Rect pos, initial_position; +// Sint32 x, y, z, c, i; +// node_t* node; +// string_t* string; +// +// const int x1 = players[player]->camera_x1(); +// const int x2 = players[player]->camera_x2(); +// const int y1 = players[player]->camera_y1(); +// const int y2 = players[player]->camera_y2(); +// +// const Sint32 mousex = inputs.getMouse(player, Inputs::X); +// const Sint32 mousey = inputs.getMouse(player, Inputs::Y); +// const Sint32 omousex = inputs.getMouse(player, Inputs::OX); +// const Sint32 omousey = inputs.getMouse(player, Inputs::OY); +// const Sint32 mousexrel = inputs.getMouse(player, Inputs::XREL); +// const Sint32 mouseyrel = inputs.getMouse(player, Inputs::YREL); +// +// //pos.x = players[player]->statusBarUI.getStartX(); +// auto& hotbar_t = players[player]->hotbar; +// auto& hotbar = hotbar_t.slots(); +// +// int gui_mode = players[player]->gui_mode; +// bool shootmode = players[player]->shootmode; +// +// if ( !hide_statusbar ) +// { +// //pos.y = players[player]->statusBarUI.getStartY(); +// } +// else +// { +// pos.y = y2 - 16; +// } +// //To garner the position of the hotbar. +// initial_position.x = 0; /*hotbar_t.getStartX();*/ +// initial_position.y = pos.y; +// initial_position.w = 0; +// initial_position.h = 0; +// //pos.w = status_bmp->w * uiscale_chatlog; +// //pos.h = status_bmp->h * uiscale_chatlog; +// //if ( !hide_statusbar ) +// //{ +// // drawImageScaled(status_bmp, NULL, &pos); +// //} +// +// ////players[player]->statusBarUI.messageStatusBarBox.x = pos.x; +// ////players[player]->statusBarUI.messageStatusBarBox.y = pos.y; +// ////players[player]->statusBarUI.messageStatusBarBox.w = pos.w; +// ////players[player]->statusBarUI.messageStatusBarBox.h = pos.h; +// +// //// enemy health +// //enemyHPDamageBarHandler[player].displayCurrentHPBar(player); +// +// //// messages +// //if ( !hide_statusbar ) +// //{ +// // x = players[player]->statusBarUI.getStartX() + 24 * uiscale_chatlog; +// // y = players[player]->camera_y2(); +// // textscroll = std::min(list_Size(&messages) - 3, textscroll); +// // c = 0; +// // for ( node = messages.last; node != NULL; node = node->prev ) +// // { +// // c++; +// // if ( c <= textscroll ) +// // { +// // continue; +// // } +// // string = (string_t*)node->element; +// // if ( uiscale_chatlog >= 1.5 ) +// // { +// // y -= TTF16_HEIGHT * string->lines; +// // if ( y < y2 - (status_bmp->h * uiscale_chatlog) + 8 * uiscale_chatlog ) +// // { +// // break; +// // } +// // } +// // else if ( uiscale_chatlog != 1.f ) +// // { +// // y -= TTF12_HEIGHT * string->lines; +// // if ( y < y2 - status_bmp->h * 1.1 + 4 ) +// // { +// // break; +// // } +// // } +// // else +// // { +// // y -= TTF12_HEIGHT * string->lines; +// // if ( y < y2 - status_bmp->h + 4 ) +// // { +// // break; +// // } +// // } +// // z = 0; +// // for ( i = 0; i < strlen(string->data); i++ ) +// // { +// // if ( string->data[i] != 10 ) // newline +// // { +// // z++; +// // } +// // else +// // { +// // z = 0; +// // } +// // if ( z == 65 ) +// // { +// // if ( string->data[i] != 10 ) +// // { +// // char* tempString = (char*)malloc(sizeof(char) * (strlen(string->data) + 2)); +// // strcpy(tempString, string->data); +// // strcpy((char*)(tempString + i + 1), (char*)(string->data + i)); +// // tempString[i] = 10; +// // free(string->data); +// // string->data = tempString; +// // string->lines++; +// // } +// // z = 0; +// // } +// // } +// // Uint32 color = makeColor( 0, 0, 0, 255); // black color +// // if ( uiscale_chatlog >= 1.5 ) +// // { +// // ttfPrintTextColor(ttf16, x, y, color, false, string->data); +// // } +// // else +// // { +// // ttfPrintTextColor(ttf12, x, y, color, false, string->data); +// // } +// // } +// // if ( inputs.bMouseLeft(player) ) +// // { +// // if ( omousey >= y2 - status_bmp->h * uiscale_chatlog + 7 && omousey < y2 - status_bmp->h * uiscale_chatlog + (7 + 27) * uiscale_chatlog ) +// // { +// // if ( omousex >= players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog +// // && omousex < players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog + 11 * uiscale_chatlog ) +// // { +// // // text scroll up +// // buttonclick = 3; +// // textscroll++; +// // inputs.mouseClearLeft(player); +// // } +// // } +// // else if ( omousey >= y2 - status_bmp->h * uiscale_chatlog + 34 && omousey < y2 - status_bmp->h * uiscale_chatlog + (34 + 28) * uiscale_chatlog ) +// // { +// // if ( omousex >= players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog +// // && omousex < players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog + 11 * uiscale_chatlog ) +// // { +// // // text scroll down +// // buttonclick = 12; +// // textscroll--; +// // if ( textscroll < 0 ) +// // { +// // textscroll = 0; +// // } +// // inputs.mouseClearLeft(player); +// // } +// // } +// // else if ( omousey >= y2 - status_bmp->h * uiscale_chatlog + 62 && omousey < y2 - status_bmp->h * uiscale_chatlog + (62 + 31) * uiscale_chatlog ) +// // { +// // if ( omousex >= players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog +// // && omousex < players[player]->statusBarUI.getStartX() + 618 * uiscale_chatlog + 11 * uiscale_chatlog ) +// // { +// // // text scroll down all the way +// // buttonclick = 4; +// // textscroll = 0; +// // inputs.mouseClearLeft(player); +// // } +// // } +// // /*else if( omousey>=y2-status_bmp->h+8 && omouseyh+8+30 ) { +// // if( omousex>=players[player]->statusBarUI.getStartX()+618 && omousexstatusBarUI.getStartX()+618+11 ) { +// // // text scroll up all the way +// // buttonclick=13; +// // textscroll=list_Size(&messages)-4; +// // mousestatus[SDL_BUTTON_LEFT]=0; +// // } +// // }*/ +// // } +// +// // // mouse wheel +// // if ( !shootmode ) +// // { +// // if ( mousex >= players[player]->statusBarUI.getStartX() && mousex < players[player]->statusBarUI.getStartX() + status_bmp->w * uiscale_chatlog ) +// // { +// // if ( mousey >= initial_position.y && mousey < initial_position.y + status_bmp->h * uiscale_chatlog ) +// // { +// // if ( mousestatus[SDL_BUTTON_WHEELDOWN] ) +// // { +// // mousestatus[SDL_BUTTON_WHEELDOWN] = 0; +// // textscroll--; +// // if ( textscroll < 0 ) +// // { +// // textscroll = 0; +// // } +// // } +// // else if ( mousestatus[SDL_BUTTON_WHEELUP] ) +// // { +// // mousestatus[SDL_BUTTON_WHEELUP] = 0; +// // textscroll++; +// // } +// // } +// // } +// // } +// // if (showfirst) +// // { +// // textscroll = list_Size(&messages) - 3; +// // } +// +// +// // //Text scroll up button. +// // if ( buttonclick == 3 ) +// // { +// // pos.x = players[player]->statusBarUI.getStartX() + 617 * uiscale_chatlog; +// // pos.y = y2 - status_bmp->h * uiscale_chatlog + 7 * uiscale_chatlog; +// // pos.w = 11 * uiscale_chatlog; +// // pos.h = 27 * uiscale_chatlog; +// // drawRect(&pos, makeColorRGB(255, 255, 255), 80); +// // //drawImage(textup_bmp, NULL, &pos); +// // } +// // //Text scroll down all the way button. +// // if ( buttonclick == 4 ) +// // { +// // pos.x = players[player]->statusBarUI.getStartX() + 617 * uiscale_chatlog; +// // pos.y = y2 - status_bmp->h * uiscale_chatlog + 62 * uiscale_chatlog; +// // pos.w = 11 * uiscale_chatlog; +// // pos.h = 31 * uiscale_chatlog; +// // drawRect(&pos, makeColorRGB(255, 255, 255), 80); +// // //drawImage(textdown_bmp, NULL, &pos); +// // } +// // //Text scroll down button. +// // if ( buttonclick == 12 ) +// // { +// // pos.x = players[player]->statusBarUI.getStartX() + 617 * uiscale_chatlog; +// // pos.y = y2 - status_bmp->h * uiscale_chatlog + 34 * uiscale_chatlog; +// // pos.w = 11 * uiscale_chatlog; +// // pos.h = 28 * uiscale_chatlog; +// // drawRect(&pos, makeColorRGB(255, 255, 255), 80); +// // //drawImage(textup_bmp, NULL, &pos); +// // } +// // //Text scroll up all the way button. +// // /*if( buttonclick==13 ) { +// // pos.x=players[player]->statusBarUI.getStartX()+617; pos.y=y2-status_bmp->h+8; +// // pos.w=11; pos.h=30; +// // drawRect(&pos,0xffffffff,80); +// // //drawImage(textdown_bmp, NULL, &pos); +// // }*/ +// //} +// +// //int playerStatusBarWidth = 38 * uiscale_playerbars; +// //int playerStatusBarHeight = 156 * uiscale_playerbars; +// +// //if ( !players[player]->hud.hpFrame ) +// //{ +// // drawHPMPBars(player); +// //} +// // +// //// hunger icon +// //if ( stats[player] && stats[player]->type != AUTOMATON +// // && (svFlags & SV_FLAG_HUNGER) && stats[player]->HUNGER <= 250 && (ticks % 50) - (ticks % 25) ) +// //{ +// // pos.x = /*xoffset*/ + playerStatusBarWidth + 10; // was pos.x = 128; +// // pos.y = y2 - 160; +// // pos.w = 64; +// // pos.h = 64; +// // if ( playerRequiresBloodToSustain(player) ) +// // { +// // drawImageScaled(hunger_blood_bmp, NULL, &pos); +// // } +// // else +// // { +// // drawImageScaled(hunger_bmp, NULL, &pos); +// // } +// //} +// +// //if ( stats[player] && stats[player]->type == AUTOMATON ) +// //{ +// // if ( stats[player]->HUNGER > 300 || (ticks % 50) - (ticks % 25) ) +// // { +// // pos.x = /*xoffset*/ + playerStatusBarWidth + 10; // was pos.x = 128; +// // pos.y = y2 - 160; +// // pos.w = 64; +// // pos.h = 64; +// // if ( stats[player]->HUNGER > 1200 ) +// // { +// // drawImageScaled(hunger_boiler_hotflame_bmp, nullptr, &pos); +// // } +// // else +// // { +// // if ( stats[player]->HUNGER > 600 ) +// // { +// // drawImageScaledPartial(hunger_boiler_flame_bmp, nullptr, &pos, 1.f); +// // } +// // else +// // { +// // float percent = (stats[player]->HUNGER - 300) / 300.f; // always show a little bit more at the bottom (10-20%) +// // drawImageScaledPartial(hunger_boiler_flame_bmp, nullptr, &pos, percent); +// // } +// // } +// // drawImageScaled(hunger_boiler_bmp, nullptr, &pos); +// // } +// //} +// +// //// minotaur icon +// //if ( minotaurlevel && (ticks % 50) - (ticks % 25) ) +// //{ +// // pos.x = /*xoffset*/ + playerStatusBarWidth + 10; // was pos.x = 128; +// // pos.y = y2 - 160 + 64 + 2; +// // pos.w = 64; +// // pos.h = 64; +// // drawImageScaled(minotaur_bmp, nullptr, &pos); +// //} +// +// // draw action prompts. +// /*if ( players[player]->hud.bShowActionPrompts ) +// { +// int skill = (ticks / 100) % 16; +// int iconSize = 48; +// SDL_Rect skillPos{0, 0, iconSize, iconSize }; +// skillPos.x = hotbar_t.hotbarBox.x - 3.5 * iconSize; +// skillPos.y = players[player]->camera_y2() - 106 - iconSize - 16; +// std::string promptString; +// players[player]->hud.drawActionIcon(skillPos, players[player]->hud.getActionIconForPlayer(Player::HUD_t::ACTION_PROMPT_OFFHAND, promptString)); +// players[player]->hud.drawActionGlyph(skillPos, Player::HUD_t::ACTION_PROMPT_OFFHAND); +// +// skillPos.x = hotbar_t.hotbarBox.x - 2.5 * iconSize; +// players[player]->hud.drawActionIcon(skillPos, players[player]->hud.getActionIconForPlayer(Player::HUD_t::ACTION_PROMPT_MAINHAND, promptString)); +// players[player]->hud.drawActionGlyph(skillPos, Player::HUD_t::ACTION_PROMPT_MAINHAND); +// +// skillPos.x = hotbar_t.hotbarBox.x + hotbar_t.hotbarBox.w + 0.5 * iconSize; +// players[player]->hud.drawActionIcon(skillPos, players[player]->hud.getActionIconForPlayer(Player::HUD_t::ACTION_PROMPT_MAGIC, promptString)); +// players[player]->hud.drawActionGlyph(skillPos, Player::HUD_t::ACTION_PROMPT_MAGIC); +// }*/ +// +// Item* item = nullptr; +// //Now the hotbar. +// int num = 0; +// //Reset the position to the top left corner of the status bar to draw the hotbar slots.. +// //pos.x = initial_position.x; +// pos.x = 0; /*hotbar_t.getStartX();*/ +// pos.y = initial_position.y - hotbar_t.getSlotSize(); +// +// hotbar_t.hotbarBox.x = pos.x; +// hotbar_t.hotbarBox.y = pos.y; +// hotbar_t.hotbarBox.w = NUM_HOTBAR_SLOTS * hotbar_t.getSlotSize(); +// hotbar_t.hotbarBox.h = hotbar_t.getSlotSize(); +// +// if ( players[player]->hotbar.useHotbarFaceMenu ) +// { +// players[player]->hotbar.initFaceButtonHotbar(); +// } +// +// for ( num = 0; num < NUM_HOTBAR_SLOTS; ++num ) +// { +// Uint32 color; +// if ( players[player]->hotbar.current_hotbar == num && !openedChest[player] ) +// { +// color = makeColor( 255, 255, 0, 255); //Draw gold border around currently selected hotbar. +// } +// else +// { +// color = makeColor( 255, 255, 255, 60); //Draw normal grey border. +// } +// pos.w = hotbar_t.getSlotSize(); +// pos.h = hotbar_t.getSlotSize(); +// +// if ( players[player]->hotbar.useHotbarFaceMenu ) +// { +// pos.x = players[player]->hotbar.faceButtonPositions[num].x; +// pos.y = players[player]->hotbar.faceButtonPositions[num].y; +// } +// +// //drawImageScaledColor(hotbar_img, NULL, &pos, color); +// item = uidToItem(hotbar[num].item); +// if ( item ) +// { +// if ( item->type == BOOMERANG ) +// { +// hotbar_t.magicBoomerangHotbarSlot = num; +// } +// bool used = false; +// pos.w = hotbar_t.getSlotSize(); +// pos.h = hotbar_t.getSlotSize(); +// +// SDL_Rect highlightBox; +// highlightBox.x = pos.x + 2; +// highlightBox.y = pos.y + 2; +// highlightBox.w = 60 * uiscale_hotbar; +// highlightBox.h = 60 * uiscale_hotbar; +// +// if ( !item->identified ) +// { +// // give it a yellow background if it is unidentified +// drawRect(&highlightBox, makeColorRGB(128, 128, 0), 64); //31875 +// } +// else if ( item->beatitude < 0 ) +// { +// // give it a red background if cursed +// drawRect(&highlightBox, makeColorRGB(128, 0, 0), 64); +// } +// else if ( item->beatitude > 0 ) +// { +// // give it a green background if blessed (light blue if colorblind mode) +// if ( colorblind ) +// { +// drawRect(&highlightBox, makeColorRGB(50, 128, 128), 64); +// } +// else +// { +// drawRect(&highlightBox, makeColorRGB(0, 128, 0), 64); +// } +// } +// if ( item->status == BROKEN ) +// { +// drawRect(&highlightBox, makeColorRGB(64, 64, 64), 125); +// } +// +// Uint32 itemColor = makeColor( 255, 255, 255, 255); +// if ( hotbar_t.useHotbarFaceMenu && hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_NONE ) +// { +// bool dimColor = false; +// if ( hotbar_t.faceMenuButtonHeld != hotbar_t.getFaceMenuGroupForSlot(num) ) +// { +// dimColor = true; +// } +// if ( dimColor ) +// { +// itemColor = makeColor( 255, 255, 255, 128); +// } +// } +// drawImageScaledColor(itemSprite(item), NULL, &pos, itemColor); +// +// bool disableItemUsage = false; +// +// if ( players[player] && players[player]->entity && players[player]->entity->effectShapeshift != NOTHING ) +// { +// // shape shifted, disable some items +// if ( !item->usableWhileShapeshifted(stats[player]) ) +// { +// disableItemUsage = true; +// drawRect(&highlightBox, makeColorRGB(64, 64, 64), 144); +// } +// } +// if ( client_classes[player] == CLASS_SHAMAN ) +// { +// if ( item->type == SPELL_ITEM && !(playerUnlockedShamanSpell(player, item)) ) +// { +// disableItemUsage = true; +// drawRect(&highlightBox, makeColorRGB(64, 64, 64), 144); +// } +// } +// +// if ( stats[player] && stats[player]->HP > 0 ) +// { +// Item*& selectedItem = inputs.getUIInteraction(player)->selectedItem; +// +// if ( !shootmode && mouseInBounds(player, pos.x, pos.x + hotbar_t.getSlotSize(), pos.y, pos.y + hotbar_t.getSlotSize()) ) +// { +// if ( (inputs.bMouseLeft(player) +// || (inputs.bControllerInputPressed(player, INJOY_MENU_LEFT_CLICK) +// && !openedChest[player] +// && gui_mode != (GUI_MODE_SHOP) +// && !GenericGUI[player].isGUIOpen())) +// && !selectedItem ) +// { +// inputs.getUIInteraction(player)->toggleclick = false; +// if ( keystatus[SDLK_LSHIFT] || keystatus[SDLK_RSHIFT] ) +// { +// hotbar[num].item = 0; +// hotbar[num].resetLastItem(); +// } +// else +// { +// //Remove the item if left clicked. +// selectedItem = item; +// if ( selectedItem ) +// { +// playSound(139, 64); // click sound +// } +// hotbar[num].item = 0; +// hotbar[num].resetLastItem(); +// +// if ( inputs.bControllerInputPressed(player, INJOY_MENU_LEFT_CLICK) && !openedChest[player] && gui_mode != (GUI_MODE_SHOP) ) +// { +// inputs.controllerClearInput(player, INJOY_MENU_LEFT_CLICK); +// inputs.getUIInteraction(player)->toggleclick = true; +// inputs.getUIInteraction(player)->selectedItemFromHotbar = num; +// //TODO: Change the mouse cursor to THE HAND. +// } +// } +// } +// if ( inputs.bMouseRight(player) +// || (inputs.bControllerInputPressed(player, INJOY_MENU_USE) +// && !openedChest[player] +// && gui_mode != (GUI_MODE_SHOP) +// && !GenericGUI[player].isGUIOpen()) ) +// { +// //Use the item if right clicked. +// inputs.mouseClearRight(player); +// inputs.controllerClearInput(player, INJOY_MENU_USE); +// bool badpotion = false; +// bool learnedSpell = false; +// +// if ( itemCategory(item) == POTION && item->identified ) +// { +// badpotion = isPotionBad(*item); //So that you wield empty potions be default. +// } +// if ( item->type == POTION_EMPTY ) +// { +// badpotion = true; +// } +// if ( itemCategory(item) == SPELLBOOK && (item->identified || itemIsEquipped(item, player)) ) +// { +// // equipped spellbook will unequip on use. +// learnedSpell = (playerLearnedSpellbook(player, item) || itemIsEquipped(item, player)); +// } +// +// if ( keystatus[SDLK_LSHIFT] || keystatus[SDLK_RSHIFT] ) +// { +// players[player]->inventoryUI.appraisal.appraiseItem(item); +// } +// else +// { +// if ( (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK || item->type == FOOD_CREAMPIE ) +// && (keystatus[SDLK_LALT] || keystatus[SDLK_RALT]) ) +// { +// badpotion = true; +// learnedSpell = true; +// } +// +// if ( !learnedSpell && item->identified +// && itemCategory(item) == SPELLBOOK && players[player] && players[player]->entity ) +// { +// learnedSpell = true; // let's always equip/unequip spellbooks from the hotbar? +// spell_t* currentSpell = getSpellFromID(getSpellIDFromSpellbook(item->type)); +// if ( currentSpell ) +// { +// int skillLVL = stats[player]->getModifiedProficiency(PRO_LEGACY_MAGIC) + statGetINT(stats[player], players[player]->entity); +// if ( stats[player]->getModifiedProficiency(PRO_LEGACY_MAGIC) >= 100 ) +// { +// skillLVL = 100; +// } +// if ( skillLVL >= currentSpell->difficulty ) +// { +// // can learn spell, try that instead. +// learnedSpell = false; +// } +// } +// } +// +// if ( itemCategory(item) == SPELLBOOK && stats[player] +// && (stats[player]->type == GOBLIN +// || (stats[player]->playerRace == RACE_GOBLIN && stats[player]->stat_appearance == 0)) ) +// { +// learnedSpell = true; // goblinos can't learn spells but always equip books. +// } +// +// if ( !badpotion && !learnedSpell ) +// { +// if ( !(isItemEquippableInShieldSlot(item) && cast_animation[player].active_spellbook) ) +// { +// if ( !disableItemUsage ) +// { +// if ( stats[player] && stats[player]->type == AUTOMATON +// && (item->type == TOOL_METAL_SCRAP || item->type == TOOL_MAGIC_SCRAP) ) +// { +// // consume item +// if ( multiplayer == CLIENT ) +// { +// strcpy((char*)net_packet->data, "FODA"); +// SDLNet_Write32((Uint32)item->type, &net_packet->data[4]); +// SDLNet_Write32((Uint32)item->status, &net_packet->data[8]); +// SDLNet_Write32((Uint32)item->beatitude, &net_packet->data[12]); +// SDLNet_Write32((Uint32)item->count, &net_packet->data[16]); +// SDLNet_Write32((Uint32)item->appearance, &net_packet->data[20]); +// net_packet->data[24] = item->identified; +// net_packet->data[25] = player; +// net_packet->address.host = net_server.host; +// net_packet->address.port = net_server.port; +// net_packet->len = 26; +// sendPacketSafe(net_sock, -1, net_packet, 0); +// } +// item_FoodAutomaton(item, player); +// } +// else +// { +// useItem(item, player); +// } +// } +// else +// { +// if ( client_classes[player] == CLASS_SHAMAN && item->type == SPELL_ITEM ) +// { +// messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3488)); // unable to use with current level. +// } +// else +// { +// messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3432)); // unable to use in current form message. +// } +// playSoundPlayer(player, 90, 64); +// } +// } +// } +// else +// { +// if ( !disableItemUsage ) +// { +// playerTryEquipItemAndUpdateServer(player, item, false); +// } +// else +// { +// if ( client_classes[player] == CLASS_SHAMAN && item->type == SPELL_ITEM ) +// { +// messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3488)); // unable to use with current level. +// } +// else +// { +// messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3432)); // unable to use in current form message. +// } +// playSoundPlayer(player, 90, 64); +// } +// } +// used = true; +// if ( disableItemUsage ) +// { +// used = false; +// } +// } +// } +// } +// } +// +// // item count +// if ( !used ) +// { +// if ( item->count > 1 ) +// { +// int digits = numdigits_sint16(item->count); +// SDL_Surface* digitFont = font12x12_bmp; +// if ( uiscale_hotbar >= 1.5 ) +// { +// digitFont = font16x16_bmp; +// printTextFormatted(digitFont, pos.x + hotbar_t.getSlotSize() - (24 * digits), pos.y + hotbar_t.getSlotSize() - 24, "%d", item->count); +// } +// else +// { +// printTextFormatted(digitFont, pos.x + hotbar_t.getSlotSize() - (14 * digits), pos.y + hotbar_t.getSlotSize() - 14, "%d", item->count); +// } +// } +// +// SDL_Rect src; +// src.x = pos.x + 2; +// src.h = 16 * uiscale_hotbar; +// src.y = pos.y + hotbar_t.getSlotSize() - src.h - 2; +// src.w = 16 * uiscale_hotbar; +// +// // item equipped +// if ( itemCategory(item) != SPELL_CAT ) +// { +// if ( itemIsEquipped(item, player) ) +// { +// //drawImageScaled(equipped_bmp, NULL, &src); +// } +// else if ( item->status == BROKEN ) +// { +// //drawImageScaled(itembroken_bmp, NULL, &src); +// } +// } +// else +// { +// spell_t* spell = getSpellFromItem(player, item, false); +// if ( players[player]->magic.selectedSpell() == spell +// && (players[player]->magic.selected_spell_last_appearance == item->appearance || players[player]->magic.selected_spell_last_appearance == -1 ) ) +// { +// //drawImageScaled(equipped_bmp, NULL, &src); +// } +// } +// } +// } +// +// // draw hotbar slot 'numbers' or glyphs +// if ( players[player]->hotbar.useHotbarFaceMenu ) +// { +// if ( players[player]->hotbar.faceMenuAlternateLayout ) +// { +// SDL_Rect tmp{ pos.x, pos.y, pos.w, pos.h }; +// if ( num == 1 ) +// { +// tmp.y = pos.y + .75 * pos.h - 4; +// tmp.x = pos.x + pos.w / 2 + 16; +// tmp.x += pos.w; +// if ( !(hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_NONE && hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_LEFT) ) +// { +// //players[player]->hotbar.drawFaceButtonGlyph(num, tmp); +// } +// } +// else if ( num == 4 ) +// { +// tmp.y = pos.y + pos.h + 24; +// if ( !(hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_NONE && hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_MIDDLE) ) +// { +// //players[player]->hotbar.drawFaceButtonGlyph(num, tmp); +// } +// } +// else if ( num == 7 ) +// { +// tmp.y = pos.y + .75 * pos.h - 4; +// tmp.x = pos.x - (pos.w / 2 + 16); +// tmp.x -= pos.w; +// +// if ( !(hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_NONE && hotbar_t.faceMenuButtonHeld != hotbar_t.GROUP_RIGHT) ) +// { +// //players[player]->hotbar.drawFaceButtonGlyph(num, tmp); +// } +// } +// else +// { +// //players[player]->hotbar.drawFaceButtonGlyph(num, tmp); +// } +// } +// else +// { +// //players[player]->hotbar.drawFaceButtonGlyph(num, pos); +// } +// } +// else if ( uiscale_hotbar >= 1.5 ) +// { +// printTextFormatted(font16x16_bmp, pos.x + 2, pos.y + 2, "%d", (num + 1) % 10); // slot number +// } +// else +// { +// printTextFormatted(font12x12_bmp, pos.x + 2, pos.y + 2, "%d", (num + 1) % 10); // slot number +// } +// pos.x += hotbar_t.getSlotSize(); +// } +// +// bool drawHotBarTooltipOnCycle = false; +// if ( !intro ) +// { +// if ( hotbar_t.hotbarTooltipLastGameTick != 0 && (ticks - hotbar_t.hotbarTooltipLastGameTick) < TICKS_PER_SECOND * 2 ) +// { +// drawHotBarTooltipOnCycle = true; +// } +// else if ( players[player]->hotbar.useHotbarFaceMenu && players[player]->hotbar.faceMenuButtonHeld != Player::Hotbar_t::GROUP_NONE ) +// { +// drawHotBarTooltipOnCycle = true; +// } +// } +// +// if ( !shootmode || drawHotBarTooltipOnCycle ) +// { +// pos.x = initial_position.x; +// +// //Go back through all of the hotbar slots and draw the tooltips. +// for ( num = 0; num < NUM_HOTBAR_SLOTS; ++num ) +// { +// if ( players[player]->hotbar.useHotbarFaceMenu ) +// { +// pos.x = players[player]->hotbar.faceButtonPositions[num].x; +// pos.y = players[player]->hotbar.faceButtonPositions[num].y; +// } +// item = uidToItem(hotbar[num].item); +// if ( item ) +// { +// bool drawTooltipOnSlot = !shootmode && mouseInBounds(player, pos.x, pos.x + hotbar_t.getSlotSize(), pos.y, pos.y + hotbar_t.getSlotSize()); +// if ( !drawTooltipOnSlot ) +// { +// if ( drawHotBarTooltipOnCycle && players[player]->hotbar.current_hotbar == num ) +// { +// drawTooltipOnSlot = true; +// } +// } +// else +// { +// if ( !shootmode ) +// { +// // reset timer. +// hotbar_t.hotbarTooltipLastGameTick = 0; +// drawHotBarTooltipOnCycle = false; +// } +// else +// { +// if ( drawHotBarTooltipOnCycle ) +// { +// drawTooltipOnSlot = false; +// } +// } +// } +// +// if ( drawTooltipOnSlot ) +// { +// //Tooltip +// SDL_Rect src; +// src.x = mousex + 16; +// src.y = mousey + 8; +// +// if ( drawHotBarTooltipOnCycle ) +// { +// if ( players[player]->hotbar.useHotbarFaceMenu ) +// { +// src.x = pos.x + hotbar_t.getSlotSize() / 2; +// src.y = pos.y - 32; +// } +// else +// { +// src.x = pos.x + hotbar_t.getSlotSize(); +// src.y = pos.y + hotbar_t.getSlotSize(); +// src.y -= 16; +// } +// } +// +// if ( itemCategory(item) == SPELL_CAT ) +// { +// spell_t* spell = getSpellFromItem(player, item, false); +// if ( drawHotBarTooltipOnCycle ) +// { +// //drawSpellTooltip(player, spell, item, &src); +// } +// else +// { +// //drawSpellTooltip(player, spell, item, nullptr); +// } +// } +// else +// { +// src.w = std::max(13, longestline(item->description())) * TTF12_WIDTH + 8; +// src.h = TTF12_HEIGHT * 4 + 8; +// char spellEffectText[256] = ""; +// if ( item->identified ) +// { +// bool learnedSpellbook = false; +// if ( itemCategory(item) == SPELLBOOK ) +// { +// learnedSpellbook = playerLearnedSpellbook(player, item); +// if ( !learnedSpellbook && stats[player] && players[player] && players[player]->entity ) +// { +// // spellbook tooltip shows if you have the magic requirement as well (for goblins) +// int skillLVL = stats[player]->getModifiedProficiency(PRO_LEGACY_MAGIC) + statGetINT(stats[player], players[player]->entity); +// spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item->type)); +// if ( spell && skillLVL >= spell->difficulty ) +// { +// learnedSpellbook = true; +// } +// } +// } +// +// if ( itemCategory(item) == WEAPON || itemCategory(item) == ARMOR || itemCategory(item) == THROWN +// || itemTypeIsQuiver(item->type) ) +// { +// src.h += TTF12_HEIGHT; +// } +// else if ( itemCategory(item) == SCROLL && item->identified ) +// { +// src.h += TTF12_HEIGHT; +// src.w = std::max((2 + longestline(Language::get(3862)) + longestline(item->getScrollLabel())) * TTF12_WIDTH + 8, src.w); +// } +// else if ( itemCategory(item) == SPELLBOOK && learnedSpellbook ) +// { +// int height = 1; +// char effectType[32] = ""; +// int spellID = getSpellIDFromSpellbook(item->type); +// int damage = 0;;// drawSpellTooltip(player, getSpellFromID(spellID), item, nullptr); +// real_t dummy = 0.f; +// //getSpellEffectString(spellID, spellEffectText, effectType, damage, &height, &dummy); +// int width = longestline(spellEffectText) * TTF12_WIDTH + 8; +// if ( width > src.w ) +// { +// src.w = width; +// } +// src.h += height * TTF12_HEIGHT; +// } +// else if ( item->type == TOOL_GYROBOT || item->type == TOOL_DUMMYBOT +// || item->type == TOOL_SENTRYBOT || item->type == TOOL_SPELLBOT +// || (item->type == ENCHANTED_FEATHER && item->identified) ) +// { +// src.w += 7 * TTF12_WIDTH; +// } +// } +// +// int furthestX = players[player]->camera_x2(); +// /*if ( players[player]->characterSheet.proficienciesPage == 0 ) +// { +// if ( src.y < players[player]->characterSheet.skillsSheetBox.y + players[player]->characterSheet.skillsSheetBox.h ) +// { +// furthestX = players[player]->camera_x2() - players[player]->characterSheet.skillsSheetBox.w; +// } +// } +// else +// { +// if ( src.y < players[player]->characterSheet.partySheetBox.y + players[player]->characterSheet.partySheetBox.h ) +// { +// furthestX = players[player]->camera_x2() - players[player]->characterSheet.partySheetBox.w; +// } +// }*/ +// +// if ( drawHotBarTooltipOnCycle && players[player]->hotbar.useHotbarFaceMenu ) +// { +// // draw centred. +// src.x -= src.w / 2; +// src.y -= src.h; +// } +// +// if ( src.x + src.w + 16 > furthestX ) // overflow right side of screen +// { +// src.x -= (src.w + 32); +// } +// if ( src.y + src.h + 16 > y2 ) // overflow bottom of screen +// { +// src.y -= (src.y + src.h + 16 - y2); +// } +// +// if ( drawHotBarTooltipOnCycle ) +// { +// drawTooltip(&src); +// } +// else +// { +// drawTooltip(&src); +// } +// +// Uint32 color = 0xFFFFFFFF; +// if ( !item->identified ) +// { +// color = makeColorRGB(255, 255, 0); +// ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT, color, Language::get(309)); +// } +// else +// { +// if ( item->beatitude < 0 ) +// { +// color = makeColorRGB(255, 0, 0); +// ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT, color, Language::get(310)); +// } +// else if ( item->beatitude == 0 ) +// { +// color = 0xFFFFFFFF; +// ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT, color, Language::get(311)); +// } +// else +// { +// color = makeColorRGB(0, 255, 0); +// ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT, color, Language::get(312)); +// } +// } +// if ( item->beatitude == 0 || !item->identified ) +// { +// color = 0xFFFFFFFF; +// } +// +// if ( item->type == TOOL_GYROBOT || item->type == TOOL_DUMMYBOT +// || item->type == TOOL_SENTRYBOT || item->type == TOOL_SPELLBOT ) +// { +// int health = 100; +// if ( !item->tinkeringBotIsMaxHealth() ) +// { +// health = 25 * (item->appearance % 10); +// if ( health == 0 && item->status != BROKEN ) +// { +// health = 5; +// } +// } +// ttfPrintTextFormattedColor(ttf12, src.x + 4, src.y + 4, color, "%s (%d%%)", item->description(), health); +// } +// else if ( item->type == ENCHANTED_FEATHER && item->identified ) +// { +// ttfPrintTextFormattedColor(ttf12, src.x + 4, src.y + 4, color, "%s (%d%%)", item->description(), item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY); +// } +// else +// { +// ttfPrintTextFormattedColor(ttf12, src.x + 4, src.y + 4, color, "%s", item->description()); +// } +// int itemWeight = item->getWeight(); +// ttfPrintTextFormatted(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 2, Language::get(313), itemWeight); +// ttfPrintTextFormatted(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 3, Language::get(314), item->sellValue(player)); +// if ( strcmp(spellEffectText, "") ) +// { +// ttfPrintTextFormattedColor(ttf12, src.x + 4, src.y + 4 + TTF12_HEIGHT * 4, makeColorRGB(0, 255, 255), spellEffectText); +// } +// +// if ( item->identified && stats[player] ) +// { +// if ( itemCategory(item) == WEAPON || itemCategory(item) == THROWN +// || itemTypeIsQuiver(item->type) ) +// { +// Monster tmpRace = stats[player]->type; +// if ( stats[player]->type == TROLL +// || stats[player]->type == RAT +// || stats[player]->type == SPIDER +// || stats[player]->type == CREATURE_IMP ) +// { +// // these monsters have 0 bonus from weapons, but want the tooltip to say the normal amount. +// stats[player]->type = HUMAN; +// } +// +// if ( item->weaponGetAttack(stats[player]) >= 0 ) +// { +// color = makeColorRGB(0, 255, 255); +// } +// else +// { +// color = makeColorRGB(255, 0, 0); +// } +// if ( stats[player]->type != tmpRace ) +// { +// color = makeColorRGB(127, 127, 127); // grey out the text if monster doesn't benefit. +// } +// +// ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 4, color, Language::get(315), item->weaponGetAttack(stats[player])); +// stats[player]->type = tmpRace; +// } +// else if ( itemCategory(item) == ARMOR ) +// { +// Monster tmpRace = stats[player]->type; +// if ( stats[player]->type == TROLL +// || stats[player]->type == RAT +// || stats[player]->type == SPIDER +// || stats[player]->type == CREATURE_IMP ) +// { +// // these monsters have 0 bonus from armor, but want the tooltip to say the normal amount. +// stats[player]->type = HUMAN; +// } +// +// if ( item->armorGetAC(stats[player]) >= 0 ) +// { +// color = makeColorRGB(0, 255, 255); +// } +// else +// { +// color = makeColorRGB(255, 0, 0); +// } +// if ( stats[player]->type != tmpRace ) +// { +// color = makeColorRGB(127, 127, 127); // grey out the text if monster doesn't benefit. +// } +// +// ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 4, color, Language::get(316), item->armorGetAC(stats[player])); +// stats[player]->type = tmpRace; +// } +// else if ( itemCategory(item) == SCROLL ) +// { +// color = makeColorRGB(0, 255, 255); +// ttfPrintTextFormattedColor(ttf12, src.x + 4 + TTF12_WIDTH, src.y + 4 + TTF12_HEIGHT * 4, color, "%s%s", Language::get(3862), item->getScrollLabel()); +// } +// } +// } +// if ( !drawHotBarTooltipOnCycle && playerSettings[multiplayer ? 0 : player].hotbar_numkey_quick_add && inputs.bPlayerUsingKeyboardControl(player) ) +// { +// Uint32 swapItem = 0; +// if ( keystatus[SDLK_1] ) +// { +// keystatus[SDLK_1] = 0; +// swapItem = hotbar[0].item; +// hotbar[0].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_2] ) +// { +// keystatus[SDLK_2] = 0; +// swapItem = hotbar[1].item; +// hotbar[1].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_3] ) +// { +// keystatus[SDLK_3] = 0; +// swapItem = hotbar[2].item; +// hotbar[2].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_4] ) +// { +// keystatus[SDLK_4] = 0; +// swapItem = hotbar[3].item; +// hotbar[3].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_5] ) +// { +// keystatus[SDLK_5] = 0; +// swapItem = hotbar[4].item; +// hotbar[4].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_6] ) +// { +// keystatus[SDLK_6] = 0; +// swapItem = hotbar[5].item; +// hotbar[5].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_7] ) +// { +// keystatus[SDLK_7] = 0; +// swapItem = hotbar[6].item; +// hotbar[6].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_8] ) +// { +// keystatus[SDLK_8] = 0; +// swapItem = hotbar[7].item; +// hotbar[7].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_9] ) +// { +// keystatus[SDLK_9] = 0; +// swapItem = hotbar[8].item; +// hotbar[8].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// if ( keystatus[SDLK_0] ) +// { +// keystatus[SDLK_0] = 0; +// swapItem = hotbar[9].item; +// hotbar[9].item = hotbar[num].item; +// hotbar[num].item = swapItem; +// } +// } +// } +// } +// pos.x += hotbar_t.getSlotSize(); +// } +// } +// +// //NOTE: If you change the number of hotbar slots, you *MUST* change this. +// if ( !command && stats[player] && stats[player]->HP > 0 ) +// { +// Item* item = NULL; +// const auto& inventoryUI = players[multiplayer ? 0 : player]->inventoryUI; +// if ( !(!shootmode && playerSettings[multiplayer ? 0 : player].hotbar_numkey_quick_add +// /*&& ( +// (omousex >= inventoryUI.getStartX() +// && omousex <= inventoryUI.getStartX() + inventoryUI.getSizeX() * inventoryUI.getSlotSize() +// && omousey >= inventoryUI.getStartY() +// && omousey <= inventoryUI.getStartY() + inventoryUI.getSizeY() * inventoryUI.getSlotSize() +// ) +// || +// (omousex >= initial_position.x +// && omousex <= initial_position.x + hotbar_t.getSlotSize() * NUM_HOTBAR_SLOTS +// && omousey >= initial_position.y - hotbar_t.getSlotSize() +// && omousey <= initial_position.y +// ) +// )*/ +// ) ) +// { +// // if hotbar_numkey_quick_add is enabled, then the number keys won't do the default equip function +// // skips equipping items if the mouse is in the hotbar or inventory area. otherwise the below code runs. +// if ( inputs.bPlayerUsingKeyboardControl(player) && !StatueManager.activeEditing ) +// { +// if ( keystatus[SDLK_1] ) +// { +// keystatus[SDLK_1] = 0; +// item = uidToItem(hotbar[0].item); +// hotbar_t.current_hotbar = 0; +// } +// if ( keystatus[SDLK_2] ) +// { +// keystatus[SDLK_2] = 0; +// item = uidToItem(hotbar[1].item); +// hotbar_t.current_hotbar = 1; +// } +// if ( keystatus[SDLK_3] ) +// { +// keystatus[SDLK_3] = 0; +// item = uidToItem(hotbar[2].item); +// hotbar_t.current_hotbar = 2; +// } +// if ( keystatus[SDLK_4] ) +// { +// keystatus[SDLK_4] = 0; +// item = uidToItem(hotbar[3].item); +// hotbar_t.current_hotbar = 3; +// } +// if ( keystatus[SDLK_5] ) +// { +// keystatus[SDLK_5] = 0; +// item = uidToItem(hotbar[4].item); +// hotbar_t.current_hotbar = 4; +// } +// if ( keystatus[SDLK_6] ) +// { +// keystatus[SDLK_6] = 0; +// item = uidToItem(hotbar[5].item); +// hotbar_t.current_hotbar = 5; +// } +// if ( keystatus[SDLK_7] ) +// { +// keystatus[SDLK_7] = 0; +// item = uidToItem(hotbar[6].item); +// hotbar_t.current_hotbar = 6; +// } +// if ( keystatus[SDLK_8] ) +// { +// keystatus[SDLK_8] = 0; +// item = uidToItem(hotbar[7].item); +// hotbar_t.current_hotbar = 7; +// } +// if ( keystatus[SDLK_9] ) +// { +// keystatus[SDLK_9] = 0; +// item = uidToItem(hotbar[8].item); +// hotbar_t.current_hotbar = 8; +// } +// if ( keystatus[SDLK_0] ) +// { +// keystatus[SDLK_0] = 0; +// item = uidToItem(hotbar[9].item); +// hotbar_t.current_hotbar = 9; +// } +// } +// if ( players[player]->hotbar.useHotbarFaceMenu +// && !openedChest[player] +// && gui_mode != (GUI_MODE_SHOP) +// && !GenericGUI[player].isGUIOpen() +// && !inputs.getUIInteraction(player)->selectedItem +// && shootmode ) +// { +// Player::Hotbar_t::FaceMenuGroup pressed = Player::Hotbar_t::GROUP_NONE; +// +// for ( int i = 0; i < 3; ++i ) +// { +// int button = SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B + i; +// if ( inputs.bControllerRawInputPressed(player, 301 + button) ) +// { +// if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN) ) +// { +// inputs.controllerClearRawInput(player, 301 + button); +// inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN); +// inputs.controllerClearRawInputRelease(player, 301 + button); +// break; +// } +// +// switch ( button ) +// { +// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B: +// pressed = Player::Hotbar_t::GROUP_RIGHT; +// break; +// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_Y: +// pressed = Player::Hotbar_t::GROUP_MIDDLE; +// break; +// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X: +// pressed = Player::Hotbar_t::GROUP_LEFT; +// break; +// default: +// break; +// } +// +// std::array slotOrder = { 0, 1, 2 }; +// int centerSlot = 1; +// if ( hotbar_t.faceMenuAlternateLayout ) +// { +// slotOrder = { 0, 2, 1 }; +// } +// if ( button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B ) +// { +// centerSlot = 7; +// if ( hotbar_t.faceMenuAlternateLayout ) +// { +// slotOrder = { 7, 6, 8 }; +// } +// else +// { +// slotOrder = { 6, 7, 8 }; +// } +// } +// else if ( button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_Y ) +// { +// centerSlot = 4; +// slotOrder = { 3, 4, 5 }; +// } +// +// if ( hotbar_t.faceMenuAlternateLayout ) +// { +// if ( true ) +// { +// // temp test +// if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ) +// { +// hotbar_t.selectHotbarSlot(slotOrder[0]); +// } +// else if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ) +// { +// hotbar_t.selectHotbarSlot(slotOrder[2]); +// } +// else if ( players[player]->hotbar.faceMenuButtonHeld == Player::Hotbar_t::GROUP_NONE ) +// { +// hotbar_t.selectHotbarSlot(slotOrder[1]); +// } +// } +// else +// { +// if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ) +// { +// hotbar_t.selectHotbarSlot(std::max(centerSlot - 1, hotbar_t.current_hotbar - 1)); +// inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER); +// } +// else if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ) +// { +// hotbar_t.selectHotbarSlot(std::min(centerSlot + 1, hotbar_t.current_hotbar + 1)); +// inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER); +// } +// else if ( players[player]->hotbar.faceMenuButtonHeld == Player::Hotbar_t::GROUP_NONE ) +// { +// hotbar_t.selectHotbarSlot(slotOrder[1]); +// } +// } +// } +// else +// { +// if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ) +// { +// hotbar_t.selectHotbarSlot(std::max(centerSlot - 1, hotbar_t.current_hotbar - 1)); +// inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER); +// } +// else if ( inputs.bControllerRawInputPressed(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ) +// { +// hotbar_t.selectHotbarSlot(std::min(centerSlot + 1, hotbar_t.current_hotbar + 1)); +// inputs.controllerClearRawInput(player, 301 + SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER); +// } +// else if ( players[player]->hotbar.faceMenuButtonHeld == Player::Hotbar_t::GROUP_NONE ) +// { +// hotbar_t.selectHotbarSlot(slotOrder[1]); +// } +// } +// break; +// } +// else if ( inputs.bControllerRawInputReleased(player, 301 + button) ) +// { +// item = uidToItem(players[player]->hotbar.slots()[hotbar_t.current_hotbar].item); +// inputs.controllerClearRawInputRelease(player, 301 + button); +// break; +// } +// } +// +// players[player]->hotbar.faceMenuButtonHeld = pressed; +// +// if ( pressed != Player::Hotbar_t::GROUP_NONE +// && players[player]->hotbar.faceMenuQuickCastEnabled && item && itemCategory(item) == SPELL_CAT ) +// { +// spell_t* spell = getSpellFromItem(player, item, false); +// if ( spell && players[player]->magic.selectedSpell() == spell ) +// { +// players[player]->hotbar.faceMenuQuickCast = true; +// } +// } +// } +// } +// +// //Moving the cursor changes the currently selected hotbar slot. +// if ( (mousexrel || mouseyrel) && !shootmode ) +// { +// pos.x = initial_position.x; +// pos.y = initial_position.y - hotbar_t.getSlotSize(); +// for ( c = 0; c < NUM_HOTBAR_SLOTS; ++c, pos.x += hotbar_t.getSlotSize() ) +// { +// if ( players[player]->hotbar.useHotbarFaceMenu ) +// { +// pos.x = players[player]->hotbar.faceButtonPositions[c].x; +// pos.y = players[player]->hotbar.faceButtonPositions[c].y; +// } +// if ( mouseInBoundsRealtimeCoords(player, pos.x, pos.x + hotbar_t.getSlotSize(), pos.y, pos.y + hotbar_t.getSlotSize()) ) +// { +// players[player]->hotbar.selectHotbarSlot(c); +// } +// } +// } +// +// bool bumper_moved = false; +// +// Input& input = Input::inputs[player]; +// +// if ( !players[player]->hotbar.useHotbarFaceMenu && input.consumeBinaryToggle("Hotbar Right") +// && players[player]->bControlEnabled && !gamePaused && !players[player]->usingCommand() ) +// { +// if ( shootmode && !inputs.getUIInteraction(player)->itemMenuOpen && !openedChest[player] +// && gui_mode != (GUI_MODE_SHOP) && !players[player]->bookGUI.bBookOpen +// && !GenericGUI[player].isGUIOpen() ) +// { +// bumper_moved = true; +// hotbar_t.hotbarTooltipLastGameTick = ticks; +// players[player]->hotbar.selectHotbarSlot(players[player]->hotbar.current_hotbar + 1); +// } +// else +// { +// hotbar_t.hotbarTooltipLastGameTick = 0; +// } +// } +// if ( !players[player]->hotbar.useHotbarFaceMenu && input.consumeBinaryToggle("Hotbar Left") +// && players[player]->bControlEnabled && !gamePaused && !players[player]->usingCommand() ) +// { +// if ( shootmode && !inputs.getUIInteraction(player)->itemMenuOpen && !openedChest[player] +// && gui_mode != (GUI_MODE_SHOP) && !players[player]->bookGUI.bBookOpen +// && !GenericGUI[player].isGUIOpen() ) +// { +// bumper_moved = true; +// hotbar_t.hotbarTooltipLastGameTick = ticks; +// hotbar_t.selectHotbarSlot(hotbar_t.current_hotbar - 1); +// } +// else +// { +// hotbar_t.hotbarTooltipLastGameTick = 0; +// } +// } +// +// if ( bumper_moved && !inputs.getUIInteraction(player)->itemMenuOpen +// && !openedChest[player] && gui_mode != (GUI_MODE_SHOP) +// && !players[player]->bookGUI.bBookOpen +// && !GenericGUI[player].isGUIOpen() ) +// { +// warpMouseToSelectedHotbarSlot(player); +// } +// +// if ( !inputs.getUIInteraction(player)->itemMenuOpen && !inputs.getUIInteraction(player)->selectedItem && !openedChest[player] && gui_mode != (GUI_MODE_SHOP) ) +// { +// if ( shootmode && input.consumeBinaryToggle("Hotbar Up / Select") +// && (!hotbar_t.useHotbarFaceMenu || (hotbar_t.useHotbarFaceMenu && !inputs.hasController(player))) +// && players[player]->bControlEnabled && !gamePaused +// && !players[player]->usingCommand() +// && !openedChest[player] && gui_mode != (GUI_MODE_SHOP) +// && !players[player]->bookGUI.bBookOpen +// && !GenericGUI[player].isGUIOpen() ) +// { +// //Show a tooltip +// hotbar_t.hotbarTooltipLastGameTick = std::max(ticks - TICKS_PER_SECOND, ticks - hotbar_t.hotbarTooltipLastGameTick); +// +// //Activate a hotbar slot if in-game. +// item = uidToItem(hotbar[hotbar_t.current_hotbar].item); +// } +// +// // We don't have a hotbar clear binding, but if we need one, feel free to add it +// // in MainMenu.cpp +// +// /*if ( !shootmode && inputs.bControllerInputPressed(player, INJOY_MENU_HOTBAR_CLEAR) && !players[player]->bookGUI.bBookOpen ) //TODO: Don't activate if any of the previous if statement's conditions are true? +// { +// //Clear a hotbar slot if in-inventory. +// inputs.controllerClearInput(player, INJOY_MENU_HOTBAR_CLEAR); +// +// hotbar[hotbar_t.current_hotbar].item = 0; +// }*/ +// +// pos.x = initial_position.x + (hotbar_t.current_hotbar * hotbar_t.getSlotSize()); +// pos.y = initial_position.y - hotbar_t.getSlotSize(); +// //if ( !shootmode && !players[player]->bookGUI.bBookOpen && !openedChest[player] && inputs.bControllerInputPressed(player, INJOY_MENU_DROP_ITEM) +// // && mouseInBounds(player, pos.x, pos.x + hotbar_img->w * uiscale_hotbar, pos.y, pos.y + hotbar_img->h * uiscale_hotbar) ) +// //{ +// // //Drop item if this hotbar is currently active & the player pressed the cancel button on the gamepad (typically "b"). +// // inputs.controllerClearInput(player, INJOY_MENU_DROP_ITEM); +// // Item* itemToDrop = uidToItem(hotbar[hotbar_t.current_hotbar].item); +// // if ( itemToDrop ) +// // { +// // dropItem(itemToDrop, player); +// // } +// //} +// } +// +// if ( item ) +// { +// bool badpotion = false; +// bool learnedSpell = false; +// if ( itemCategory(item) == POTION ) +// { +// badpotion = isPotionBad(*item); +// } +// +// if ( itemCategory(item) == SPELLBOOK && (item->identified || itemIsEquipped(item, player)) ) +// { +// // equipped spellbook will unequip on use. +// learnedSpell = (playerLearnedSpellbook(player, item) || itemIsEquipped(item, player)); +// } +// +// if ( (keystatus[SDLK_LALT] || keystatus[SDLK_RALT]) +// && (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK || item->type == FOOD_CREAMPIE) ) +// { +// badpotion = true; +// learnedSpell = true; +// } +// +// if ( !learnedSpell && item->identified +// && itemCategory(item) == SPELLBOOK && players[player] && players[player]->entity ) +// { +// learnedSpell = true; // let's always equip/unequip spellbooks from the hotbar? +// spell_t* currentSpell = getSpellFromID(getSpellIDFromSpellbook(item->type)); +// if ( currentSpell && stats[player] ) +// { +// int skillLVL = stats[player]->getModifiedProficiency(PRO_LEGACY_MAGIC) + statGetINT(stats[player], players[player]->entity); +// if ( stats[player]->getModifiedProficiency(PRO_LEGACY_MAGIC) >= 100 ) +// { +// skillLVL = 100; +// } +// if ( skillLVL >= currentSpell->difficulty ) +// { +// // can learn spell, try that instead. +// learnedSpell = false; +// } +// } +// } +// +// if ( itemCategory(item) == SPELLBOOK && stats[player] ) +// { +// if ( stats[player]->type == GOBLIN || stats[player]->type == CREATURE_IMP +// || (stats[player]->playerRace == RACE_GOBLIN && stats[player]->stat_appearance == 0) ) +// { +// learnedSpell = true; // goblinos can't learn spells but always equip books. +// } +// } +// +// bool disableItemUsage = false; +// if ( players[player] && players[player]->entity ) +// { +// if ( players[player]->entity->effectShapeshift != NOTHING ) +// { +// if ( !item->usableWhileShapeshifted(stats[player]) ) +// { +// disableItemUsage = true; +// } +// } +// else +// { +// if ( itemCategory(item) == SPELL_CAT && item->appearance >= 1000 ) +// { +// if ( canUseShapeshiftSpellInCurrentForm(player, *item) != 1 ) +// { +// disableItemUsage = true; +// } +// } +// } +// } +// if ( client_classes[player] == CLASS_SHAMAN ) +// { +// if ( item->type == SPELL_ITEM && !(playerUnlockedShamanSpell(player, item)) ) +// { +// disableItemUsage = true; +// } +// } +// +// if ( !disableItemUsage ) +// { +// if ( !badpotion && !learnedSpell ) +// { +// if ( !(isItemEquippableInShieldSlot(item) && cast_animation[player].active_spellbook) ) +// { +// if ( stats[player] && stats[player]->type == AUTOMATON +// && (item->type == TOOL_METAL_SCRAP || item->type == TOOL_MAGIC_SCRAP) ) +// { +// // consume item +// if ( multiplayer == CLIENT ) +// { +// strcpy((char*)net_packet->data, "FODA"); +// SDLNet_Write32((Uint32)item->type, &net_packet->data[4]); +// SDLNet_Write32((Uint32)item->status, &net_packet->data[8]); +// SDLNet_Write32((Uint32)item->beatitude, &net_packet->data[12]); +// SDLNet_Write32((Uint32)item->count, &net_packet->data[16]); +// SDLNet_Write32((Uint32)item->appearance, &net_packet->data[20]); +// net_packet->data[24] = item->identified; +// net_packet->data[25] = player; +// net_packet->address.host = net_server.host; +// net_packet->address.port = net_server.port; +// net_packet->len = 26; +// sendPacketSafe(net_sock, -1, net_packet, 0); +// } +// item_FoodAutomaton(item, player); +// } +// else +// { +// useItem(item, player); +// } +// } +// } +// else +// { +// playerTryEquipItemAndUpdateServer(player, item, false); +// } +// } +// else +// { +// if ( client_classes[player] == CLASS_SHAMAN && item->type == SPELL_ITEM ) +// { +// messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3488)); // unable to use with current level. +// } +// else +// { +// messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3432)); // unable to use in current form message. +// } +// playSoundPlayer(player, 90, 64); +// } +// } +// } +// +// FollowerMenu[player].drawFollowerMenu(); +// +// //// stat increase icons +// //pos.w = 64; +// //pos.h = 64; +// //pos.x = players[player]->camera_x2() - pos.w * 3 - 9; +// //pos.y = players[player]->characterSheet.skillsSheetBox.h + (32 + pos.h * 2 + 3); // 131px from end of prof window. +// +// //if ( (!shootmode || players[player]->characterSheet.lock_right_sidebar) && players[player]->characterSheet.proficienciesPage == 1 +// // && pos.y < (players[player]->characterSheet.partySheetBox.y + players[player]->characterSheet.partySheetBox.h + 16) ) +// //{ +// // pos.y = players[player]->characterSheet.partySheetBox.y + players[player]->characterSheet.partySheetBox.h + 16; +// //} +// +// //if ( splitscreen ) +// //{ +// // // todo - adjust position. +// // pos.w = 48; +// // pos.h = 48; +// // pos.x = players[player]->camera_x2() - pos.w * 3 - 9; +// // pos.y = players[player]->characterSheet.skillsSheetBox.h + (16 + pos.h * 2 + 3); +// //} +// //else +// //{ +// // if ( pos.y + pos.h > (players[player]->camera_y2() - minimaps[player].y - minimaps[player].h) ) // check if overlapping minimap +// // { +// // pos.y = (players[player]->camera_y2() - minimaps[player].y - minimaps[player].h) - (64 + 3); // align above minimap +// // } +// //} +// +// SDL_Surface *tmp_bmp = NULL; +// +// for ( i = 0; i < NUMSTATS; i++ ) +// { +// if ( stats[player] && stats[player]->PLAYER_LVL_STAT_TIMER[i] > 0 && ((ticks % 50) - (ticks % 10)) ) +// { +// stats[player]->PLAYER_LVL_STAT_TIMER[i]--; +// +// //switch ( i ) +// //{ +// // // prepare the stat image. +// // case STAT_STR: +// // tmp_bmp = str_bmp64u; +// // break; +// // case STAT_DEX: +// // tmp_bmp = dex_bmp64u; +// // break; +// // case STAT_CON: +// // tmp_bmp = con_bmp64u; +// // break; +// // case STAT_INT: +// // tmp_bmp = int_bmp64u; +// // break; +// // case STAT_PER: +// // tmp_bmp = per_bmp64u; +// // break; +// // case STAT_CHR: +// // tmp_bmp = chr_bmp64u; +// // break; +// // default: +// // break; +// //} +// drawImageScaled(tmp_bmp, NULL, &pos); +// if ( stats[player]->PLAYER_LVL_STAT_TIMER[i + NUMSTATS] > 0 ) +// { +// // bonus stat acheived, draw additional stat icon above. +// pos.y -= 64 + 3; +// drawImageScaled(tmp_bmp, NULL, &pos); +// pos.y += 64 + 3; +// stats[player]->PLAYER_LVL_STAT_TIMER[i + NUMSTATS]--; +// } +// +// pos.x += pos.h + 3; +// } +// } +//} + void drawStatusNew(const int player) { Sint32 x, y, z, c, i; @@ -2336,6 +2136,10 @@ void drawStatusNew(const int player) { hotbar_t.magicBoomerangHotbarSlot = num; } + if ( item->type == TOOL_DUCK ) + { + hotbar_t.magicDuckHotbarSlot = num; + } bool used = false; bool disableItemUsage = false; @@ -2355,7 +2159,7 @@ void drawStatusNew(const int player) } } - if ( stats[player] && stats[player]->HP > 0 ) + if ( stats[player] && stats[player]->HP > 0 && !players[player]->ghost.isActive() ) { Item*& selectedItem = inputs.getUIInteraction(player)->selectedItem; @@ -2441,15 +2245,36 @@ void drawStatusNew(const int player) if ( (keystatus[SDLK_LSHIFT] || keystatus[SDLK_RSHIFT]) ) //TODO: selected shop slot, identify, remove curse? { // auto-appraise the item + int prevAppraisedManual = players[player]->inventoryUI.appraisal.manual_appraised_item; players[player]->inventoryUI.appraisal.appraiseItem(item); + if ( players[player]->inventoryUI.appraisal.current_item == item->uid ) + { + if ( prevAppraisedManual == item->uid ) + { + players[player]->inventoryUI.appraisal.manual_appraised_item = 0; + } + else + { + players[player]->inventoryUI.appraisal.manual_appraised_item = item->uid; + } + } Input::inputs[player].consumeBinaryToggle("MenuRightClick"); } - else if ( !disableItemUsage && (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK || item->type == FOOD_CREAMPIE) && + else if ( !disableItemUsage && (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK + || itemCategory(item) == SPELL_CAT + || item->type == FOOD_CREAMPIE) && (keystatus[SDLK_LALT] || keystatus[SDLK_RALT]) ) { Input::inputs[player].consumeBinaryToggle("MenuRightClick"); - // force equip potion/spellbook - playerTryEquipItemAndUpdateServer(player, item, false); + if ( itemCategory(item) == SPELL_CAT ) + { + players[player]->inventoryUI.activateItemContextMenuOption(item, ItemContextMenuPrompts::PROMPT_SPELL_QUICKCAST); + } + else + { + // force equip potion/spellbook + playerTryEquipItemAndUpdateServer(player, item, true); + } } else { @@ -2899,6 +2724,7 @@ void drawStatusNew(const int player) //NOTE: If you change the number of hotbar slots, you *MUST* change this. if ( !players[player]->usingCommand() && stats[player] && stats[player]->HP > 0 + && !players[player]->ghost.isActive() && players[player]->bControlEnabled && !gamePaused ) { Item* item = NULL; @@ -2915,63 +2741,90 @@ void drawStatusNew(const int player) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 1"); item = uidToItem(hotbar[0].item); - hotbar_t.current_hotbar = 0; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 0; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 2") ) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 2"); item = uidToItem(hotbar[1].item); - hotbar_t.current_hotbar = 1; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 1; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 3") ) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 3"); item = uidToItem(hotbar[2].item); - hotbar_t.current_hotbar = 2; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 2; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 4") ) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 4"); item = uidToItem(hotbar[3].item); - hotbar_t.current_hotbar = 3; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 3; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 5") ) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 5"); item = uidToItem(hotbar[4].item); - hotbar_t.current_hotbar = 4; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 4; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 6") ) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 6"); item = uidToItem(hotbar[5].item); - hotbar_t.current_hotbar = 5; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 5; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 7") ) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 7"); item = uidToItem(hotbar[6].item); - hotbar_t.current_hotbar = 6; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 6; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 8") ) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 8"); item = uidToItem(hotbar[7].item); - hotbar_t.current_hotbar = 7; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 7; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 9") ) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 9"); item = uidToItem(hotbar[8].item); - hotbar_t.current_hotbar = 8; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 8; + } pressed = true; } if ( Input::inputs[player].binaryToggle("Hotbar Slot 10") @@ -2980,7 +2833,10 @@ void drawStatusNew(const int player) { Input::inputs[player].consumeBinaryToggle("Hotbar Slot 10"); item = uidToItem(hotbar[9].item); - hotbar_t.current_hotbar = 9; + if ( playerSettings[multiplayer ? 0 : player].hotbar_numkey_change_slot ) + { + hotbar_t.current_hotbar = 9; + } pressed = true; } @@ -3399,12 +3255,14 @@ void drawStatusNew(const int player) learnedSpell = (playerLearnedSpellbook(player, item) || itemIsEquipped(item, player)); } - if ( ((keystatus[SDLK_LALT] || keystatus[SDLK_RALT]) - && (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK)) - || item->type == FOOD_CREAMPIE ) + bool altUse = false; + if ( (keystatus[SDLK_LALT] || keystatus[SDLK_RALT]) + && (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK || itemCategory(item) == SPELL_CAT + || item->type == FOOD_CREAMPIE) ) { badpotion = true; learnedSpell = true; + altUse = true; } if ( !learnedSpell && item->identified @@ -3414,8 +3272,8 @@ void drawStatusNew(const int player) spell_t* currentSpell = getSpellFromID(getSpellIDFromSpellbook(item->type)); if ( currentSpell && stats[player] ) { - int skillLVL = stats[player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[player], players[player]->entity); - if ( stats[player]->getModifiedProficiency(PRO_MAGIC) >= 100 ) + int skillLVL = stats[player]->getModifiedProficiency(currentSpell->skillID) + statGetINT(stats[player], players[player]->entity); + if ( stats[player]->getModifiedProficiency(currentSpell->skillID) >= 100 ) { skillLVL = 100; } @@ -3467,7 +3325,11 @@ void drawStatusNew(const int player) if ( !disableItemUsage ) { - if ( !badpotion && !learnedSpell ) + if ( altUse && itemCategory(item) == SPELL_CAT ) + { + players[player]->inventoryUI.activateItemContextMenuOption(item, ItemContextMenuPrompts::PROMPT_SPELL_QUICKCAST); + } + else if ( !badpotion && !learnedSpell ) { if ( !(isItemEquippableInShieldSlot(item) && cast_animation[player].active_spellbook) ) { @@ -3494,7 +3356,7 @@ void drawStatusNew(const int player) } else { - if ( itemCategory(item) == SPELLBOOK ) + if ( itemCategory(item) == SPELLBOOK || itemCategory(item) == TOME_SPELL ) { players[player]->magic.spellbookUidFromHotbarSlot = item->uid; } @@ -3505,7 +3367,7 @@ void drawStatusNew(const int player) } else { - playerTryEquipItemAndUpdateServer(player, item, false); + playerTryEquipItemAndUpdateServer(player, item, true); } } else diff --git a/src/interface/identify_and_appraise.cpp b/src/interface/identify_and_appraise.cpp index 0f83b425a..e12af7f45 100644 --- a/src/interface/identify_and_appraise.cpp +++ b/src/interface/identify_and_appraise.cpp @@ -369,7 +369,60 @@ bool Player::Inventory_t::Appraisal_t::appraisalPossible(Item* item) if ( stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) < 100 ) { - if ( item->type == GEM_GLASS ) + int value = item->type == GEM_GLASS ? 1000 : item->getGoldValue(); + + int skillLVL = stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) + + statGetPER(stats[player.playernum], player.entity) * Player::Inventory_t::Appraisal_t::perStatMult; + + for ( auto& table : appraisal_tables ) + { + if ( skillLVL >= table.skillLVL ) + { + return value <= table.goldValueLimit; + } + } + /*if ( skillLVL >= 50 ) + { + return true; + } + else if ( skillLVL >= 40 ) + { + return value <= 1500; + } + else if ( skillLVL >= 35 ) + { + return value <= 425; + } + else if ( skillLVL >= 30 ) + { + return value <= 320; + } + else if ( skillLVL >= 25 ) + { + return value <= 260; + } + else if ( skillLVL >= 20 ) + { + return value <= 200; + } + else if ( skillLVL >= 15 ) + { + return value <= 150; + } + else if ( skillLVL >= 10 ) + { + return value <= 100; + } + else if ( skillLVL >= 5 ) + { + return value <= 65; + } + else if ( skillLVL >= 0 ) + { + return value <= 30; + }*/ + + /*if ( item->type == GEM_GLASS ) { if ( (stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) + statGetPER(stats[player.playernum], player.entity) * 5) >= 100 ) @@ -384,7 +437,7 @@ bool Player::Inventory_t::Appraisal_t::appraisalPossible(Item* item) { return true; } - } + }*/ } else { @@ -431,7 +484,7 @@ void Player::Inventory_t::Appraisal_t::appraiseItem(Item* item) //Appraising. //If appraisal skill >= LEGENDARY, then auto-complete appraisal. Else, do the normal routine. - if ( stats[player.playernum]->getProficiency(PRO_APPRAISAL) >= CAPSTONE_UNLOCK_LEVEL[PRO_APPRAISAL] ) + /*if ( stats[player.playernum]->getProficiency(PRO_APPRAISAL) >= CAPSTONE_UNLOCK_LEVEL[PRO_APPRAISAL] ) { if ( !item->identified ) { @@ -445,12 +498,13 @@ void Player::Inventory_t::Appraisal_t::appraiseItem(Item* item) timer = 0; current_item = 0; } + Item::onItemIdentified(player.playernum, item); if ( item->type == GEM_GLASS ) { steamStatisticUpdate(STEAM_STAT_RHINESTONE_COWBOY, STEAM_STAT_INT, 1); } } - else + else*/ { Item* oldItemToUpdate = nullptr; bool doMessage = true; @@ -475,10 +529,10 @@ void Player::Inventory_t::Appraisal_t::appraiseItem(Item* item) //Once the timer hits zero, roll to see if the item is identified. //If it is identified, identify it and print out a message for the player. timer = getAppraisalTime(item); - if ( stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) >= CAPSTONE_UNLOCK_LEVEL[PRO_APPRAISAL] ) - { - timer = 1; // our modified proficiency is legendary, so make a really short timer to almost be instant - } + //if ( stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) >= CAPSTONE_UNLOCK_LEVEL[PRO_APPRAISAL] ) + //{ + // timer = 1; // our modified proficiency is legendary, so make a really short timer to almost be instant + //} timermax = timer; if ( oldItemToUpdate && current_item != item->uid ) { @@ -508,76 +562,197 @@ void Player::Inventory_t::Appraisal_t::appraiseItem(Item* item) } } current_item = item->uid; + if ( appraisalProgressionItems.find(current_item) != appraisalProgressionItems.end() ) + { + timer = std::min(appraisalProgressionItems[current_item], timer); + } + if ( doMessage ) { animAppraisal = PI; animStartTick = ticks; } + + manual_appraised_item = 0; } old_item = 0; } +std::vector> Player::Inventory_t::Appraisal_t::appraisal_time_points; +std::vector Player::Inventory_t::Appraisal_t::appraisal_tables; +int Player::Inventory_t::Appraisal_t::fastTimeAppraisal = 10 * TICKS_PER_SECOND; +int Player::Inventory_t::Appraisal_t::perStatMult = 3; +void Player::Inventory_t::Appraisal_t::readFromFile() +{ + std::string filename = "data/appraisal_tables.json"; + if ( !PHYSFS_getRealDir(filename.c_str()) ) + { + printlog("[JSON]: Error: Could not locate json file %s", filename.c_str()); + return; + } + + std::string inputPath = PHYSFS_getRealDir(filename.c_str()); + inputPath.append(PHYSFS_getDirSeparator()); + inputPath.append(filename.c_str()); + + File* fp = FileIO::open(inputPath.c_str(), "rb"); + if ( !fp ) + { + printlog("[JSON]: Error: Could not locate json file %s", inputPath.c_str()); + return; + } + + static char buf[32000]; + int count = fp->read(buf, sizeof(buf[0]), sizeof(buf) - 1); + buf[count] = '\0'; + rapidjson::StringStream is(buf); + FileIO::close(fp); + + rapidjson::Document d; + d.ParseStream(is); + if ( !d.IsObject() ) + { + return; + } + if ( !d.HasMember("version") + || !d.HasMember("appraisal_times") + || !d.HasMember("appraisal_tables") + || !d.HasMember("fast_time_seconds") + || !d.HasMember("per_stat_mult") ) + { + printlog("[JSON]: Error: No 'version' value in json file, or JSON syntax incorrect! %s", inputPath.c_str()); + return; + } + + appraisal_time_points.clear(); + appraisal_tables.clear(); + + fastTimeAppraisal = d["fast_time_seconds"].GetInt() * TICKS_PER_SECOND; + perStatMult = d["per_stat_mult"].GetInt(); + + for ( auto itr = d["appraisal_times"].Begin(); itr != d["appraisal_times"].End(); ++itr ) + { + int value = (*itr)["value"].GetInt(); + int slow_time = (*itr)["slow_time_seconds"].GetInt() * TICKS_PER_SECOND; + + appraisal_time_points.push_back(std::make_pair(value, slow_time)); + } + + for ( auto itr = d["appraisal_tables"].Begin(); itr != d["appraisal_tables"].End(); ++itr ) + { + int skill = (*itr)["skill"].GetInt(); + int gold_value_limit = (*itr)["gold_value_limit"].GetInt(); + int fast_time_gold = (*itr)["fast_time_gold"].GetInt(); + + appraisal_tables.push_back(AppraisalBreakpoint_t()); + auto& table = appraisal_tables.back(); + + table.skillLVL = skill; + table.goldValueLimit = gold_value_limit; + table.fastTimeGold = fast_time_gold; + } +} + int Player::Inventory_t::Appraisal_t::getAppraisalTime(Item* item) { - int appraisal_time; + int appraisal_time = 0; - if ( item->type != GEM_GLASS ) + //if ( item->type != GEM_GLASS ) { - appraisal_time = (items[item->type].value * 60) / (stats[this->player.playernum]->getModifiedProficiency(PRO_APPRAISAL) + 1); // time in ticks until item is appraised - if ( stats[player.playernum] && stats[player.playernum]->mask && stats[player.playernum]->mask->type == MONOCLE ) + int skillLVL = stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) + + statGetPER(stats[player.playernum], player.entity) * Player::Inventory_t::Appraisal_t::perStatMult; + int value = item->type == GEM_GLASS ? 1000 : item->getGoldValue(); + + bool fast_time = false; + + for ( auto& table : appraisal_tables ) { - real_t mult = 1.0; - if ( stats[player.playernum]->mask->beatitude == 0 ) + if ( skillLVL >= table.skillLVL ) { - mult = .5; + fast_time = value <= table.fastTimeGold; + break; } - else if ( stats[player.playernum]->mask->beatitude > 0 || shouldInvertEquipmentBeatitude(stats[player.playernum]) ) - { - mult = .25; - } - else if ( stats[player.playernum]->mask->beatitude < 0 ) - { - mult = 2.0; - } - appraisal_time *= mult; } - int playerCount = 0; - for ( int i = 0; i < MAXPLAYERS; ++i ) + + appraisal_time = Player::Inventory_t::Appraisal_t::fastTimeAppraisal; + if ( !fast_time ) { - if ( !client_disconnected[i] ) + for ( auto& pair : appraisal_time_points ) { - ++playerCount; + if ( value >= pair.first ) + { + appraisal_time = pair.second; + break; + } } } - if ( playerCount == 3 ) + + Category cat = itemCategory(item); + if ( cat == FOOD || cat == SCROLL || cat == POTION ) { - appraisal_time /= 1.25; + real_t ratio = std::max(0.2, 1.0 + (-skillLVL) / 100.0); + appraisal_time = std::max((real_t)Player::Inventory_t::Appraisal_t::fastTimeAppraisal * ratio, appraisal_time * ratio); + appraisal_time = std::max(2 * TICKS_PER_SECOND, appraisal_time); } - else if ( playerCount == 4 ) + else if ( skillLVL >= 50 ) { - appraisal_time /= 1.5; + real_t ratio = std::max(0.2, 0.5 + (100 - skillLVL) / 100.0); + appraisal_time = std::max((real_t)Player::Inventory_t::Appraisal_t::fastTimeAppraisal * ratio, appraisal_time * ratio); + appraisal_time = std::max(2 * TICKS_PER_SECOND, appraisal_time); } - //messagePlayer(clientnum, "time: %d", appraisal_time); - } - else - { - appraisal_time = (1000 * 60) / (stats[this->player.playernum]->getModifiedProficiency(PRO_APPRAISAL) + 1); // time in ticks until item is appraised+- + /*if ( fast_time ) + { + appraisal_time = 10; + } + else if ( value > 3000 ) + { + appraisal_time = 12 * 60; + } + else if ( value > 1000 ) + { + appraisal_time = 6 * 60; + } + else if ( value > 500 ) + { + appraisal_time = 3 * 60; + } + else if ( value > 250 ) + { + appraisal_time = 2 * 60; + } + else if ( value > 100 ) + { + appraisal_time = 1 * 60; + } + else if ( value > 50 ) + { + appraisal_time = 30; + } + else + { + appraisal_time = 15; + }*/ + + //appraisal_time = (items[item->type].value * 60) / (stats[this->player.playernum]->getModifiedProficiency(PRO_APPRAISAL) + 1); // time in ticks until item is appraised if ( stats[player.playernum] && stats[player.playernum]->mask && stats[player.playernum]->mask->type == MONOCLE ) { - real_t mult = 1.0; - if ( stats[player.playernum]->mask->beatitude == 0 ) - { - mult = .5; - } - else if ( stats[player.playernum]->mask->beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player.playernum]) ) - { - mult = .25; - } - else if ( stats[player.playernum]->mask->beatitude < 0 ) + if ( cat == GEM ) { - mult = 2.0; + real_t mult = 1.0; + if ( stats[player.playernum]->mask->beatitude == 0 ) + { + mult = .5; + } + else if ( stats[player.playernum]->mask->beatitude > 0 || shouldInvertEquipmentBeatitude(stats[player.playernum]) ) + { + mult = .25; + } + else if ( stats[player.playernum]->mask->beatitude < 0 ) + { + mult = 2.0; + } + appraisal_time *= mult; } - appraisal_time *= mult; } int playerCount = 0; for ( int i = 0; i < MAXPLAYERS; ++i ) @@ -589,13 +764,55 @@ int Player::Inventory_t::Appraisal_t::getAppraisalTime(Item* item) } if ( playerCount == 3 ) { - appraisal_time /= 1.15; + appraisal_time /= 1.25; } else if ( playerCount == 4 ) { - appraisal_time /= 1.25; + appraisal_time /= 1.5; } + if ( stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) >= SKILL_LEVEL_LEGENDARY ) + { + appraisal_time *= 0.75; + } + //messagePlayer(clientnum, "time: %d", appraisal_time); } + //else + //{ + // appraisal_time = (1000 * 60) / (stats[this->player.playernum]->getModifiedProficiency(PRO_APPRAISAL) + 1); // time in ticks until item is appraised+- + // if ( stats[player.playernum] && stats[player.playernum]->mask && stats[player.playernum]->mask->type == MONOCLE ) + // { + // real_t mult = 1.0; + // if ( stats[player.playernum]->mask->beatitude == 0 ) + // { + // mult = .5; + // } + // else if ( stats[player.playernum]->mask->beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player.playernum]) ) + // { + // mult = .25; + // } + // else if ( stats[player.playernum]->mask->beatitude < 0 ) + // { + // mult = 2.0; + // } + // appraisal_time *= mult; + // } + // int playerCount = 0; + // for ( int i = 0; i < MAXPLAYERS; ++i ) + // { + // if ( !client_disconnected[i] ) + // { + // ++playerCount; + // } + // } + // if ( playerCount == 3 ) + // { + // appraisal_time /= 1.15; + // } + // else if ( playerCount == 4 ) + // { + // appraisal_time /= 1.25; + // } + //} appraisal_time = std::min(std::max(1, appraisal_time), 36000); return appraisal_time; } diff --git a/src/interface/interface.cpp b/src/interface/interface.cpp index c5f9c091e..5d1f22791 100644 --- a/src/interface/interface.cpp +++ b/src/interface/interface.cpp @@ -94,7 +94,7 @@ list_t chestInv[MAXPLAYERS]; //SDL_Surface* attributesrightunclicked_bmp = NULL; //SDL_Surface* inventory_bmp = NULL, *inventoryoption_bmp = NULL, *inventoryoptionChest_bmp = NULL, *equipped_bmp = NULL; //SDL_Surface* itembroken_bmp = nullptr; -//SDL_Surface *category_bmp[NUMCATEGORIES]; +//SDL_Surface *category_bmp[Category::CATEGORY_MAX]; //SDL_Surface* shopkeeper_bmp = NULL; //SDL_Surface* shopkeeper2_bmp = NULL; //SDL_Surface* damage_bmp = NULL; @@ -534,7 +534,7 @@ void freeInterfaceResources() //{ // SDL_FreeSurface(damage_bmp); //} - //for( c=0; cgetAbsoluteSize(); + pos.x -= player.camera_virtualx1(); + pos.y -= player.camera_virtualy1(); + inventoryUI.updateSelectedSlotAnimation(pos.x, pos.y, + inventoryUI.getSlotSize(), inventoryUI.getSlotSize(), moveCursorInstantly); + } + } + return true; + } else if ( activeModule == MODULE_ASSISTSHRINE ) { auto& assistShrineGUI = GenericGUI[player.playernum].assistShrineGUI; @@ -1279,6 +1298,7 @@ void Player::GUI_t::activateModule(Player::GUI_t::GUIModules module) || oldModule == MODULE_CHEST || oldModule == MODULE_SHOP || oldModule == MODULE_ALCHEMY + || oldModule == MODULE_MAILBOX || oldModule == MODULE_TINKERING || oldModule == MODULE_FEATHER || oldModule == MODULE_ASSISTSHRINE) @@ -1288,6 +1308,7 @@ void Player::GUI_t::activateModule(Player::GUI_t::GUIModules module) || activeModule == MODULE_CHEST || activeModule == MODULE_SHOP || activeModule == MODULE_ALCHEMY + || activeModule == MODULE_MAILBOX || activeModule == MODULE_TINKERING || activeModule == MODULE_FEATHER || activeModule == MODULE_ASSISTSHRINE) @@ -1310,6 +1331,7 @@ void Player::GUI_t::activateModule(Player::GUI_t::GUIModules module) || activeModule == MODULE_CHEST || activeModule == MODULE_SHOP || activeModule == MODULE_ALCHEMY + || activeModule == MODULE_MAILBOX || activeModule == MODULE_TINKERING || activeModule == MODULE_FEATHER || activeModule == MODULE_ASSISTSHRINE) @@ -1319,6 +1341,7 @@ void Player::GUI_t::activateModule(Player::GUI_t::GUIModules module) || oldModule == MODULE_CHEST || oldModule == MODULE_SHOP || oldModule == MODULE_ALCHEMY + || oldModule == MODULE_MAILBOX || oldModule == MODULE_TINKERING || oldModule == MODULE_FEATHER || oldModule == MODULE_ASSISTSHRINE)) @@ -2164,6 +2187,17 @@ void FollowerRadialMenu::drawFollowerMenu() { followerFrame->setDisabled(true); } + + if ( followerToCommand ) + { + if ( followerToCommand->monsterAllyIndex < 0 && followerToCommand->getStats() && + (!followerToCommand->getStats()->getEffectActive(EFF_COMMAND) + || followerToCommand->getStats()->getEffectActive(EFF_COMMAND) - 1 != gui_player) ) + { + selectMoveTo = false; + closeFollowerMenuGUI(); + } + } return; } @@ -2257,7 +2291,7 @@ void FollowerRadialMenu::drawFollowerMenu() if ( players[gui_player] && players[gui_player]->entity && followerToCommand->monsterTarget == players[gui_player]->entity->getUID() ) { - players[gui_player]->closeAllGUIs(CLOSEGUI_ENABLE_SHOOTMODE, CLOSEGUI_DONT_CLOSE_FOLLOWERGUI); + players[gui_player]->closeAllGUIs(CLOSEGUI_ENABLE_SHOOTMODE, CLOSEGUI_CLOSE_ALL); return; } @@ -2267,6 +2301,13 @@ void FollowerRadialMenu::drawFollowerMenu() closeFollowerMenuGUI(); return; } + if ( followerToCommand->monsterAllyIndex < 0 && + (!followerStats->getEffectActive(EFF_COMMAND) + || followerStats->getEffectActive(EFF_COMMAND) - 1 != gui_player)) + { + closeFollowerMenuGUI(); + return; + } tinkeringFollower = isTinkeringFollower(followerStats->type); int skillLVL = 0; @@ -2384,7 +2425,9 @@ void FollowerRadialMenu::drawFollowerMenu() optionSelected = ALLY_CMD_DUMMYBOT_RETURN; } } - else if ( followerToCommand->monsterAllySummonRank != 0 && optionSelected == ALLY_CMD_CLASS_TOGGLE ) + else if ( (followerToCommand->monsterAllySummonRank != 0 + || followerStats->type == MONSTER_ADORCISED_WEAPON + || followerStats->type == FLAME_ELEMENTAL) && optionSelected == ALLY_CMD_CLASS_TOGGLE ) { optionSelected = ALLY_CMD_RETURN_SOUL; } @@ -2475,7 +2518,10 @@ void FollowerRadialMenu::drawFollowerMenu() { if ( !usingLastCmd ) { - if ( stats[gui_player]->shield && itemCategory(stats[gui_player]->shield) == SPELLBOOK ) + if ( stats[gui_player]->shield + && (itemCategory(stats[gui_player]->shield) == SPELLBOOK + || itemTypeIsFoci(stats[gui_player]->shield->type) + || itemTypeIsInstrument(stats[gui_player]->shield->type)) ) { input.consumeBinaryToggle("Defend"); // don't try cast when menu closes. } @@ -2554,15 +2600,15 @@ void FollowerRadialMenu::drawFollowerMenu() // tell player current monster can't do what you asked (e.g using last command & swapping between monsters with different requirements) if ( disableOption < 0 ) { - messagePlayer(gui_player, MESSAGE_MISC, Language::get(3640), getMonsterLocalizedName(followerStats->type).c_str()); + messagePlayer(gui_player, MESSAGE_MISC, Language::get(3640), getMonsterLocalizedName(followerStats->type, followerStats).c_str()); } else if ( tinkeringFollower ) { - messagePlayer(gui_player, MESSAGE_MISC, Language::get(3639), getMonsterLocalizedName(followerStats->type).c_str()); + messagePlayer(gui_player, MESSAGE_MISC, Language::get(3639), getMonsterLocalizedName(followerStats->type, followerStats).c_str()); } else { - messagePlayer(gui_player, MESSAGE_MISC, Language::get(3638), getMonsterLocalizedName(followerStats->type).c_str()); + messagePlayer(gui_player, MESSAGE_MISC, Language::get(3638), getMonsterLocalizedName(followerStats->type, followerStats).c_str()); } } @@ -3022,7 +3068,10 @@ void FollowerRadialMenu::drawFollowerMenu() break; } } - else if ( followerToCommand && followerToCommand->monsterAllySummonRank != 0 ) + else if ( followerToCommand + && (followerToCommand->monsterAllySummonRank != 0 + || followerStats->type == MONSTER_ADORCISED_WEAPON + || followerStats->type == FLAME_ELEMENTAL) ) { getSizeOfText(ttf12, "Relinquish ", &width, nullptr); (*cvar_showoldwheel) ? ttfPrintText(ttf12, txt.x - width / 2, txt.y - 12, Language::get(3196)) : SDL_Rect{}; @@ -3644,14 +3693,21 @@ void FollowerRadialMenu::drawFollowerMenu() auto& textMap = FollowerMenu[gui_player].iconEntries["invalid_action"].text_map["command_unavailable"]; setFollowerBannerTextFormatted(gui_player, bannerTxt, hudColors.characterSheetRed, textMap.second, textMap.first.c_str(), - getMonsterLocalizedName(followerStats->type).c_str()); + getMonsterLocalizedName(followerStats->type, followerStats).c_str()); + } + else if ( disableOption == -4 ) // disabled due to command spell type + { + auto& textMap = FollowerMenu[gui_player].iconEntries["invalid_action"].text_map["command_unavailable_spell"]; + setFollowerBannerTextFormatted(gui_player, bannerTxt, hudColors.characterSheetRed, + textMap.second, textMap.first.c_str(), + getMonsterLocalizedName(followerStats->type, followerStats).c_str()); } else if ( disableOption == -3 ) // disabled due to tinkerbot quality { auto& textMap = FollowerMenu[gui_player].iconEntries["invalid_action"].text_map["tinker_quality_low"]; setFollowerBannerTextFormatted(gui_player, bannerTxt, hudColors.characterSheetRed, textMap.second, textMap.first.c_str(), - getMonsterLocalizedName(followerStats->type).c_str()); + getMonsterLocalizedName(followerStats->type, followerStats).c_str()); } else { @@ -3933,7 +3989,7 @@ void FollowerRadialMenu::drawFollowerMenu() } else { - snprintf(buf, sizeof(buf), Language::get(4200), getMonsterLocalizedName(followerStats->type).c_str()); + snprintf(buf, sizeof(buf), Language::get(4200), getMonsterLocalizedName(followerStats->type, followerStats).c_str()); } for ( int c = 0; c <= strlen(buf); ++c ) @@ -4196,6 +4252,18 @@ bool FollowerRadialMenu::allowedInteractEntity(Entity& selectedEntity, bool upda skillLVL = SKILL_LEVEL_LEGENDARY; } + if ( followerToCommand ) + { + if ( Stat* followerStats = followerToCommand->getStats() ) + { + if ( followerStats->getEffectActive(EFF_COMMAND) >= 1 && followerStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + interactWorld = false; + interactItems = false; + } + } + } + bool enableAttack = (optionDisabledForCreature(skillLVL, followerStats->type, ALLY_CMD_ATTACK_CONFIRM, followerToCommand) == 0); if ( !interactItems && !interactWorld && enableAttack ) @@ -4234,6 +4302,14 @@ bool FollowerRadialMenu::allowedInteractEntity(Entity& selectedEntity, bool upda strcat(interactText, Language::get(4044)); // "switch" } } + else if ( (selectedEntity.behavior == &actWallButton || selectedEntity.sprite == 1151 + || selectedEntity.sprite == 1152) && interactWorld ) + { + if ( updateInteractText ) + { + strcat(interactText, Language::get(6393)); // "button" + } + } else if ( (selectedEntity.behavior == &actTeleportShrine ) && (interactWorld || interactItems || enableAttack) && followerStats->type != GYROBOT ) { if ( updateInteractText ) @@ -4256,6 +4332,7 @@ bool FollowerRadialMenu::allowedInteractEntity(Entity& selectedEntity, bool upda strcat(interactText, Language::get(4310)); // "ladder" break; case 2: + case 3: strcat(interactText, Language::get(4311)); // "portal" break; default: @@ -4337,6 +4414,12 @@ int FollowerRadialMenu::optionDisabledForCreature(int playerSkillLVL, int monste case GYROBOT: case SENTRYBOT: case SPELLBOT: + case REVENANT_SKULL: + case MINIMIMIC: + case MONSTER_ADORCISED_WEAPON: + case FLAME_ELEMENTAL: + case EARTH_ELEMENTAL: + case MOTH_SMALL: creatureTier = 0; break; case GOBLIN: @@ -4355,6 +4438,11 @@ int FollowerRadialMenu::optionDisabledForCreature(int playerSkillLVL, int monste case INSECTOID: case GOATMAN: case BUGBEAR: + case DRYAD: + case MYCONID: + case SALAMANDER: + case GREMLIN: + case MIMIC: creatureTier = 2; break; case CRYSTALGOLEM: @@ -4485,6 +4573,11 @@ int FollowerRadialMenu::optionDisabledForCreature(int playerSkillLVL, int monste } } + if ( option == ALLY_CMD_SPECIAL && monsterType == EARTH_ELEMENTAL ) + { + return -1; // disabled due to monster. + } + if ( option == ALLY_CMD_SPECIAL && follower->monsterAllySpecialCooldown != 0 ) { @@ -4512,6 +4605,10 @@ int FollowerRadialMenu::optionDisabledForCreature(int playerSkillLVL, int monste { return 0; } + if ( followerStats && followerStats->getEffectActive(EFF_COMMAND) >= 1 && followerStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + return -4; // unavailable due to spell + } if ( creatureTier > 0 ) { requirement = 20 * creatureTier; // 20, 40, 60. @@ -4617,7 +4714,9 @@ int FollowerRadialMenu::optionDisabledForCreature(int playerSkillLVL, int monste break; case ALLY_CMD_CLASS_TOGGLE: - if ( follower && follower->monsterAllySummonRank != 0 ) + if ( follower && (follower->monsterAllySummonRank != 0 + || monsterType == MONSTER_ADORCISED_WEAPON + || monsterType == FLAME_ELEMENTAL) ) { return 0; } @@ -4629,6 +4728,10 @@ int FollowerRadialMenu::optionDisabledForCreature(int playerSkillLVL, int monste { return -1; // disabled due to creature. } + if ( followerStats && followerStats->getEffectActive(EFF_COMMAND) >= 1 && followerStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + return -4; // unavailable due to spell + } if ( playerSkillLVL < requirement ) { return requirement; // disabled due to basic skill requirements. @@ -4645,6 +4748,10 @@ int FollowerRadialMenu::optionDisabledForCreature(int playerSkillLVL, int monste { return 0; } + if ( followerStats && followerStats->getEffectActive(EFF_COMMAND) >= 1 && followerStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + return -4; // unavailable due to spell + } if ( playerSkillLVL < requirement ) { return requirement; // disabled due to basic skill requirements. @@ -4657,6 +4764,10 @@ int FollowerRadialMenu::optionDisabledForCreature(int playerSkillLVL, int monste { return -1; // disabled due to creature. } + if ( followerStats && followerStats->getEffectActive(EFF_COMMAND) >= 1 && followerStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + return -4; // unavailable due to spell + } if ( playerSkillLVL < requirement ) { return requirement; // disabled due to basic skill requirements. @@ -4801,6 +4912,16 @@ bool FollowerRadialMenu::allowedInteractItems(int monsterType) { return false; } + /*if ( followerToCommand ) + { + if ( Stat* followerStats = followerToCommand->getStats() ) + { + if ( followerStats->getEffectActive(EFF_COMMAND) >= 1 && followerStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + return false; + } + } + }*/ return true; break; default: @@ -4811,7 +4932,20 @@ bool FollowerRadialMenu::allowedInteractItems(int monsterType) bool FollowerRadialMenu::attackCommandOnly(int monsterType) { - return !(allowedInteractItems(monsterType) || allowedInteractWorld(monsterType) || allowedInteractFood(monsterType)); + bool result = !(allowedInteractItems(monsterType) || allowedInteractWorld(monsterType) || allowedInteractFood(monsterType)); + + if ( followerToCommand ) + { + if ( Stat* followerStats = followerToCommand->getStats() ) + { + if ( followerStats->getEffectActive(EFF_COMMAND) >= 1 && followerStats->getEffectActive(EFF_COMMAND) < MAXPLAYERS + 1 ) + { + return true; + } + } + } + + return result; } void FollowerRadialMenu::monsterGyroBotConvertCommand(int* option) @@ -4951,6 +5085,523 @@ bool GenericGUIMenu::isItemEnchantWeaponable(const Item* item) return false; } +bool GenericGUIMenu::isItemDesecratable(const Item* item) +{ + if ( !item ) + { + return false; + } + if ( !item->identified ) + { + return false; + } + + if ( item->beatitude > 0 ) + { + return true; + } + + + return false; +} + +void GenericGUIMenu::desecrateItem(Item* item) +{ + if ( !item ) + { + return; + } + if ( !shouldDisplayItemInGUI(item) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6720), item->getName()); + return; + } + + item->beatitude = -item->beatitude; + messagePlayer(gui_player, MESSAGE_HINT, Language::get(858), item->getName()); // glows black + + if ( multiplayer == CLIENT ) + { + Item** slot = itemSlot(stats[gui_player], item); + int armornum = -1; + if ( slot ) + { + if ( slot == &stats[gui_player]->weapon ) + { + armornum = 0; + } + else if ( slot == &stats[gui_player]->helmet ) + { + armornum = 1; + } + else if ( slot == &stats[gui_player]->breastplate ) + { + armornum = 2; + } + else if ( slot == &stats[gui_player]->gloves ) + { + armornum = 3; + } + else if ( slot == &stats[gui_player]->shoes ) + { + armornum = 4; + } + else if ( slot == &stats[gui_player]->shield ) + { + armornum = 5; + } + else if ( slot == &stats[gui_player]->cloak ) + { + armornum = 6; + } + else if ( slot == &stats[gui_player]->mask ) + { + armornum = 7; + } + else if ( slot == &stats[gui_player]->mask ) + { + armornum = 7; + } + } + if ( armornum >= 0 ) + { + strcpy((char*)net_packet->data, "BEAT"); + net_packet->data[4] = gui_player; + net_packet->data[5] = armornum; + net_packet->data[6] = item->beatitude + 100; + SDLNet_Write16((Sint16)item->type, &net_packet->data[7]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, 0); + //messagePlayer(player, "sent server: %d, %d, %d", net_packet->data[4], net_packet->data[5], net_packet->data[6]); + } + } + closeGUI(); +} + +bool GenericGUIMenu::isItemBlessWaterable(const Item* item) +{ + if ( !item ) + { + return false; + } + if ( !item->identified ) + { + return false; + } + + if ( item->type == POTION_WATER ) + { + return true; + } + + return false; +} + +void GenericGUIMenu::blessWater(Item* item) +{ + if ( !item ) + { + return; + } + if ( !shouldDisplayItemInGUI(item) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6721), item->getName()); + return; + } + + item->beatitude = item->beatitude + 1; + item->status = SERVICABLE; + messagePlayer(gui_player, MESSAGE_HINT, Language::get(859), item->getName()); // glows blue + + closeGUI(); +} + +bool GenericGUIMenu::isItemSanctifiable(const Item* item) +{ + if ( !item ) + { + return false; + } + if ( !item->identified ) + { + return false; + } + + if ( item->beatitude < 0 ) + { + return true; + } + + return false; +} + +void GenericGUIMenu::sanctifyItem(Item* item) +{ + if ( !item ) + { + return; + } + if ( !shouldDisplayItemInGUI(item) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6721), item->getName()); + return; + } + + item->beatitude = -item->beatitude; + messagePlayer(gui_player, MESSAGE_HINT, Language::get(859), item->getName()); // glows blue + + if ( multiplayer == CLIENT ) + { + Item** slot = itemSlot(stats[gui_player], item); + int armornum = -1; + if ( slot ) + { + if ( slot == &stats[gui_player]->weapon ) + { + armornum = 0; + } + else if ( slot == &stats[gui_player]->helmet ) + { + armornum = 1; + } + else if ( slot == &stats[gui_player]->breastplate ) + { + armornum = 2; + } + else if ( slot == &stats[gui_player]->gloves ) + { + armornum = 3; + } + else if ( slot == &stats[gui_player]->shoes ) + { + armornum = 4; + } + else if ( slot == &stats[gui_player]->shield ) + { + armornum = 5; + } + else if ( slot == &stats[gui_player]->cloak ) + { + armornum = 6; + } + else if ( slot == &stats[gui_player]->mask ) + { + armornum = 7; + } + else if ( slot == &stats[gui_player]->mask ) + { + armornum = 7; + } + } + if ( armornum >= 0 ) + { + strcpy((char*)net_packet->data, "BEAT"); + net_packet->data[4] = gui_player; + net_packet->data[5] = armornum; + net_packet->data[6] = item->beatitude + 100; + SDLNet_Write16((Sint16)item->type, &net_packet->data[7]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, 0); + //messagePlayer(player, "sent server: %d, %d, %d", net_packet->data[4], net_packet->data[5], net_packet->data[6]); + } + } + closeGUI(); +} + +bool GenericGUIMenu::isItemCleaseFoodable(const Item* item) +{ + if ( !item ) + { + return false; + } + if ( !item->identified ) + { + return false; + } + + if ( itemCategory(item) == FOOD ) + { + if ( item->status < EXCELLENT ) + { + return true; + } + else if ( item->beatitude < 0 ) + { + return true; + } + } + + return false; +} + +void GenericGUIMenu::cleanseFood(Item* item) +{ + if ( !item ) + { + return; + } + if ( !shouldDisplayItemInGUI(item) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6723), item->getName()); + return; + } + + bool statusModified = false; + if ( item->status < EXCELLENT ) + { + item->status = EXCELLENT; + statusModified = true; + } + else + { + if ( item->beatitude < 0 ) + { + item->beatitude += 1; + statusModified = true; + } + } + + /*if ( item->beatitude == 0 ) + { + item->beatitude = 1; + statusModified = true; + }*/ + if ( statusModified ) + { + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, nullptr, SPELL_CLEANSE_FOOD, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + + messagePlayer(gui_player, MESSAGE_HINT, Language::get(6725), item->getName()); // looks fresher + } + closeGUI(); +} + +bool GenericGUIMenu::isItemVoidable(const Item* item) +{ + if ( !item ) + { + return false; + } + if ( !item->identified ) + { + return false; + } + + if ( items[item->type].hasAttribute("UNVOIDABLE") ) + { + return false; + } + return true; +} + +bool GenericGUIMenu::isItemAdorcisable(const Item* item) +{ + if ( !item ) + { + return false; + } + if ( !item->identified ) + { + return false; + } + + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ADORCISE_WEAPON ) + { + if ( getWeaponSkill(item) >= PRO_SWORD && getWeaponSkill(item) <= PRO_POLEARM ) + { + return true; + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ADORCISE_INSTRUMENT ) + { + if ( itemTypeIsInstrument(item->type) ) + { + return true; + } + } + return false; +} + +bool GenericGUIMenu::isItemScepterChargeable(const Item* item) +{ + if ( !item ) + { + return false; + } + if ( !item->identified ) + { + return false; + } + + if ( itemCategory(item) == SPELL_CAT ) + { + return true; + } + return false; +} + +bool GenericGUIMenu::isItemAlterable(const Item* item) +{ + if ( !item ) + { + return false; + } + if ( !item->identified ) + { + return false; + } + + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_INSTRUMENT ) + { + if ( itemTypeIsInstrument(item->type) ) + { + return true; + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_METALLURGY ) + { + int metal = 0; + int magic = 0; + GenericGUIMenu::tinkeringGetItemValue(item, &metal, &magic); + if ( metal > 0 && itemCategory(item) != GEM ) + { + if ( item->getGoldValue() > 0 ) + { + int value = item->sellValue(-1) / 4; + return value > 0; + } + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_VANDALISE ) + { + if ( item->status == BROKEN ) + { + return false; + } + + if ( items[item->type].hasAttribute("UNVANDALISABLE") ) + { + return false; + } + + if ( item->getGoldValue() > 0 ) + { + int value = item->sellValue(-1) / 20; + return value > 0; + } + return true; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_GEOMANCY ) + { + int metal = 0; + int magic = 0; + GenericGUIMenu::tinkeringGetItemValue(item, &metal, &magic); + if ( metal > 0 ) + { + if ( item->getGoldValue() > 0 ) + { + int value = item->sellValue(-1) / 2; + return value > 0; + } + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_KEY ) + { + if ( item->type == KEY_IRON + || item->type == KEY_BRONZE + || item->type == KEY_SILVER + /*|| item->type == KEY_GOLD*/ + || (itemCategory(item) == GEM && item->type != GEM_ROCK && item->type != GEM_LUCK /*&& item->type != GEM_GLASS*/) ) + { + return true; + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_JEWEL ) + { + /*if ( item->type == GEM_GLASS ) + { + return true; + } + else */ + if ( (itemCategory(item) == GEM && item->type != GEM_ROCK && item->type != GEM_LUCK /*&& item->type != GEM_GLASS*/ + && !(item->type == GEM_JEWEL && item->status == EXCELLENT)) ) + { + return true; + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ENHANCE_WEAPON ) + { + if ( item->type == BRONZE_AXE + || item->type == BRONZE_MACE + || item->type == BRONZE_SWORD + || item->type == BRONZE_TOMAHAWK ) + { + return true; + } + else if ( item->type == IRON_AXE + || item->type == IRON_MACE + || item->type == IRON_SWORD + || item->type == IRON_SPEAR + || item->type == IRON_DAGGER ) + { + return true; + } + else if ( item->type == STEEL_AXE + || item->type == STEEL_MACE + || item->type == STEEL_SWORD + || item->type == STEEL_HALBERD + || item->type == STEEL_CHAKRAM ) + { + return true; + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESHAPE_WEAPON ) + { + if ( item->type == BRONZE_AXE + || item->type == BRONZE_MACE + || item->type == BRONZE_SWORD ) + { + return true; + } + else if ( item->type == IRON_AXE + || item->type == IRON_MACE + || item->type == IRON_SWORD + || item->type == IRON_SPEAR ) + { + return true; + } + else if ( item->type == STEEL_AXE + || item->type == STEEL_MACE + || item->type == STEEL_SWORD + || item->type == STEEL_HALBERD ) + { + return true; + } + else if ( item->type == CRYSTAL_BATTLEAXE + || item->type == CRYSTAL_MACE + || item->type == CRYSTAL_SWORD + || item->type == CRYSTAL_SPEAR ) + { + return true; + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_ARROW ) + { + if ( itemCategory(item) == GEM ) + { + return true; + } + else if ( itemTypeIsQuiver(item->type) ) + { + return true; + } + } + return false; +} + bool GenericGUIMenu::isItemRepairable(const Item* item, int repairScroll) { if ( !item ) @@ -4972,6 +5623,18 @@ bool GenericGUIMenu::isItemRepairable(const Item* item, int repairScroll) } return false; } + else if(item->type == MAGICSTAFF_SCEPTER) + { + if ( item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX < 100 ) + { + return true; + } + if ( item->status == EXCELLENT ) + { + return false; + } + return true; + } else if ( cat == MAGICSTAFF ) { if ( item->status == EXCELLENT ) @@ -5025,6 +5688,7 @@ bool GenericGUIMenu::isItemRepairable(const Item* item, int repairScroll) case TOOL_DECOY: case TOOL_DUMMYBOT: case ENCHANTED_FEATHER: + case TOOL_DUCK: return false; break; default: @@ -5206,40 +5870,56 @@ void GenericGUIMenu::updateGUI() } if ( guiType == GUI_TYPE_ALCHEMY ) { - if ( !alembicItem ) + if ( (alembicEntityUid == 0 && !alembicItem) || (alembicEntityUid != 0 && !uidToEntity(alembicEntityUid)) ) { closeGUI(); return; } - if ( !alembicItem->node ) + + if ( alembicItem ) { - closeGUI(); - return; + if ( !alembicItem->node ) + { + closeGUI(); + return; + } + if ( alembicItem->node->list != &stats[gui_player]->inventory ) + { + // dropped out of inventory or something. + closeGUI(); + return; + } } - if ( alembicItem->node->list != &stats[gui_player]->inventory ) + } + else if ( guiType == GUI_TYPE_MAILBOX ) + { + if ( mailboxEntityUid == 0 || (mailboxEntityUid != 0 && !uidToEntity(mailboxEntityUid)) ) { - // dropped out of inventory or something. closeGUI(); return; } } else if ( guiType == GUI_TYPE_TINKERING ) { - if ( !tinkeringKitItem ) + if ( (workstationEntityUid == 0 && !tinkeringKitItem) || (workstationEntityUid != 0 && !uidToEntity(workstationEntityUid)) ) { closeGUI(); return; } - if ( !tinkeringKitItem->node ) - { - closeGUI(); - return; - } - if ( tinkeringKitItem->node->list != &stats[gui_player]->inventory ) + + if ( tinkeringKitItem ) { - // dropped out of inventory or something. - closeGUI(); - return; + if ( !tinkeringKitItem->node ) + { + closeGUI(); + return; + } + if ( tinkeringKitItem->node->list != &stats[gui_player]->inventory ) + { + // dropped out of inventory or something. + closeGUI(); + return; + } } } else if ( guiType == GUI_TYPE_SCRIBING ) @@ -5566,106 +6246,6 @@ void GenericGUIMenu::updateGUI() //ttfPrintText(font, highlightBtn.x + 4 + charWidth, pos.y - (8 - txtHeight), Language::get(3719)); } - if ( guiType != GUI_TYPE_TINKERING && guiType != GUI_TYPE_ALCHEMY - && guiType != GUI_TYPE_SCRIBING ) // gradually remove all this for all windows once upgraded - { - //drawImage(identifyGUI_img, NULL, &pos); - - ////Buttons - //if ( inputs.bMouseLeft(gui_player) ) - //{ - // //GUI scroll up button. - // if ( omousey >= gui_startx + 16 && omousey < gui_startx + 52 ) - // { - // if ( omousex >= gui_starty + (identifyGUI_img->w - 28) && omousex < gui_starty + (identifyGUI_img->w - 12) ) - // { - // buttonclick = 7; - // scroll--; - // inputs.mouseClearLeft(gui_player); - // } - // } - // //GUI scroll down button. - // else if ( omousey >= gui_startx + 52 && omousey < gui_startx + 88 ) - // { - // if ( omousex >= gui_starty + (identifyGUI_img->w - 28) && omousex < gui_starty + (identifyGUI_img->w - 12) ) - // { - // buttonclick = 8; - // scroll++; - // inputs.mouseClearLeft(gui_player); - // } - // } - // else if ( omousey >= gui_startx && omousey < gui_startx + 15 ) - // { - // //GUI close button. - // if ( omousex >= gui_starty + 393 && omousex < gui_starty + 407 ) - // { - // buttonclick = 9; - // inputs.mouseClearLeft(gui_player); - // } - - // // 20/12/20 - disabling this for now. unnecessary - // if ( false ) - // { - // if ( omousex >= gui_starty && omousex < gui_starty + 377 && omousey >= gui_startx && omousey < gui_startx + 15 ) - // { - // gui_clickdrag[gui_player] = true; - // draggingGUI = true; - // dragoffset_x[gui_player] = omousex - gui_starty; - // dragoffset_y[gui_player] = omousey - gui_startx; - // inputs.mouseClearLeft(gui_player); - // } - // } - // } - //} - - //// mousewheel - //if ( omousex >= gui_starty + 12 && omousex < gui_starty + (identifyGUI_img->w - 28) ) - //{ - // if ( omousey >= gui_startx + 16 && omousey < gui_startx + (identifyGUI_img->h - 8) ) - // { - // if ( mousestatus[SDL_BUTTON_WHEELDOWN] ) - // { - // mousestatus[SDL_BUTTON_WHEELDOWN] = 0; - // scroll++; - // } - // else if ( mousestatus[SDL_BUTTON_WHEELUP] ) - // { - // mousestatus[SDL_BUTTON_WHEELUP] = 0; - // scroll--; - // } - // } - //} - - //if ( draggingGUI ) - //{ - // if ( gui_clickdrag[gui_player] ) - // { - // offsetx = (omousex - dragoffset_x[gui_player]) - (gui_starty - offsetx); - // offsety = (omousey - dragoffset_y[gui_player]) - (gui_startx - offsety); - // if ( gui_starty <= 0 ) - // { - // offsetx = 0 - (gui_starty - offsetx); - // } - // if ( gui_starty > 0 + xres - identifyGUI_img->w ) - // { - // offsetx = (0 + xres - identifyGUI_img->w) - (gui_starty - offsetx); - // } - // if ( gui_startx <= 0 ) - // { - // offsety = 0 - (gui_startx - offsety); - // } - // if ( gui_startx > 0 + players[gui_player]->camera_y2() - identifyGUI_img->h ) - // { - // offsety = (0 + players[gui_player]->camera_y2() - identifyGUI_img->h) - (gui_startx - offsety); - // } - // } - // else - // { - // draggingGUI = false; - // } - //} - } - list_t* player_inventory = &stats[gui_player]->inventory; if ( guiType == GUI_TYPE_TINKERING ) { @@ -5691,390 +6271,43 @@ void GenericGUIMenu::updateGUI() messagePlayer(0, MESSAGE_DEBUG, "Warning: stats[%d].inventory is not a valid list. This should not happen.", gui_player); } - //else - //{ - // //Print the window label signifying this GUI. - // char* window_name; - // /*if ( guiType == GUI_TYPE_REPAIR ) - // { - // if ( itemEffectItemType == SCROLL_REPAIR ) - // { - // window_name = Language::get(3286); - // } - // else if ( itemEffectItemType == SCROLL_CHARGING ) - // { - // window_name = Language::get(3732); - // } - // ttfPrintText(ttf8, (gui_starty + 2 + ((identifyGUI_img->w / 2) - ((TTF8_WIDTH * longestline(window_name)) / 2))), gui_startx + 4, window_name); - // } - // else */ - // if ( guiType == GUI_TYPE_ALCHEMY ) - // { - // /*if ( !basePotion ) - // { - // if ( !experimentingAlchemy ) - // { - // window_name = Language::get(3328); - // } - // else - // { - // window_name = Language::get(3344); - // } - // ttfPrintText(ttf8, (gui_starty + 2 + ((identifyGUI_img->w / 2) - ((TTF8_WIDTH * longestline(window_name)) / 2))), gui_startx + 4, window_name); - // } - // else - // { - // if ( !experimentingAlchemy ) - // { - // window_name = Language::get(3329); - // } - // else - // { - // window_name = Language::get(3345); - // } - // ttfPrintText(ttf8, (gui_starty + 2 + ((identifyGUI_img->w / 2) - ((TTF8_WIDTH * longestline(window_name)) / 2))), - // gui_startx + 4 - TTF8_HEIGHT - 4, window_name); - // int count = basePotion->count; - // basePotion->count = 1; - // char *description = basePotion->description(); - // basePotion->count = count; - // ttfPrintText(ttf8, (gui_starty + 2 + ((identifyGUI_img->w / 2) - ((TTF8_WIDTH * longestline(description)) / 2))), - // gui_startx + 4, description); - // }*/ - // } - // /*else if ( guiType == GUI_TYPE_REMOVECURSE ) - // { - // window_name = Language::get(346); - // ttfPrintText(ttf8, (gui_starty + 2 + ((identifyGUI_img->w / 2) - ((TTF8_WIDTH * longestline(window_name)) / 2))), gui_startx + 4, window_name); - // } - // else if ( guiType == GUI_TYPE_IDENTIFY ) - // { - // window_name = Language::get(318); - // ttfPrintText(ttf8, (gui_starty + 2 + ((identifyGUI_img->w / 2) - ((TTF8_WIDTH * longestline(window_name)) / 2))), gui_startx + 4, window_name); - // }*/ - - // if ( guiType != GUI_TYPE_TINKERING - // && guiType != GUI_TYPE_ALCHEMY - // && guiType != GUI_TYPE_SCRIBING ) - // { - // //GUI up button. - // if ( buttonclick == 7 ) - // { - // pos.x = gui_starty + (identifyGUI_img->w - 28); - // pos.y = gui_startx + 16; - // pos.w = 0; - // pos.h = 0; - // drawImage(invup_bmp, NULL, &pos); - // } - // //GUI down button. - // if ( buttonclick == 8 ) - // { - // pos.x = gui_starty + (identifyGUI_img->w - 28); - // pos.y = gui_startx + 52; - // pos.w = 0; - // pos.h = 0; - // drawImage(invdown_bmp, NULL, &pos); - // } - // //GUI close button. - // if ( buttonclick == 9 ) - // { - // pos.x = gui_starty + 393; - // pos.y = gui_startx; - // pos.w = 0; - // pos.h = 0; - // drawImage(invclose_bmp, NULL, &pos); - // closeGUI(); - // } - - // Item *item = nullptr; - - // bool selectingSlot = false; - // SDL_Rect slotPos; - // slotPos.x = gui_starty + 12; - // slotPos.w = inventoryoptionChest_bmp->w; - // slotPos.y = gui_startx + 16; - // slotPos.h = inventoryoptionChest_bmp->h; - // bool mouseWithinBoundaryX = (mousex >= slotPos.x && mousex < slotPos.x + slotPos.w); - - // for ( int i = 0; i < kNumShownItems; ++i, slotPos.y += slotPos.h ) - // { - // pos.x = slotPos.x; - // pos.w = 0; - // pos.h = 0; - - - // if ( mouseWithinBoundaryX && omousey >= slotPos.y && omousey < slotPos.y + slotPos.h && itemsDisplayed[i] ) - // { - // pos.y = slotPos.y; - // drawImage(inventoryoptionChest_bmp, nullptr, &pos); - // selectedSlot = i; - // selectingSlot = true; - // if ( (inputs.bMouseLeft(gui_player) || inputs.bControllerInputPressed(gui_player, INJOY_MENU_USE)) ) - // { - // inputs.controllerClearInput(gui_player, INJOY_MENU_USE); - // inputs.mouseClearLeft(gui_player); - - // bool result = executeOnItemClick(itemsDisplayed[i]); - // GUICurrentType oldType = guiType; - // rebuildGUIInventory(); - - // if ( oldType == GUI_TYPE_ALCHEMY && !guiActive ) - // { - // // do nothing - // } - // else if ( itemsDisplayed[i] == nullptr ) - // { - // if ( itemsDisplayed[0] == nullptr ) - // { - // //Go back to inventory. - // selectedSlot = -1; - // players[gui_player]->inventoryUI.warpMouseToSelectedItem(nullptr, (Inputs::SET_CONTROLLER)); - // } - // else - // { - // //Move up one slot. - // --selectedSlot; - // warpMouseToSelectedSlot(); - // } - // } - // } - // } - // } - - // if ( !selectingSlot ) - // { - // selectedSlot = -1; - // } - // } + if ( player_inventory && guiType != GUI_TYPE_ALCHEMY ) + { + rebuildGUIInventory(); - // //Okay, now prepare to render all the items. - // y = gui_startx + 22; - // c = 0; - if ( player_inventory && guiType != GUI_TYPE_ALCHEMY ) + std::unordered_map itemCounts; + if ( guiType == GUI_TYPE_TINKERING && tinkeringFilter == TINKER_FILTER_CRAFTABLE ) { - rebuildGUIInventory(); - - std::unordered_map itemCounts; - if ( guiType == GUI_TYPE_TINKERING && tinkeringFilter == TINKER_FILTER_CRAFTABLE ) + for ( node = stats[gui_player]->inventory.first; node != NULL; node = node->next ) { - for ( node = stats[gui_player]->inventory.first; node != NULL; node = node->next ) + if ( node->element ) { - if ( node->element ) - { - Item* item = (Item*)node->element; - itemCounts[item->type] += item->count; - } + Item* item = (Item*)node->element; + itemCounts[item->type] += item->count; } - for ( node = player_inventory->first; node != NULL; node = node->next ) + } + for ( node = player_inventory->first; node != NULL; node = node->next ) + { + if ( node->element ) { - if ( node->element ) + Item* item = (Item*)node->element; + if ( isNodeTinkeringCraftableItem(item->node) ) { - Item* item = (Item*)node->element; - if ( isNodeTinkeringCraftableItem(item->node) ) + // make the displayed items reflect how many you are carrying. + item->count = 0; + if ( itemCounts.find(item->type) != itemCounts.end() ) { - // make the displayed items reflect how many you are carrying. - item->count = 0; - if ( itemCounts.find(item->type) != itemCounts.end() ) - { - item->count = itemCounts[item->type]; - } - } - else - { - // stop once we reach normal inventory. - break; + item->count = itemCounts[item->type]; } } + else + { + // stop once we reach normal inventory. + break; + } } } - - // //Actually render the items. - // c = 0; - // for ( node = player_inventory->first; node != NULL; node = node->next ) - // { - // if ( node->element ) - // { - // Item* item = (Item*)node->element; - // bool displayItem = shouldDisplayItemInGUI(item); - // if ( displayItem ) //Skip over all non-used items - // { - // c++; - // if ( c <= scroll ) - // { - // continue; - // } - // char tempstr[256] = { 0 }; - // int showTinkeringBotHealthPercentage = false; - // Uint32 color = uint32ColorWhite; - // if ( guiType == GUI_TYPE_TINKERING ) - // { - // break; - // if ( isNodeTinkeringCraftableItem(item->node) ) - // { - // // if anything, these should be doing - // // strncpy(tempstr, Language::get(N), TEMPSTR_LEN - ) - // // not strlen(Language::get(N)). there is zero safety conferred from this - // // anti-pattern. different story with memcpy(), but strcpy() is not - // // memcpy(). - // strcpy(tempstr, Language::get(3644)); // craft - // strncat(tempstr, item->description(), 46 - strlen(Language::get(3644))); - // if ( !tinkeringPlayerCanAffordCraft(item) || (tinkeringPlayerHasSkillLVLToCraft(item) == -1) ) - // { - // color = uint32ColorGray; - // } - // } - // else if ( isItemSalvageable(item, gui_player) && tinkeringFilter != TINKER_FILTER_REPAIRABLE ) - // { - // strcpy(tempstr, Language::get(3645)); // salvage - // strncat(tempstr, item->description(), 46 - strlen(Language::get(3645))); - // } - // else if ( tinkeringIsItemRepairable(item, gui_player) ) - // { - // if ( tinkeringIsItemUpgradeable(item) ) - // { - // if ( tinkeringUpgradeMaxStatus(item) <= item->status ) - // { - // color = uint32ColorGray; // can't upgrade since it's higher status than we can craft. - // } - // else if ( !tinkeringPlayerCanAffordRepair(item) ) - // { - // color = uint32ColorGray; // can't upgrade since no materials - // } - // strcpy(tempstr, Language::get(3684)); // upgrade - // strncat(tempstr, item->description(), 46 - strlen(Language::get(3684))); - // } - // else - // { - // if ( tinkeringPlayerHasSkillLVLToCraft(item) == -1 && itemCategory(item) == TOOL ) - // { - // color = uint32ColorGray; // can't repair since no we can't craft it. - // } - // else if ( !tinkeringPlayerCanAffordRepair(item) ) - // { - // color = uint32ColorGray; // can't repair since no materials - // } - // strcpy(tempstr, Language::get(3646)); // repair - // strncat(tempstr, item->description(), 46 - strlen(Language::get(3646))); - // } - // if ( item->type == TOOL_SENTRYBOT || item->type == TOOL_DUMMYBOT || item->type == TOOL_SPELLBOT ) - // { - // showTinkeringBotHealthPercentage = true; - // } - // } - // else - // { - // messagePlayer(clientnum, MESSAGE_DEBUG, "%d", item->type); - // strncat(tempstr, "invalid item", 13); - // } - // } - // else if ( guiType == GUI_TYPE_SCRIBING ) - // { - // break; - // if ( isNodeScribingCraftableItem(item->node) ) - // { - // snprintf(tempstr, sizeof(tempstr), Language::get(3721), item->getScrollLabel()); - // } - // else - // { - // if ( scribingFilter == SCRIBING_FILTER_REPAIRABLE ) - // { - // strcpy(tempstr, Language::get(3719)); // repair - // strncat(tempstr, item->description(), 46 - strlen(Language::get(3718))); - // } - // else - // { - // strcpy(tempstr, Language::get(3718)); // inscribe - // int oldcount = item->count; - // item->count = 1; - // strncat(tempstr, item->description(), 46 - strlen(Language::get(3718))); - // item->count = oldcount; - // } - // } - // } - // else - // { - // strncpy(tempstr, item->description(), 46); - // } - - // if ( showTinkeringBotHealthPercentage ) - // { - // int health = 100; - // if ( item->appearance >= 0 && item->appearance <= 4 ) - // { - // health = 25 * item->appearance; - // if ( health == 0 && item->status != BROKEN ) - // { - // health = 5; - // } - // } - // char healthstr[32] = ""; - // snprintf(healthstr, 16, " (%d%%)", health); - // strncat(tempstr, healthstr, 46 - strlen(tempstr) - strlen(healthstr)); - // } - // else if ( item->type == ENCHANTED_FEATHER && item->identified ) - // { - // char healthstr[32] = ""; - // snprintf(healthstr, 16, " (%d%%)", item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY); - // strncat(tempstr, healthstr, 46 - strlen(tempstr) - strlen(healthstr)); - // } - // - - // if ( strlen(tempstr) >= 46 ) - // { - // strcat(tempstr, " ..."); - // } - // ttfPrintTextColor(ttf8, gui_starty + 36, y, color, true, tempstr); - // pos.x = gui_starty + 16; - // pos.y = gui_startx + 17 + 18 * (c - scroll - 1); - // pos.w = 16; - // pos.h = 16; - // drawImageScaled(itemSprite(item), NULL, &pos); - // if ( guiType == GUI_TYPE_TINKERING ) - // { - // int metal = 0; - // int magic = 0; - // if ( isNodeTinkeringCraftableItem(item->node) ) - // { - // tinkeringGetCraftingCost(item, &metal, &magic); - // } - // else if ( isItemSalvageable(item, gui_player) && tinkeringFilter != TINKER_FILTER_REPAIRABLE ) - // { - // tinkeringGetItemValue(item, &metal, &magic); - // } - // else if ( tinkeringIsItemRepairable(item, gui_player) ) - // { - // tinkeringGetRepairCost(item, &metal, &magic); - // } - // pos.x = windowX2 - 20 - TTF8_WIDTH * 12; - // if ( !item->identified ) - // { - // ttfPrintTextFormattedColor(ttf8, windowX2 - 24 - TTF8_WIDTH * 15, y, color, " ? ?"); - // } - // else - // { - // ttfPrintTextFormattedColor(ttf8, windowX2 - 24 - TTF8_WIDTH * 15, y, color, "%3d %3d", metal, magic); - // } - // node_t* imageNode = items[TOOL_METAL_SCRAP].surfaces.first; - // if ( imageNode ) - // { - // drawImageScaled(*((SDL_Surface**)imageNode->element), NULL, &pos); - // } - // pos.x += TTF12_WIDTH * 4; - // imageNode = items[TOOL_MAGIC_SCRAP].surfaces.first; - // if ( imageNode ) - // { - // drawImageScaled(*((SDL_Surface**)imageNode->element), NULL, &pos); - // } - // } - // y += 18; - // if ( c > 3 + scroll ) - // { - // break; - // } - // } - // } - // } - // } + } } } } @@ -6105,17 +6338,66 @@ bool GenericGUIMenu::shouldDisplayItemInGUI(Item* item) { return isItemRepairable(item, itemEffectItemType); } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESTORE ) + { + return isItemRepairable(item, SCROLL_REPAIR); + } else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SCROLL_REMOVECURSE || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SPELL_REMOVECURSE ) { return isItemRemoveCursable(item); } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_INSTRUMENT + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_METALLURGY + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_GEOMANCY + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_KEY + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_JEWEL + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ENHANCE_WEAPON + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESHAPE_WEAPON + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_ARROW + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_VANDALISE ) + { + return isItemAlterable(item); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_DESECRATE ) + { + return isItemDesecratable(item); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SANCTIFY_WATER ) + { + return isItemBlessWaterable(item); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SANCTIFY ) + { + return isItemSanctifiable(item); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_CLEANSE_FOOD ) + { + return isItemCleaseFoodable(item); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ADORCISE_WEAPON + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ADORCISE_INSTRUMENT ) + { + return isItemAdorcisable(item); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_PUNCTURE_VOID ) + { + return isItemVoidable(item); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SCEPTER_CHARGE ) + { + return isItemScepterChargeable(item); + } return false; } else if ( guiType == GUI_TYPE_ALCHEMY ) { return isItemMixable(item); } + else if ( guiType == GUI_TYPE_MAILBOX ) + { + return isItemMailable(item); + } else if ( guiType == GUI_TYPE_TINKERING ) { if ( isNodeTinkeringCraftableItem(item->node) ) @@ -6180,6 +6462,13 @@ void GenericGUIMenu::uncurseItem(Item* item) return; } + if ( item->beatitude != 0 ) + { + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SPELL_REMOVECURSE ) + { + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, nullptr, SPELL_REMOVECURSE, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } item->beatitude = 0; //0 = uncursed. > 0 = blessed. messagePlayer(gui_player, MESSAGE_MISC, Language::get(348), item->description()); @@ -6255,13 +6544,578 @@ void GenericGUIMenu::identifyItem(Item* item) if ( !item->identified ) { Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_APPRAISED, item->type, 1); + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SPELL_IDENTIFY ) + { + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, nullptr, SPELL_IDENTIFY, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } } } + bool prevIdentified = item->identified; item->identified = true; + + if ( !prevIdentified ) + { + Item::onItemIdentified(gui_player, item); + } + messagePlayer(gui_player, MESSAGE_MISC, Language::get(320), item->description()); closeGUI(); } +bool GenericGUIMenu::ItemEffectGUI_t::consumeResourcesForTransmute() +{ + if ( modeHasCostEffect != COST_EFFECT_NONE && players[parentGUI.gui_player] && players[parentGUI.gui_player]->entity ) + { + bool hasGold = costEffectGoldAmount <= 0 || stats[parentGUI.gui_player]->GOLD >= costEffectGoldAmount; + bool hasMana = costEffectMPAmount == 0 || (stats[parentGUI.gui_player]->MP >= costEffectMPAmount || stats[parentGUI.gui_player]->type == VAMPIRE); + if ( hasGold && hasMana ) + { + if ( costEffectGoldAmount != 0 ) + { + stats[parentGUI.gui_player]->GOLD -= costEffectGoldAmount; + stats[parentGUI.gui_player]->GOLD = std::max(0, stats[parentGUI.gui_player]->GOLD); + if ( costEffectGoldAmount < 0 ) + { + playSound(89, 64); // earned money + } + else + { + if ( players[parentGUI.gui_player]->isLocalPlayer() ) + { + Compendium_t::Events_t::eventUpdateCodex(parentGUI.gui_player, Compendium_t::CPDM_GOLD_CASTED, "gold", costEffectGoldAmount); + Compendium_t::Events_t::eventUpdateCodex(parentGUI.gui_player, Compendium_t::CPDM_GOLD_CASTED_RUN, "gold", costEffectGoldAmount); + + steamStatisticUpdate(STEAM_STAT_PAY_TO_WIN, STEAM_STAT_INT, costEffectGoldAmount); + } + } + } + if ( multiplayer != CLIENT ) + { + if ( costEffectMPAmount > 0 ) + { + if ( players[parentGUI.gui_player]->isLocalPlayer() ) + { + if ( costEffectMPAmount > stats[parentGUI.gui_player]->MP ) + { + cameravars[parentGUI.gui_player].shakex += 0.1; + cameravars[parentGUI.gui_player].shakey += 10; + playSoundPlayer(parentGUI.gui_player, 28, 92); + } + Sint32 prevMP = stats[parentGUI.gui_player]->MP; + players[parentGUI.gui_player]->entity->drainMP(costEffectMPAmount); + + if ( parentGUI.itemEffectScrollItem && parentGUI.itemEffectScrollItem->type == SPELL_ITEM ) + { + if ( auto spell = getSpellFromItem(parentGUI.gui_player, parentGUI.itemEffectScrollItem, false) ) + { + players[parentGUI.gui_player]->mechanics.baseSpellIncrementMP( + prevMP - stats[parentGUI.gui_player]->MP, spell->skillID); + } + } + } + } + } + if ( multiplayer == CLIENT ) + { + strcpy((char*)net_packet->data, "FXGD"); + net_packet->data[4] = parentGUI.gui_player; + SDLNet_Write32((Uint32)costEffectGoldAmount, &net_packet->data[5]); + SDLNet_Write32((Uint32)costEffectMPAmount, &net_packet->data[9]); + + Uint16 spellID = 0; + if ( parentGUI.itemEffectScrollItem && parentGUI.itemEffectScrollItem->type == SPELL_ITEM ) + { + if ( auto spell = getSpellFromItem(parentGUI.gui_player, parentGUI.itemEffectScrollItem, false) ) + { + spellID = spell->ID; + } + } + SDLNet_Write16(spellID, &net_packet->data[13]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 15; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + return true; + } + else if ( !hasMana ) + { + messagePlayer(parentGUI.gui_player, MESSAGE_MISC, Language::get(375)); + if ( players[parentGUI.gui_player]->isLocalPlayer() ) + { + playSound(563, 64); + if ( players[parentGUI.gui_player]->magic.noManaProcessedOnTick == 0 ) + { + players[parentGUI.gui_player]->magic.flashNoMana(); + } + } + } + else if ( !hasGold ) + { + messagePlayer(parentGUI.gui_player, MESSAGE_MISC, Language::get(6540)); + } + } + return false; +} + +int GenericGUIMenu::getAlterItemResultAtCycle(Item* item) +{ + if ( !item ) + { + return -1; + } + + std::vector targetItems; + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESHAPE_WEAPON ) + { + switch ( item->type ) + { + case BRONZE_AXE: + case BRONZE_MACE: + case BRONZE_SWORD: + targetItems.push_back(BRONZE_AXE); + targetItems.push_back(BRONZE_MACE); + targetItems.push_back(BRONZE_SWORD); + break; + case IRON_AXE: + case IRON_MACE: + case IRON_SWORD: + case IRON_SPEAR: + targetItems.push_back(IRON_AXE); + targetItems.push_back(IRON_MACE); + targetItems.push_back(IRON_SWORD); + targetItems.push_back(IRON_SPEAR); + break; + case STEEL_AXE: + case STEEL_MACE: + case STEEL_SWORD: + case STEEL_HALBERD: + targetItems.push_back(STEEL_AXE); + targetItems.push_back(STEEL_MACE); + targetItems.push_back(STEEL_SWORD); + targetItems.push_back(STEEL_HALBERD); + break; + case CRYSTAL_BATTLEAXE: + case CRYSTAL_MACE: + case CRYSTAL_SWORD: + case CRYSTAL_SPEAR: + targetItems.push_back(CRYSTAL_BATTLEAXE); + targetItems.push_back(CRYSTAL_MACE); + targetItems.push_back(CRYSTAL_SWORD); + targetItems.push_back(CRYSTAL_SPEAR); + break; + default: + break; + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_ARROW ) + { + if ( itemTypeIsQuiver(item->type) || itemCategory(item) == GEM ) + { + targetItems = + { + QUIVER_LIGHTWEIGHT, + QUIVER_FIRE, + QUIVER_KNOCKBACK, + QUIVER_PIERCE, + //QUIVER_BONE, + QUIVER_SILVER, + //QUIVER_BLACKIRON, + QUIVER_CRYSTAL, + QUIVER_HUNTING, + }; + } + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_INSTRUMENT ) + { + targetItems = { + INSTRUMENT_FLUTE, + INSTRUMENT_LYRE, + INSTRUMENT_DRUM, + INSTRUMENT_LUTE, + INSTRUMENT_HORN + }; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_KEY ) + { + targetItems = { + KEY_IRON, + KEY_BRONZE, + KEY_SILVER, + KEY_GOLD + }; + } + + if ( targetItems.size() == 0 ) { return -1; } + + auto find = std::find(targetItems.begin(), targetItems.end(), item->type); + { + bool inList = find != targetItems.end(); + if ( !inList ) + { + find = targetItems.begin(); + } + + if ( targetItems.size() >= (2 + (inList ? 1 : 0)) ) + { + int cycles = transmuteItemScroll % (targetItems.size() - (inList ? 1 : 0)); + while ( cycles >= 0 ) + { + --cycles; + ++find; + if ( find == targetItems.end() ) + { + find = targetItems.begin(); + } + if ( *find == item->type ) + { + ++find; + if ( find == targetItems.end() ) + { + find = targetItems.begin(); + } + } + } + return *find; + } + else if ( targetItems.size() == (1 + (inList ? 1 : 0)) ) + { + ++find; + if ( find == targetItems.end() ) + { + find = targetItems.begin(); + } + if ( *find == item->type ) + { + ++find; + if ( find == targetItems.end() ) + { + find = targetItems.begin(); + } + } + return *find; + } + else + { + return targetItems.at(0); + } + } + + return -1; +} + +void GenericGUIMenu::alterItem(Item* item) +{ + if ( !item || gui_player < 0 ) + { + return; + } + if ( !shouldDisplayItemInGUI(item) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6514), item->getName()); + closeGUI(); + return; + } + if ( !itemfxGUI.consumeResourcesForTransmute() ) + { + closeGUI(); + return; + } + + bool isEquipped = itemIsEquipped(item, gui_player); + + ItemType prevType = item->type; + std::string prevItem = item->getName(); + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_INSTRUMENT ) + { + int result = getAlterItemResultAtCycle(item); + if ( result >= 0 ) + { + item->type = (ItemType)(result); + } + else + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6514), item->description()); + closeGUI(); + return; + } + + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_ALTER_INSTRUMENT, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ENHANCE_WEAPON ) + { + switch ( item->type ) + { + case BRONZE_AXE: + item->type = IRON_AXE; + break; + case BRONZE_MACE: + item->type = IRON_MACE; + break; + case BRONZE_SWORD: + item->type = IRON_SWORD; + break; + case BRONZE_TOMAHAWK: + item->type = IRON_DAGGER; + break; + case IRON_AXE: + item->type = STEEL_AXE; + break; + case IRON_MACE: + item->type = STEEL_MACE; + break; + case IRON_SWORD: + item->type = STEEL_SWORD; + break; + case IRON_SPEAR: + item->type = STEEL_HALBERD; + break; + case IRON_DAGGER: + item->type = STEEL_CHAKRAM; + break; + case STEEL_AXE: + item->type = CRYSTAL_BATTLEAXE; + break; + case STEEL_MACE: + item->type = CRYSTAL_MACE; + break; + case STEEL_SWORD: + item->type = CRYSTAL_SWORD; + break; + case STEEL_HALBERD: + item->type = CRYSTAL_SPEAR; + break; + case STEEL_CHAKRAM: + item->type = CRYSTAL_SHURIKEN; + break; + default: + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6514), item->description()); + closeGUI(); + return; + } + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_ENHANCE_WEAPON, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESHAPE_WEAPON ) + { + int result = getAlterItemResultAtCycle(item); + if ( result >= 0 ) + { + item->type = (ItemType)(result); + } + else + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6514), item->description()); + closeGUI(); + return; + } + + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_RESHAPE_WEAPON, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_KEY ) + { + ItemType newType = GEM_ROCK; + /*switch ( item->type ) + { + case KEY_IRON: + newType = KEY_BRONZE; + break; + case KEY_BRONZE: + newType = KEY_SILVER; + break; + case KEY_SILVER: + newType = KEY_GOLD; + break; + case KEY_GOLD: + newType = TOOL_SKELETONKEY; + break; + default: + if ( itemCategory(item) == GEM && item->type != GEM_ROCK && item->type != GEM_LUCK ) + { + newType = KEY_IRON; + } + break; + }*/ + int result = getAlterItemResultAtCycle(item); + if ( result >= 0 ) + { + newType = (ItemType)(result); + } + else + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6514), item->description()); + closeGUI(); + return; + } + + if ( newType == GEM_ROCK ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6514), item->getName()); + } + else + { + Item* itemToPickup = newItem(newType, SERVICABLE, item->beatitude, 1, item->appearance, true, nullptr); + Item* pickedUp = itemPickup(gui_player, itemToPickup); + int oldCount = item->count; + item->count = 1; + std::string desc = itemToPickup->description(); + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6553), desc.c_str(), item->description()); + item->count = oldCount; + free(itemToPickup); + consumeItem(item, gui_player); + + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_FORGE_KEY, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + closeGUI(); + return; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_JEWEL ) + { + if ( itemCategory(item) == GEM && item->type != GEM_ROCK && item->type != GEM_LUCK /*&& item->type != GEM_GLASS*/ + && !(item->type == GEM_JEWEL && item->status == EXCELLENT) ) + { + int value = item->getGoldValue(); + Status result = DECREPIT; + if ( item->type == GEM_JEWEL ) + { + result = (Status)std::min((int)EXCELLENT, (int)item->status + 1); + } + else if ( value < 250 ) + { + result = DECREPIT; + } + else if ( value < 500 ) + { + result = WORN; + } + else if ( value < 1000 ) + { + result = SERVICABLE; + } + else if ( value >= 1000 ) + { + result = EXCELLENT; + } + + if ( item->type == GEM_JEWEL ) + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_UPGRADED, GEM_JEWEL, 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_FORGED, GEM_JEWEL, 1); + } + + Item* itemToPickup = newItem(GEM_JEWEL, result, 0, 1, 0, true, nullptr); + Item* pickedUp = itemPickup(gui_player, itemToPickup); + int oldCount = item->count; + item->count = 1; + std::string desc = itemToPickup->description(); + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6553), desc.c_str(), item->description()); + item->count = oldCount; + free(itemToPickup); + consumeItem(item, gui_player); + + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_FORGE_JEWEL, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + else + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6514), item->description()); + } + closeGUI(); + return; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_METALLURGY ) + { + int oldCount = item->count; + item->count = 1; + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6554), -itemfxGUI.costEffectGoldAmount, item->description()); + item->count = oldCount; + consumeItem(item, gui_player); + + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_METALLURGY, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + + closeGUI(); + return; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_GEOMANCY ) + { + int oldCount = item->count; + item->count = 1; + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6554), -itemfxGUI.costEffectGoldAmount, item->description()); + item->count = oldCount; + consumeItem(item, gui_player); + + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_GEOMANCY, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + + closeGUI(); + return; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_VANDALISE ) + { + int oldCount = item->count; + item->count = 1; + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6724), -itemfxGUI.costEffectGoldAmount, item->description()); + item->count = oldCount; + consumeItem(item, gui_player); + + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_VANDALISE, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + + closeGUI(); + return; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_ARROW ) + { + int result = getAlterItemResultAtCycle(item); + if ( result >= 0 ) + { + item->type = (ItemType)(result); + + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, + nullptr, SPELL_ALTER_ARROW, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + else + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6514), item->description()); + closeGUI(); + return; + } + } + messagePlayer(gui_player, MESSAGE_HINT, Language::get(6515), prevItem.c_str(), item->description()); + + std::unordered_set appearancesOfSimilarItems; + // reroll any other conflicting items + for ( node_t* node = stats[gui_player]->inventory.first; node != nullptr; node = node->next ) + { + Item* item2 = static_cast(node->element); + if ( item2 && item2 != item && !itemCompare(item, item2, true) ) + { + // items are the same (incl. appearance!) + // if they shouldn't stack, we need to change appearance of the new item. + appearancesOfSimilarItems.insert(item2->appearance); + } + } + + Item::itemFindUniqueAppearance(item, appearancesOfSimilarItems); + + if ( multiplayer == CLIENT ) + { + if ( isEquipped ) + { + clientSendItemTypeUpdateToServer(gui_player, item, prevType); + } + } + + closeGUI(); +} + void GenericGUIMenu::enchantItem(Item* item) { if ( !item ) @@ -6274,6 +7128,12 @@ void GenericGUIMenu::enchantItem(Item* item) return; } + if ( !itemfxGUI.consumeResourcesForTransmute() ) + { + closeGUI(); + return; + } + if ( itemEffectItemBeatitude >= 0 ) { if ( gui_player >= 0 && gui_player < MAXPLAYERS && players[gui_player]->isLocalPlayer() ) @@ -6363,6 +7223,15 @@ void GenericGUIMenu::repairItem(Item* item) return; } + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESTORE ) + { + if ( !itemfxGUI.consumeResourcesForTransmute() ) + { + closeGUI(); + return; + } + } + bool isEquipped = itemIsEquipped(item, gui_player); if ( itemEffectItemType == SCROLL_CHARGING ) @@ -6370,7 +7239,28 @@ void GenericGUIMenu::repairItem(Item* item) if ( itemCategory(item) == MAGICSTAFF ) { Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_MAGICSTAFF_RECHARGED, item->type, 1); - if ( item->status == BROKEN ) + if ( item->type == MAGICSTAFF_SCEPTER ) + { + int durability = item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX; + int repairAmount = ((MAGICSTAFF_SCEPTER_CHARGE_MAX - 1) - durability); + if ( repairAmount > (MAGICSTAFF_SCEPTER_CHARGE_MAX / 2) ) + { + if ( itemEffectItemBeatitude == 0 ) + { + repairAmount = MAGICSTAFF_SCEPTER_CHARGE_MAX / 2; + } + } + item->status = EXCELLENT; + item->appearance += repairAmount; + messagePlayer(gui_player, MESSAGE_MISC, Language::get(3730), item->getName()); + closeGUI(); + if ( multiplayer == CLIENT && isEquipped ) + { + clientSendAppearanceUpdateToServer(gui_player, item, false); + } + return; + } + else if ( item->status == BROKEN ) { if ( itemEffectItemBeatitude > 0 ) { @@ -6410,7 +7300,11 @@ void GenericGUIMenu::repairItem(Item* item) } else { - if ( (item->type >= ARTIFACT_SWORD && item->type <= ARTIFACT_GLOVES) || item->type == BOOMERANG ) + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESTORE ) + { + item->status = static_cast(std::min(item->status + 1, static_cast(EXCELLENT))); + } + else if ( (item->type >= ARTIFACT_SWORD && item->type <= ARTIFACT_GLOVES) || item->type == BOOMERANG ) { item->status = static_cast(std::min(item->status + 1, static_cast(EXCELLENT))); } @@ -6419,6 +7313,12 @@ void GenericGUIMenu::repairItem(Item* item) item->status = static_cast(std::min(item->status + 2 + itemEffectItemBeatitude, static_cast(EXCELLENT))); } } + + if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESTORE ) + { + magicOnSpellCastEvent(players[gui_player]->entity, players[gui_player]->entity, nullptr, SPELL_RESTORE, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_REPAIRS, item->type, 1); messagePlayer(gui_player, MESSAGE_MISC, Language::get(872), item->getName()); } @@ -6476,6 +7376,7 @@ void GenericGUIMenu::closeGUI() tinkeringFreeLists(); scribingFreeLists(); guiActive = false; + auto prevGUI = guiType; guiType = GUI_TYPE_NONE; basePotion = nullptr; secondaryPotion = nullptr; @@ -6485,6 +7386,15 @@ void GenericGUIMenu::closeGUI() itemEffectScrollItem = nullptr; itemEffectItemType = 0; itemEffectItemBeatitude = 0; + transmuteItemTarget = nullptr; + transmuteItemScroll = 0; + if ( prevGUI == GUI_TYPE_ITEMFX && itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SCEPTER_CHARGE ) + { + if ( players[gui_player]->inventory_mode == INVENTORY_MODE_SPELL ) + { + players[gui_player]->inventory_mode = INVENTORY_MODE_ITEM; + } + } itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_NONE; if ( tinkerGUI.bOpen ) { @@ -6494,6 +7404,10 @@ void GenericGUIMenu::closeGUI() { alchemyGUI.closeAlchemyMenu(); } + if ( mailboxGUI.bOpen ) + { + mailboxGUI.closeMailMenu(); + } if ( featherGUI.bOpen ) { featherGUI.closeFeatherMenu(); @@ -6510,6 +7424,9 @@ void GenericGUIMenu::closeGUI() { players[gui_player]->inventoryUI.tooltipDelayTick = ticks + TICKS_PER_SECOND / 10; } + alembicEntityUid = 0; + workstationEntityUid = 0; + mailboxEntityUid = 0; } //inline Item* GenericGUIMenu::getItemInfo(int slot) @@ -6701,6 +7618,74 @@ void GenericGUIMenu::openGUI(int type, Item* effectItem, int effectBeatitude, in { itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_SPELL_IDENTIFY; } + else if ( usingSpellID == SPELL_ALTER_INSTRUMENT ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_ALTER_INSTRUMENT; + } + else if ( usingSpellID == SPELL_METALLURGY ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_METALLURGY; + } + else if ( usingSpellID == SPELL_GEOMANCY ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_GEOMANCY; + } + else if ( usingSpellID == SPELL_FORGE_KEY ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_FORGE_KEY; + } + else if ( usingSpellID == SPELL_FORGE_JEWEL ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_FORGE_JEWEL; + } + else if ( usingSpellID == SPELL_ENHANCE_WEAPON ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_ENHANCE_WEAPON; + } + else if ( usingSpellID == SPELL_RESHAPE_WEAPON ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_RESHAPE_WEAPON; + } + else if ( usingSpellID == SPELL_ALTER_ARROW ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_ALTER_ARROW; + } + else if ( usingSpellID == SPELL_RESTORE ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_RESTORE; + } + else if ( usingSpellID == SPELL_PUNCTURE_VOID ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_PUNCTURE_VOID; + } + else if ( usingSpellID == SPELL_ADORCISM ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_ADORCISE_WEAPON; + } + else if ( usingSpellID == SPELL_VANDALISE ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_VANDALISE; + } + else if ( usingSpellID == SPELL_DESECRATE ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_DESECRATE; + } + else if ( usingSpellID == SPELL_SANCTIFY ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_SANCTIFY; + } + else if ( usingSpellID == SPELL_SANCTIFY_WATER ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_SANCTIFY_WATER; + } + else if ( usingSpellID == SPELL_CLEANSE_FOOD ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_CLEANSE_FOOD; + } + else if ( usingSpellID == SPELL_ADORCISE_INSTRUMENT ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_ADORCISE_INSTRUMENT; + } else if ( itemEffectItemType == SCROLL_CHARGING ) { itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_SCROLL_CHARGING; @@ -6713,6 +7698,10 @@ void GenericGUIMenu::openGUI(int type, Item* effectItem, int effectBeatitude, in { itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_SPELL_REMOVECURSE; } + else if ( itemEffectItemType == MAGICSTAFF_SCEPTER ) + { + itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_SCEPTER_CHARGE; + } else if ( itemEffectItemType == SCROLL_REPAIR ) { itemfxGUI.currentMode = ItemEffectGUI_t::ITEMFX_MODE_SCROLL_REPAIR; @@ -6758,10 +7747,18 @@ void GenericGUIMenu::openGUI(int type, bool experimenting, Item* itemOpenedWith) players[gui_player]->openStatusScreen(GUI_MODE_INVENTORY, INVENTORY_MODE_ITEM); // Reset the GUI to the inventory. guiActive = true; alembicItem = itemOpenedWith; + alembicEntityUid = 0; experimentingAlchemy = experimenting; guiType = static_cast(type); //players[gui_player]->GUI.activateModule(Player::GUI_t::MODULE_ALCHEMY); - alchemyGUI.openAlchemyMenu(); + if ( alembicItem && alembicItem->type == TOOL_FRYING_PAN ) + { + alchemyGUI.openAlchemyMenu(AlchemyGUI_t::ALCHEMY_VIEW_COOK); + } + else + { + alchemyGUI.openAlchemyMenu(AlchemyGUI_t::ALCHEMY_VIEW_BREW); + } FollowerMenu[gui_player].closeFollowerMenuGUI(); CalloutMenu[gui_player].closeCalloutMenuGUI(); @@ -6801,6 +7798,7 @@ void GenericGUIMenu::openGUI(int type, Item* itemOpenedWith) // build the craftables list. if ( guiType == GUI_TYPE_TINKERING ) { + workstationEntityUid = 0; tinkeringKitItem = itemOpenedWith; tinkeringCreateCraftableItemList(); if ( tinkeringFilter == TINKER_FILTER_CRAFTABLE ) @@ -6831,12 +7829,88 @@ void GenericGUIMenu::openGUI(int type, Item* itemOpenedWith) rebuildGUIInventory(); } +void stationOpenSound(int player, int type) +{ + int sfx = -1; + int vol = 0; + if ( type == GUI_TYPE_ALCHEMY ) + { + vol = 64; + sfx = 774 + local_rng.rand() % 2; + //sfx = 401; + } + else if ( type == GUI_TYPE_TINKERING ) + { + vol = 64; + sfx = 421 + (local_rng.rand() % 2) * 3; + } + + if ( sfx > 0 ) + { + playSoundEntityLocal(players[player]->entity, sfx, vol); + if ( multiplayer == CLIENT ) + { + strcpy((char*)net_packet->data, "EMOT"); + net_packet->data[4] = player; + SDLNet_Write16(sfx, &net_packet->data[5]); + net_packet->data[7] = vol; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + else if ( multiplayer != CLIENT ) + { + for ( int c = 1; c < MAXPLAYERS; ++c ) + { + if ( !client_disconnected[c] && !players[c]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "SNEL"); + SDLNet_Write16(sfx, &net_packet->data[4]); + SDLNet_Write32((Uint32)players[player]->entity->getUID(), &net_packet->data[6]); + SDLNet_Write16(vol, &net_packet->data[10]); + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 12; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } + } + } + } +} + void GenericGUIMenu::openGUI(int type, Entity* shrine) { - Uint32 oldUID = assistShrineGUI.shrineUID; - assistShrineGUI.shrineUID = 0; - this->closeGUI(); - assistShrineGUI.shrineUID = oldUID; + // close existing guis + if ( guiType == GUI_TYPE_ASSIST ) + { + Uint32 oldUID = assistShrineGUI.shrineUID; + assistShrineGUI.shrineUID = 0; + this->closeGUI(); + assistShrineGUI.shrineUID = oldUID; + } + else if ( guiType == GUI_TYPE_ALCHEMY ) + { + Uint32 oldUID = alembicEntityUid; + alembicEntityUid = 0; + this->closeGUI(); + alembicEntityUid = oldUID; + } + else if ( guiType == GUI_TYPE_TINKERING ) + { + Uint32 oldUID = workstationEntityUid; + workstationEntityUid = 0; + this->closeGUI(); + workstationEntityUid = oldUID; + } + else if ( guiType == GUI_TYPE_MAILBOX ) + { + Uint32 oldUID = mailboxEntityUid; + mailboxEntityUid = 0; + this->closeGUI(); + mailboxEntityUid = oldUID; + } + if ( players[gui_player] && players[gui_player]->entity ) { if ( players[gui_player]->entity->isBlind() ) @@ -6864,6 +7938,42 @@ void GenericGUIMenu::openGUI(int type, Entity* shrine) players[gui_player]->GUI.activateModule(Player::GUI_t::MODULE_ASSISTSHRINE); assistShrineGUI.openAssistShrine(shrine); } + else if ( guiType == GUI_TYPE_ALCHEMY ) + { + alembicItem = nullptr; + experimentingAlchemy = true; + alchemyGUI.openAlchemyMenu(AlchemyGUI_t::ALCHEMY_VIEW_COOK); + if ( shrine ) + { + alembicEntityUid = shrine->getUID(); + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_CAULDRON_INTERACTS, "cauldron", 1); + stationOpenSound(gui_player, GUI_TYPE_ALCHEMY); + } + } + else if ( guiType == GUI_TYPE_MAILBOX ) + { + mailboxGUI.openMailMenu(); + if ( shrine ) + { + mailboxEntityUid = shrine->getUID(); + } + } + else if ( guiType == GUI_TYPE_TINKERING ) + { + tinkeringKitItem = nullptr; + tinkeringCreateCraftableItemList(); + if ( tinkeringFilter == TINKER_FILTER_CRAFTABLE ) + { + players[gui_player]->GUI.activateModule(Player::GUI_t::MODULE_TINKERING); + } + tinkerGUI.openTinkerMenu(); + if ( shrine ) + { + workstationEntityUid = shrine->getUID(); + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_WORKBENCH_INTERACTS, "workbench", 1); + stationOpenSound(gui_player, GUI_TYPE_TINKERING); + } + } FollowerMenu[gui_player].closeFollowerMenuGUI(); CalloutMenu[gui_player].closeCalloutMenuGUI(); @@ -6877,6 +7987,182 @@ void GenericGUIMenu::openGUI(int type, Entity* shrine) bool hideRecipeFromList(int type); void onFeatherChangeTabAction(const int playernum, bool changingToNewTab = true); +void GenericGUIMenu::sendItemToVoid(Item* item) +{ + if ( !item || gui_player < 0 ) + { + return; + } + if ( !shouldDisplayItemInGUI(item) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6564), item->getName()); + closeGUI(); + return; + } + + Item* newitem = newItem(item->type, item->status, item->beatitude, item->count, item->appearance, item->identified, nullptr); + if ( Item* insertedItem = Entity::addItemToVoidChest(gui_player, newitem, false, nullptr) ) + { + if ( insertedItem != newitem ) + { + free(newitem); + } + + messagePlayer(gui_player, MESSAGE_INVENTORY, Language::get(6562), item->description()); + + if ( item->node ) + { + list_RemoveNode(item->node); + } + else + { + free(item); + } + } + else + { + if ( multiplayer != CLIENT ) + { + if ( item->count < newitem->count ) + { + messagePlayer(gui_player, MESSAGE_INVENTORY, Language::get(6562), newitem->description()); + } + item->count = newitem->count; // update quantity as some may have went in + } + free(newitem); + } + closeGUI(); +} + +void GenericGUIMenu::adorciseItem(Item* item) +{ + if ( !item || gui_player < 0 ) + { + return; + } + if ( !shouldDisplayItemInGUI(item) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6618), item->getName()); + closeGUI(); + return; + } + + if ( players[gui_player]->entity ) + { + int x = floor(players[gui_player]->entity->x / 16) * 16 + 8.0 + 16.0 * cos(players[gui_player]->entity->yaw); + int y = floor(players[gui_player]->entity->y / 16) * 16 + 8.0 + 16.0 * sin(players[gui_player]->entity->yaw); + if ( multiplayer != CLIENT ) + { + if ( Entity* monster = spellEffectAdorcise(*players[gui_player]->entity, spellElementMap[SPELL_ADORCISM], + x, y, item) ) + { + consumeItem(item, gui_player); + } + else + { + // no room to spawn! + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6578)); + } + } + else + { + strcpy((char*)net_packet->data, "ADOR"); + SDLNet_Write32((Uint32)item->type, &net_packet->data[4]); + SDLNet_Write32((Uint32)item->status, &net_packet->data[8]); + SDLNet_Write32((Uint32)item->beatitude, &net_packet->data[12]); + SDLNet_Write32((Uint32)1, &net_packet->data[16]); + SDLNet_Write32((Uint32)item->appearance, &net_packet->data[20]); + net_packet->data[24] = item->identified; + net_packet->data[25] = gui_player; + SDLNet_Write16((Sint16)(x / 16), &net_packet->data[26]); + SDLNet_Write16((Sint16)(y / 16), &net_packet->data[28]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 30; + sendPacketSafe(net_sock, -1, net_packet, 0); + + consumeItem(item, gui_player); + } + } + closeGUI(); +} + +void GenericGUIMenu::rechargeScepterUsingItem(Item* item) +{ + if ( !item || gui_player < 0 ) + { + return; + } + if ( !shouldDisplayItemInGUI(item) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6837), item->getName()); + closeGUI(); + return; + } + + if ( itemEffectScrollItem && itemEffectScrollItem->type == MAGICSTAFF_SCEPTER ) + { + if ( auto spell = getSpellFromItem(gui_player, item, true) ) + { + messagePlayer(gui_player, MESSAGE_INTERACTION, Language::get(6836), spell->getSpellName()); + messagePlayerColor(gui_player, MESSAGE_INTERACTION, makeColorRGB(0, 255, 0), Language::get(3730), items[itemEffectScrollItem->type].getIdentifiedName()); + playSound(167, 64); + int difficulty = 10 + spell->difficulty / 4; + node_t* nextnode = nullptr; + for ( node_t* node = players[gui_player]->magic.spellList.first; node; node = nextnode ) + { + nextnode = node->next; + if ( node->element ) + { + spell_t* spell2 = (spell_t*)node->element; + if ( spell2 == spell ) + { + if ( spell == players[gui_player]->magic.selectedSpell() ) + { + players[gui_player]->magic.equipSpell(nullptr); + } + for ( int i = 0; i < NUM_HOTBAR_ALTERNATES; ++i ) + { + if ( players[gui_player]->magic.selected_spell_alternate[i] == spell ) + { + players[gui_player]->magic.selected_spell_alternate[i] = nullptr; + } + } + if ( client_classes[gui_player] == CLASS_SCION ) + { + players[gui_player]->mechanics.favoriteBooksAchievement[spell->ID]++; + if ( players[gui_player]->mechanics.favoriteBooksAchievement[spell->ID] >= 5 ) + { + steamAchievement("BARONY_ACH_FAVORITE_BOOK"); + } + } + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ARCHON_SPELLS_FORGOTTEN, MAGICSTAFF_SCEPTER, 1); + list_RemoveNode(node); + break; + } + } + } + consumeItem(item, gui_player); + Uint32 baseAppearance = itemEffectScrollItem->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX; + Uint32 increased = std::min(MAGICSTAFF_SCEPTER_CHARGE_MAX - 1U, baseAppearance + difficulty); + if ( increased > baseAppearance ) + { + itemEffectScrollItem->appearance += (increased - baseAppearance); + itemEffectScrollItem->appearance = itemEffectScrollItem->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX; + } + + if ( multiplayer == CLIENT ) + { + // update item appearance + if ( itemIsEquipped(itemEffectScrollItem, gui_player) ) + { + clientSendAppearanceUpdateToServer(gui_player, itemEffectScrollItem, false); + } + } + } + } + closeGUI(); +} bool GenericGUIMenu::executeOnItemClick(Item* item) { @@ -6898,6 +8184,11 @@ bool GenericGUIMenu::executeOnItemClick(Item* item) repairItem(item); return true; } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESTORE ) + { + repairItem(item); + return true; + } else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SCROLL_REMOVECURSE || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SPELL_REMOVECURSE ) { @@ -6920,6 +8211,55 @@ bool GenericGUIMenu::executeOnItemClick(Item* item) identifyItem(item); return true; } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_INSTRUMENT + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_METALLURGY + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_GEOMANCY + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_KEY + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_FORGE_JEWEL + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ENHANCE_WEAPON + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_RESHAPE_WEAPON + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ALTER_ARROW + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_VANDALISE ) + { + alterItem(item); + return true; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_PUNCTURE_VOID ) + { + sendItemToVoid(item); + return true; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_DESECRATE ) + { + desecrateItem(item); + return true; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SANCTIFY_WATER ) + { + blessWater(item); + return true; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SANCTIFY ) + { + sanctifyItem(item); + return true; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_CLEANSE_FOOD ) + { + cleanseFood(item); + return true; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ADORCISE_WEAPON + || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_ADORCISE_INSTRUMENT ) + { + adorciseItem(item); + return true; + } + else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SCEPTER_CHARGE ) + { + rechargeScepterUsingItem(item); + return true; + } else if ( itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SCROLL_ENCHANT_WEAPON || itemfxGUI.currentMode == ItemEffectGUI_t::ITEMFX_MODE_SCROLL_ENCHANT_ARMOR ) { @@ -6995,7 +8335,14 @@ bool GenericGUIMenu::executeOnItemClick(Item* item) { if ( tinkeringRepairItem(item) ) { - Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_TINKERKIT_REPAIRS, TOOL_TINKERING_KIT, 1); + if ( workstationEntityUid != 0 && uidToEntity(workstationEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_WORKBENCH_REPAIRED, "workbench", 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_TINKERKIT_REPAIRS, TOOL_TINKERING_KIT, 1); + } } } } @@ -7045,25 +8392,10 @@ bool GenericGUIMenu::isItemMixable(const Item* item) return false; } - if ( itemCategory(item) != POTION ) + if ( !alchemyGUI.inventoryItemAllowedInGUI(const_cast(item)) ) { return false; } - if ( item->type == POTION_EMPTY ) - { - return false; - } - - //if ( players[gui_player] && players[gui_player]->entity ) - //{ - // if ( players[gui_player]->entity->isBlind() ) - // { - // messagePlayer(gui_player, MESSAGE_MISC, Language::get(892)); - // closeGUI(); - // return false; // I can't see! - // } - //} - if ( !experimentingAlchemy && !item->identified ) { @@ -7098,18 +8430,21 @@ bool GenericGUIMenu::isItemMixable(const Item* item) } } - if ( !basePotion ) + if ( alchemyGUI.currentView == AlchemyGUI_t::ALCHEMY_VIEW_BREW || alchemyGUI.currentView == AlchemyGUI_t::ALCHEMY_VIEW_RECIPES ) { - // we're selecting the first potion. - switch ( item->type ) + // not used anymore?? + if ( !basePotion ) { + // we're selecting the first potion. + switch ( item->type ) + { case POTION_WATER: case POTION_POLYMORPH: case POTION_BOOZE: case POTION_JUICE: case POTION_ACID: case POTION_INVISIBILITY: - if ( clientLearnedAlchemyIngredients[gui_player].find(item->type) + if ( clientLearnedAlchemyIngredients[gui_player].find(item->type) != clientLearnedAlchemyIngredients[gui_player].end() ) { return true; @@ -7122,19 +8457,19 @@ bool GenericGUIMenu::isItemMixable(const Item* item) default: return false; break; + } } - } - if ( !secondaryPotion ) - { - if ( item == basePotion ) + if ( !secondaryPotion ) { - return false; - } + if ( item == basePotion ) + { + return false; + } - // we're selecting the second potion. - switch ( item->type ) - { + // we're selecting the second potion. + switch ( item->type ) + { case POTION_WATER: case POTION_SICKNESS: case POTION_CONFUSION: @@ -7143,7 +8478,7 @@ bool GenericGUIMenu::isItemMixable(const Item* item) case POTION_RESTOREMAGIC: case POTION_SPEED: case POTION_POLYMORPH: - if ( clientLearnedAlchemyIngredients[gui_player].find(item->type) + if ( clientLearnedAlchemyIngredients[gui_player].find(item->type) != clientLearnedAlchemyIngredients[gui_player].end() ) { return true; @@ -7156,12 +8491,232 @@ bool GenericGUIMenu::isItemMixable(const Item* item) default: return false; break; + } } } return false; } +int GenericGUIMenu::isItemRationSeasoning(int type) +{ + if ( type >= WOODEN_SHIELD && type < NUMITEMS ) + { + if ( items[type].hasAttribute("SEASONING_FOOD") ) + { + if ( isItemRation(items[type].attributes["SEASONING_FOOD"]) ) + { + return items[type].attributes["SEASONING_FOOD"]; + } + } + } + + return 0; +} + +bool GenericGUIMenu::isItemRation(int type) +{ + switch ( type ) + { + case FOOD_RATION: + case FOOD_RATION_SPICY: + case FOOD_RATION_SOUR: + case FOOD_RATION_BITTER: + case FOOD_RATION_HEARTY: + case FOOD_RATION_HERBAL: + case FOOD_RATION_SWEET: + return true; + default: + break; + } + return false; +} + +bool GenericGUIMenu::AlchemyGUI_t::alchemyMissingIngredientQty(Item* item) +{ + if ( currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + if ( potion1Uid && potion2Uid && alchemyResultPotion.type != POTION_EMPTY ) + { + if ( !item ) + { + if ( torchCount.count >= 0 && torchCount.count < 4 ) + { + return true; + } + if ( alchemyResultPotion.appearance != 0 ) + { + return true; + } + } + else if ( item && item->uid == potion1Uid ) + { + if ( (alchemyResultPotion.appearance & 0xFF) != 0 ) + { + return true; + } + } + else if ( item && item->uid == potion2Uid ) + { + if ( ((alchemyResultPotion.appearance >> 8) & 0xFF) != 0 ) + { + return true; + } + } + } + } + + return false; +} + +ItemType alchemyCookResult(int player, Item* potion1Item, Item* potion2Item, int& outCreateCount, Status& statusOut, + int& outMissingPotion1Count, int& outMissingPotion2Count) +{ + ItemType result = POTION_EMPTY; + outCreateCount = 1; + outMissingPotion1Count = 0; + outMissingPotion2Count = 0; + statusOut = SERVICABLE; + if ( !potion1Item || !potion2Item ) + { + return result; + } + + ItemType potion1 = potion1Item->type; + ItemType potion2 = potion2Item->type; + + if ( (potion1 == TOOL_TOWEL && potion2 == FOOD_RATION) + || (potion2 == TOOL_TOWEL && potion1 == FOOD_RATION) ) + { + result = GREASE_BALL; + + if ( potion1 == TOOL_TOWEL ) + { + outCreateCount = 4 * (int)(potion1Item->status); + } + else if ( potion2 == TOOL_TOWEL ) + { + outCreateCount = 4 * (int)(potion2Item->status); + } + if ( potion1 == FOOD_RATION ) + { + if ( potion1Item->count < 4 ) + { + outMissingPotion1Count = 4 - potion1Item->count; + } + } + else if ( potion2 == FOOD_RATION ) + { + if ( potion2Item->count < 4 ) + { + outMissingPotion2Count = 4 - potion2Item->count; + } + } + statusOut = WORN; + } + else if ( (potion1 == POTION_WATER && potion2 == FOOD_RATION) + || (potion2 == POTION_WATER && potion1 == FOOD_RATION) ) + { + result = SLOP_BALL; + if ( potion1 == FOOD_RATION ) + { + if ( potion1Item->count < 4 ) + { + outMissingPotion1Count = 4 - potion1Item->count; + } + } + else if ( potion2 == FOOD_RATION ) + { + if ( potion2Item->count < 4 ) + { + outMissingPotion2Count = 4 - potion2Item->count; + } + } + outCreateCount = 4; + statusOut = WORN; + } + else if ( (potion1 == FOOD_RATION && GenericGUIMenu::isItemRationSeasoning(potion2)) + || (potion2 == FOOD_RATION && GenericGUIMenu::isItemRationSeasoning(potion1)) ) + { + if ( GenericGUIMenu::isItemRationSeasoning(potion2) ) + { + result = static_cast(GenericGUIMenu::isItemRationSeasoning(potion2)); + } + else if ( GenericGUIMenu::isItemRationSeasoning(potion1) ) + { + result = static_cast(GenericGUIMenu::isItemRationSeasoning(potion1)); + } + + if ( result != POTION_EMPTY ) + { + if ( potion1 == FOOD_RATION ) + { + if ( potion1Item->count < 4 ) + { + outMissingPotion1Count = 4 - potion1Item->count; + } + statusOut = potion1Item->status; + } + else if ( potion2 == FOOD_RATION ) + { + if ( potion2Item->count < 4 ) + { + outMissingPotion2Count = 4 - potion2Item->count; + } + statusOut = potion2Item->status; + } + + int skillLVL = (stats[player]->getModifiedProficiency(PRO_ALCHEMY) + statGetPER(stats[player], players[player]->entity)) / 20; + skillLVL = std::max(0, std::min(skillLVL, 5)); + outCreateCount = 1 + skillLVL; + if ( skillLVL >= 4 ) + { + statusOut = EXCELLENT; + } + if ( player >= 0 && stats[player] && stats[player]->helmet && stats[player]->helmet->type == HAT_CHEF ) + { + statusOut = EXCELLENT; + } + } + } + else if ( items[potion1].category == FOOD && !GenericGUIMenu::isItemRation(potion1) + && items[potion2].category == FOOD && !GenericGUIMenu::isItemRation(potion2) ) + { + int satiation = Item::getBaseFoodSatiation(potion1) + Item::getBaseFoodSatiation(potion2); + if ( potion1 == FOOD_TIN || potion2 == FOOD_TIN ) + { + satiation += 600; // bonus rations + } + if ( satiation >= 200 ) + { + result = FOOD_RATION; + int count = satiation / 200; + int skillLVL = (stats[player]->getModifiedProficiency(PRO_ALCHEMY) + statGetPER(stats[player], players[player]->entity)) / 20; + skillLVL = std::max(0, std::min(skillLVL, 5)); + if ( skillLVL >= 4 ) + { + count += 1; + } + if ( satiation % 200 > 0 && skillLVL >= 1 ) + { + count += 1; + } + count = std::max(1, count); + outCreateCount = count; + if ( skillLVL >= 4 ) + { + statusOut = EXCELLENT; + } + if ( player >= 0 && stats[player] && stats[player]->helmet && stats[player]->helmet->type == HAT_CHEF ) + { + statusOut = EXCELLENT; + } + } + } + + return result; +} + ItemType alchemyMixResult(ItemType potion1, ItemType potion2, bool& outRandomResult, bool& outTryDuplicatePotion, bool& outSamePotion, bool& outExplodeSelf) { @@ -7527,6 +9082,426 @@ bool alchemyAddRecipe(int player, int basePotion, int secondaryPotion, int resul return false; } +void GenericGUIMenu::alchemyCookCombination() +{ + if ( !basePotion || !secondaryPotion ) + { + return; + } + + const ItemType basePotionType = basePotion->type; + const ItemType secondaryPotionType = secondaryPotion->type; + int createCount = 1; + Status status = SERVICABLE; + int missingPotion1Count = 0; + int missingPotion2Count = 0; + ItemType result = alchemyCookResult(getPlayer(), basePotion, secondaryPotion, createCount, status, missingPotion1Count, missingPotion2Count); + if ( result == POTION_EMPTY || alchemyGUI.alchemyMissingIngredientQty(nullptr) ) + { + return; + } + + int skillLVL = 0; + if ( stats[gui_player] ) + { + skillLVL = stats[gui_player]->getModifiedProficiency(PRO_ALCHEMY) / 20; // 0 to 5; + } + + + if ( basePotionType == TOOL_TOWEL ) + { + messagePlayerColor(gui_player, MESSAGE_INVENTORY, uint32ColorWhite, Language::get(6770), + items[secondaryPotionType].getIdentifiedName(), items[basePotionType].getIdentifiedName()); + } + else if ( secondaryPotionType == TOOL_TOWEL ) + { + messagePlayerColor(gui_player, MESSAGE_INVENTORY, uint32ColorWhite, Language::get(6770), + items[basePotionType].getIdentifiedName(), items[secondaryPotionType].getIdentifiedName()); + } + else if ( GenericGUIMenu::isItemRationSeasoning(basePotionType) || itemCategory(basePotion) == POTION ) + { + messagePlayerColor(gui_player, MESSAGE_INVENTORY, uint32ColorWhite, Language::get(3332), + items[secondaryPotionType].getIdentifiedName(), items[basePotionType].getIdentifiedName()); + } + else if ( GenericGUIMenu::isItemRationSeasoning(secondaryPotionType) || itemCategory(secondaryPotion) == POTION ) + { + messagePlayerColor(gui_player, MESSAGE_INVENTORY, uint32ColorWhite, Language::get(3332), + items[basePotionType].getIdentifiedName(), items[secondaryPotionType].getIdentifiedName()); + } + else + { + messagePlayerColor(gui_player, MESSAGE_INVENTORY, uint32ColorWhite, Language::get(6769), + items[basePotionType].getIdentifiedName(), items[secondaryPotionType].getIdentifiedName()); + } + + if ( GenericGUIMenu::isItemRationSeasoning(basePotionType) && secondaryPotionType == FOOD_RATION ) + { + status = std::max(status, secondaryPotion->status); + } + else if ( GenericGUIMenu::isItemRationSeasoning(secondaryPotionType) && basePotionType == FOOD_RATION ) + { + status = std::max(status, basePotion->status); + } + + bool degradeAlembic = false; + /*if ( (basePotion->type == POTION_ACID || secondaryPotion->type == POTION_ACID) && !samePotion ) + { + if ( local_rng.rand() % 10 == 0 ) + { + degradeAlembic = true; + } + } + else + { + if ( local_rng.rand() % 40 == 0 ) + { + degradeAlembic = true; + } + }*/ + + int appearance = 0; + int blessing = 0; + if ( isItemRation(result) ) + { + if ( basePotion->beatitude > 0 && secondaryPotion->beatitude > 0 ) + { + blessing = std::min(basePotion->beatitude, secondaryPotion->beatitude); // take least blessed + } + else if ( basePotion->beatitude < 0 && secondaryPotion->beatitude < 0 ) + { + blessing = std::min(basePotion->beatitude, secondaryPotion->beatitude); // take most cursed + } + else if ( (basePotion->beatitude < 0 && secondaryPotion->beatitude > 0) + || (secondaryPotion->beatitude < 0 && basePotion->beatitude > 0) ) + { + blessing = 0; + } + else if ( basePotion->beatitude < 0 && secondaryPotion->beatitude == 0 ) + { + blessing = basePotion->beatitude; // curse the result + } + else if ( basePotion->beatitude == 0 && secondaryPotion->beatitude < 0 ) + { + blessing = secondaryPotion->beatitude; // curse the result + } + else if ( basePotion->beatitude > 0 && secondaryPotion->beatitude == 0 ) + { + blessing = 0; // negate the blessing + } + else if ( basePotion->beatitude == 0 && secondaryPotion->beatitude > 0 ) + { + blessing = 0; // negate the blessing + } + } + + bool emptyBottle = false; + { + int consume1 = 1; + int consume2 = 1; + if ( basePotionType == FOOD_RATION ) + { + consume1 = 4; + } + if ( secondaryPotionType == FOOD_RATION ) + { + consume2 = 4; + } + while ( consume1 > 0 ) + { + int prevCount = basePotion->count; + consumeItem(basePotion, gui_player); + if ( !basePotion || basePotion->count == prevCount ) + { + break; + } + --consume1; + } + while ( consume2 > 0 ) + { + int prevCount = secondaryPotion->count; + consumeItem(secondaryPotion, gui_player); + if ( !secondaryPotion || secondaryPotion->count == prevCount ) + { + break; + } + --consume2; + } + + if ( alchemyGUI.torchCount.count >= 0 ) + { + std::vector torches; + Item* torchesEquipped; + for ( node_t* invnode = stats[gui_player]->inventory.first; invnode != NULL; invnode = invnode->next ) + { + Item* item = (Item*)invnode->element; + if ( item && item->type == TOOL_TORCH ) + { + if ( itemIsEquipped(item, gui_player) ) + { + torchesEquipped = item; + } + else + { + torches.push_back(item); + } + } + } + int consumeTorch = 4; + bool updateEquipSlot = false; + while ( consumeTorch > 0 ) + { + if ( torches.size() == 0 && !torchesEquipped ) + { + break; + } + + Item* torch = nullptr; + if ( torches.size() > 0 ) + { + torch = torches[0]; + torches.erase(torches.begin()); + + while ( torch && consumeTorch > 0 ) + { + consumeItem(torch, gui_player); + --consumeTorch; + } + continue; + } + if ( torchesEquipped ) + { + while ( torchesEquipped && consumeTorch > 0 ) + { + consumeItem(torchesEquipped, gui_player); + updateEquipSlot = true; + --consumeTorch; + } + continue; + } + } + + if ( updateEquipSlot && multiplayer == CLIENT ) + { + // if decrementing qty and holding item, then send "equip" for server to update their count of your held item. + strcpy((char*)net_packet->data, "COOK"); + Item* shield = stats[gui_player]->shield; + SDLNet_Write32(static_cast(shield ? shield->type : TOOL_TORCH), &net_packet->data[4]); + SDLNet_Write32(static_cast(shield ? shield->status : BROKEN), &net_packet->data[8]); + SDLNet_Write32(static_cast(shield ? shield->beatitude : 0), &net_packet->data[12]); + SDLNet_Write32(static_cast(shield ? shield->count : 0), &net_packet->data[16]); + SDLNet_Write32(static_cast(shield ? shield->appearance : 0), &net_packet->data[20]); + net_packet->data[24] = shield ? shield->identified : false; + net_packet->data[25] = gui_player; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 26; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + } + + if ( items[basePotionType].category == POTION || items[secondaryPotionType].category == POTION ) + { + if ( local_rng.rand() % 100 < (50 + skillLVL * 5) ) // 50 - 75% chance + { + emptyBottle = true; + } + } + } + + /*if ( skillCapstoneUnlocked(gui_player, PRO_ALCHEMY) ) + { + degradeAlembic = false; + } + + if ( degradeAlembic && alembicItem ) + { + alembicItem->status = static_cast(alembicItem->status - 1); + if ( alembicItem->status > BROKEN ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(681), alembicItem->getName()); + } + else + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(2351), alembicItem->getName()); + playSoundPlayer(gui_player, 162, 64); + consumeItem(alembicItem, gui_player); + alembicItem = nullptr; + } + }*/ + + //if ( explodeSelf && players[gui_player] && players[gui_player]->entity ) + //{ + // Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_EXPLOSIONS, TOOL_ALEMBIC, 1); + + // // hurt. + // alchemyAddRecipe(gui_player, basePotionType, secondaryPotionType, TOOL_BOMB, true); + // if ( multiplayer == CLIENT ) + // { + // strcpy((char*)net_packet->data, "BOOM"); + // net_packet->data[4] = gui_player; + // net_packet->address.host = net_server.host; + // net_packet->address.port = net_server.port; + // net_packet->len = 5; + // sendPacketSafe(net_sock, -1, net_packet, 0); + // } + // else + // { + // bool protection = false; + // if ( stats[gui_player]->mask && stats[gui_player]->mask->type == MASK_HAZARD_GOGGLES ) + // { + // bool shapeshifted = false; + // if ( stats[gui_player]->type != HUMAN ) + // { + // if ( players[gui_player]->entity->effectShapeshift != NOTHING ) + // { + // shapeshifted = true; + // } + // } + // if ( !shapeshifted ) + // { + // protection = true; + // messagePlayerColor(gui_player, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6089)); + // } + // } + // spawnMagicTower(protection ? players[gui_player]->entity : nullptr, + // players[gui_player]->entity->x, players[gui_player]->entity->y, SPELL_FIREBALL, nullptr); + // players[gui_player]->entity->setObituary(Language::get(3350)); + // stats[gui_player]->killer = KilledBy::FAILED_ALCHEMY; + // } + // closeGUI(); + // return; + //} + + appearance = std::max(0, appearance); + if ( Item* newPotion = newItem(result, status, blessing, createCount, appearance, true, nullptr) ) + { + messagePlayer(gui_player, MESSAGE_MISC, Language::get(6771), newPotion->description()); + Item* pickedUp = itemPickup(gui_player, newPotion); + if ( pickedUp ) + { + alchemyGUI.potionResultUid = pickedUp->uid; + alchemyGUI.animPotionResultCount = alchemyGUI.alchemyResultPotion.count; + } + free(newPotion); + newPotion = nullptr; + } + + if ( players[gui_player] && players[gui_player]->entity ) + { + playSoundEntityLocal(players[gui_player]->entity, 774 + local_rng.rand() % 2, 64); + } + if ( emptyBottle ) + { + Item* emptyBottle = newItem(POTION_EMPTY, SERVICABLE, 0, 1, 0, true, nullptr); + itemPickup(gui_player, emptyBottle); + messagePlayer(gui_player, MESSAGE_MISC, Language::get(3351), items[POTION_EMPTY].getIdentifiedName()); + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_BOTTLE_FROM_BREWING, POTION_EMPTY, 1); + free(emptyBottle); + } + + if ( result == SLOP_BALL ) + { + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_COOK_SLOP_BALLS, "cauldron", createCount); + steamStatisticUpdate(STEAM_STAT_WITCHES_BREW, STEAM_STAT_INT, 1); + } + if ( alembicItem && alembicItem->type == TOOL_FRYING_PAN ) + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_COOK_SLOP_BALLS, TOOL_FRYING_PAN, createCount); + } + } + else if ( result == GREASE_BALL ) + { + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_COOK_GREASE_BALLS, "cauldron", createCount); + steamStatisticUpdate(STEAM_STAT_WITCHES_BREW, STEAM_STAT_INT, 1); + } + if ( alembicItem && alembicItem->type == TOOL_FRYING_PAN ) + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_COOK_GREASE_BALLS, TOOL_FRYING_PAN, createCount); + } + } + + if ( isItemRation(result) ) + { + if ( result == FOOD_RATION ) + { + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_COOK_MEALS, "cauldron", createCount); + steamStatisticUpdate(STEAM_STAT_WITCHES_BREW, STEAM_STAT_INT, 1); + } + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_COOK_MEALS, result, createCount); + if ( alembicItem && alembicItem->type == TOOL_FRYING_PAN ) + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_COOK_MEALS, TOOL_FRYING_PAN, createCount); + } + } + else + { + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_COOK_FLAVORED_MEALS, "cauldron", createCount); + steamStatisticUpdate(STEAM_STAT_WITCHES_BREW, STEAM_STAT_INT, 1); + } + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_COOK_MEALS, result, createCount); + if ( alembicItem && alembicItem->type == TOOL_FRYING_PAN ) + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_COOK_FLAVORED_MEALS, TOOL_FRYING_PAN, createCount); + } + } + if ( stats[gui_player]->type == VAMPIRE || stats[gui_player]->type == AUTOMATON + || stats[gui_player]->type == SKELETON ) + { + steamAchievement("BARONY_ACH_CULINARY_AMBASSADOR"); + } + steamStatisticUpdate(STEAM_STAT_LET_HIM_COOK, STEAM_STAT_INT, createCount); + + bool raiseSkill = false; + if ( result == FOOD_RATION && local_rng.rand() % 10 == 0 && stats[gui_player] + && stats[gui_player]->getProficiency(PRO_ALCHEMY) < SKILL_LEVEL_SKILLED ) + { + raiseSkill = true; + } + else if ( local_rng.rand() % 5 == 0 ) + { + raiseSkill = true; + } + + if ( raiseSkill ) + { + if ( multiplayer == CLIENT ) + { + // request level up + strcpy((char*)net_packet->data, "CSKL"); + net_packet->data[4] = gui_player; + net_packet->data[5] = PRO_ALCHEMY; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + else + { + if ( players[gui_player] && players[gui_player]->entity ) + { + players[gui_player]->entity->increaseSkill(PRO_ALCHEMY); + } + } + + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + if ( stats[gui_player]->getProficiency(PRO_ALCHEMY) < 100 && alchemyGUI.bOpen ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_CAULDRON_SKILLUPS, "cauldron", 1); + } + } + } + } +} + void GenericGUIMenu::alchemyCombinePotions() { if ( !basePotion || !secondaryPotion ) @@ -8113,7 +10088,14 @@ void GenericGUIMenu::alchemyCombinePotions() if ( explodeSelf && players[gui_player] && players[gui_player]->entity ) { - Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_EXPLOSIONS, TOOL_ALEMBIC, 1); + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_ALEMBIC_EXPLOSIONS, "cauldron", 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_EXPLOSIONS, TOOL_ALEMBIC, 1); + } // hurt. alchemyAddRecipe(gui_player, basePotionType, secondaryPotionType, TOOL_BOMB, true); @@ -8198,7 +10180,14 @@ void GenericGUIMenu::alchemyCombinePotions() if ( result == POTION_WATER && !duplicateSucceed ) { messagePlayer(gui_player, MESSAGE_MISC, Language::get(3356)); - Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_DUPLICATION_FAIL, TOOL_ALEMBIC, 1); + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_ALEMBIC_DUPLICATION_FAIL, "cauldron", 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_DUPLICATION_FAIL, TOOL_ALEMBIC, 1); + } } else if ( newPotion ) { @@ -8212,7 +10201,14 @@ void GenericGUIMenu::alchemyCombinePotions() messagePlayer(gui_player, MESSAGE_MISC, Language::get(3352), newPotion->description()); if ( duplicateSucceed ) { - Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_DUPLICATED, TOOL_ALEMBIC, 1); + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_ALEMBIC_DUPLICATED, "cauldron", 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_DUPLICATED, TOOL_ALEMBIC, 1); + } } } } @@ -8222,11 +10218,26 @@ void GenericGUIMenu::alchemyCombinePotions() steamStatisticUpdate(STEAM_STAT_IN_THE_MIX, STEAM_STAT_INT, 1); if ( samePotion ) { - Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_DECANTED, TOOL_ALEMBIC, 1); + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_ALEMBIC_DECANTED, "cauldron", 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_DECANTED, TOOL_ALEMBIC, 1); + } } else { - Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_BREWED, TOOL_ALEMBIC, 1); + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_ALEMBIC_BREWED, "cauldron", 1); + steamStatisticUpdate(STEAM_STAT_WITCHES_BREW, STEAM_STAT_INT, 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_ALEMBIC_BREWED, TOOL_ALEMBIC, 1); + } if ( result != POTION_SICKNESS && result != POTION_WATER ) { @@ -8326,6 +10337,13 @@ void GenericGUIMenu::alchemyCombinePotions() players[gui_player]->entity->increaseSkill(PRO_ALCHEMY); } } + if ( stats[gui_player]->getProficiency(PRO_ALCHEMY) < 100 && alchemyGUI.bOpen ) + { + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_CAULDRON_SKILLUPS, "cauldron", 1); + } + } } break; } @@ -8402,6 +10420,14 @@ bool GenericGUIMenu::alchemyLearnRecipe(int type, bool increaseskill, bool notif players[gui_player]->entity->increaseSkill(PRO_ALCHEMY); } } + + if ( stats[gui_player]->getProficiency(PRO_ALCHEMY) < 100 && alchemyGUI.bOpen ) + { + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_CAULDRON_SKILLUPS, "cauldron", 1); + } + } } gameStatistics[STATISTICS_ALCHEMY_RECIPES] |= (1 << index); return true; @@ -8430,6 +10456,13 @@ bool GenericGUIMenu::alchemyLearnRecipe(int type, bool increaseskill, bool notif players[gui_player]->entity->increaseSkill(PRO_ALCHEMY); } } + if ( stats[gui_player]->getProficiency(PRO_ALCHEMY) < 100 && alchemyGUI.bOpen ) + { + if ( alembicEntityUid != 0 && uidToEntity(alembicEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_CAULDRON_SKILLUPS, "cauldron", 1); + } + } } } break; @@ -8683,7 +10716,15 @@ bool GenericGUIMenu::tinkeringCraftItem(Item* item) { Item* pickedUp = itemPickup(gui_player, crafted); messagePlayer(gui_player, MESSAGE_MISC, Language::get(3668), crafted->description()); - Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_TINKERKIT_CRAFTS, TOOL_TINKERING_KIT, 1); + if ( workstationEntityUid != 0 && uidToEntity(workstationEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_WORKBENCH_CRAFTS, "workbench", 1); + steamStatisticUpdate(STEAM_STAT_HOBBYIST, STEAM_STAT_INT, 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_TINKERKIT_CRAFTS, TOOL_TINKERING_KIT, 1); + } Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_GADGET_CRAFTED, crafted->type, 1); free(crafted); return true; @@ -8969,12 +11010,27 @@ bool GenericGUIMenu::tinkeringSalvageItem(Item* item, bool outsideInventory, int players[player]->entity->increaseSkill(PRO_LOCKPICKING); } } + + if ( stats[gui_player]->getProficiency(PRO_LOCKPICKING) < 100 && tinkerGUI.bOpen ) + { + if ( workstationEntityUid != 0 && uidToEntity(workstationEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_WORKBENCH_SKILLUPS, "workbench", 1); + } + } } } if ( players[player]->isLocalPlayer() && didCraft ) { - Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_TINKERKIT_SALVAGED, TOOL_TINKERING_KIT, 1); + if ( workstationEntityUid != 0 && uidToEntity(workstationEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_WORKBENCH_SALVAGE, "workbench", 1); + } + else + { + Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_TINKERKIT_SALVAGED, TOOL_TINKERING_KIT, 1); + } if ( item && item->type == TOOL_DETONATOR_CHARGE ) { Compendium_t::Events_t::eventUpdate(gui_player, Compendium_t::CPDM_DETONATOR_SCRAPPED, TOOL_DETONATOR_CHARGE, 1); @@ -9171,6 +11227,14 @@ Item* GenericGUIMenu::tinkeringCraftItemAndConsumeMaterials(const Item* item) players[gui_player]->entity->increaseSkill(PRO_LOCKPICKING); } } + + if ( stats[gui_player]->getProficiency(PRO_LOCKPICKING) < 100 && tinkerGUI.bOpen ) + { + if ( workstationEntityUid != 0 && uidToEntity(workstationEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_WORKBENCH_SKILLUPS, "workbench", 1); + } + } } if ( (ticks - tinkeringSfxLastTicks) > 100 ) @@ -9453,6 +11517,21 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case CLOAK_SILVER: case TOOL_LOCKPICK: case MASK_EYEPATCH: + case TOOL_BEARTRAP: + case TOOL_GLASSES: + case TOOL_LANTERN: + case BONE_AXE: + case BONE_MACE: + case BONE_SHORTBOW: + case BONE_SPEAR: + case BONE_SWORD: + case BONE_THROWING: + case HOOD_TEAL: + case HAT_FELT: + case BANDIT_BREASTPIECE: + case TUNIC_BLOUSE: + case BOLAS: + case LOAFERS: *metal = 1; *magic = 0; break; @@ -9480,6 +11559,7 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case HAT_SILKEN_BOW: case HAT_BANDANA: case HAT_CHEF: + case SHAWL: *metal = 1; *magic = 1; break; @@ -9501,6 +11581,12 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case GEM_AMETHYST: case GEM_FLUORITE: case MASK_PIPE: + case CLEAT_BOOTS: + case BONE_BREASTPIECE: + case BONE_BRACERS: + case BONE_BOOTS: + case BONE_SHIELD: + case BONE_HELM: *metal = 1; *magic = 2; break; @@ -9517,6 +11603,8 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case GEM_AQUAMARINE: case GEM_OPAL: case TOOL_BLINDFOLD_FOCUS: + case CLOAK_DENDRITE: + case AMULET_BURNINGRESIST: *metal = 1; *magic = 3; break; @@ -9530,6 +11618,7 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case RING_REGENERATION: case RING_RESOLVE: case GEM_DIAMOND: + case GEM_JEWEL: case TOOL_SKELETONKEY: case VAMPIRE_DOUBLET: case MAGICSTAFF_CHARM: @@ -9538,10 +11627,16 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case MAGICSTAFF_SUMMON: case MASK_SHAMAN: case HAT_CIRCLET: + case HAT_CIRCLET_SORCERY: + case HAT_CIRCLET_THAUMATURGY: case HAT_LAURELS: case HAT_HOOD_APPRENTICE: case HAT_HOOD_WHISPERS: case HAT_HOOD_ASSASSIN: + case ROBE_CULTIST: + case ROBE_HEALER: + case ROBE_MONK: + case ROBE_WIZARD: *metal = 1; *magic = 4; break; @@ -9563,26 +11658,72 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma *magic = 2; break; + case BRANCH_BOW: + case BRANCH_BOW_INFECTED: + case BRANCH_STAFF: + *metal = 0; + *magic = 3; + break; + case SCROLL_IDENTIFY: case SCROLL_REMOVECURSE: case SCROLL_FOOD: case SCROLL_SUMMON: + case SPELLBOOK_FORCEBOLT: case SPELLBOOK_LIGHT: + case SPELLBOOK_CONFUSE: case SPELLBOOK_SLOW: - case SPELLBOOK_LOCKING: - case SPELLBOOK_TELEPORTATION: case SPELLBOOK_REVERT_FORM: case SPELLBOOK_RAT_FORM: - case SPELLBOOK_SPRAY_WEB: case SPELLBOOK_POISON: - case SPELLBOOK_SPEED: - case SPELLBOOK_DETECT_FOOD: - case SPELLBOOK_SHADOW_TAG: case SPELLBOOK_SALVAGE: case SPELLBOOK_DASH: + case SPELLBOOK_HEAL_OTHER: + case SPELLBOOK_BLOOD_WARD: + case SPELLBOOK_MAXIMISE: + case SPELLBOOK_SPIRIT_WEAPON: + case SPELLBOOK_FIRE_SPRITE: + case SPELLBOOK_IGNITE: + case SPELLBOOK_SHATTER_OBJECTS: + case SPELLBOOK_HEAL_MINOR: + case SPELLBOOK_LIGHTNING: + case SPELLBOOK_SLEEP: + case SPELLBOOK_OPENING: + case SPELLBOOK_LOCKING: + case SPELLBOOK_CUREAILMENT: + case SPELLBOOK_ACID_SPRAY: + case SPELLBOOK_SPRAY_WEB: + case SPELLBOOK_SHADOW_TAG: + case SPELLBOOK_GUARD_BODY: + case SPELLBOOK_GUARD_SPIRIT: + case SPELLBOOK_DONATION: + case SPELLBOOK_SCRY_ALLIES: + case SPELLBOOK_DETECT_ENEMY: + case SPELLBOOK_TURN_UNDEAD: + case SPELLBOOK_MINIMISE: + case SPELLBOOK_ENVENOM_WEAPON: + case SPELLBOOK_DEFY_FLESH: + case SPELLBOOK_RESHAPE_WEAPON: + case SPELLBOOK_ALTER_ARROW: + case SPELLBOOK_LEAD_BOLT: + case SPELLBOOK_COWARDICE: + case SPELLBOOK_SEEK_ALLY: + case SPELLBOOK_SPORES: + case SPELLBOOK_WINDGATE: + case SPELLBOOK_ATTRACT_ITEMS: + case SPELLBOOK_SPIN: + case SPELLBOOK_CLEANSE_FOOD: + case SPELLBOOK_FLAME_CLOAK: + case SPELLBOOK_DISRUPT_EARTH: + case SPELLBOOK_THORNS: + case SPELLBOOK_SIGIL: + case SPELLBOOK_9: case SPELLBOOK_10: + case TOME_SORCERY: + case TOME_MYSTICISM: + case TOME_THAUMATURGY: *metal = 0; *magic = 4; break; @@ -9600,49 +11741,94 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case SCROLL_ENCHANTWEAPON: case SCROLL_ENCHANTARMOR: - case SPELLBOOK_COLD: + case SPELLBOOK_FIREBALL: + case SPELLBOOK_INVISIBILITY: + case SPELLBOOK_HEALING: + case SPELLBOOK_DIG: + case SPELLBOOK_BLEED: + case SPELLBOOK_SPIDER_FORM: + case SPELLBOOK_SPEED: + case SPELLBOOK_DETECT_FOOD: + case SPELLBOOK_WEAKNESS: + case SPELLBOOK_TELEPULL: + case SPELLBOOK_TROLLS_BLOOD: + case SPELLBOOK_PROF_NIMBLENESS: + case SPELLBOOK_PROF_COUNSEL: + case SPELLBOOK_BLESS_FOOD: + case SPELLBOOK_DIVINE_ZEAL: + case SPELLBOOK_INCOHERENCE: + case SPELLBOOK_OVERCHARGE: + case SPELLBOOK_GREASE_SPRAY: + case SPELLBOOK_FORGE_KEY: + case SPELLBOOK_DEEP_SHADE: + case SPELLBOOK_TELEKINESIS: + case SPELLBOOK_DISARM: + case SPELLBOOK_ABUNDANCE: + case SPELLBOOK_FORCE_SHIELD: + case SPELLBOOK_SPLINTER_GEAR: + case SPELLBOOK_FIRE_WALL: + case SPELLBOOK_MAGICIANS_ARMOR: + case SPELLBOOK_MAGICMISSILE: + case SPELLBOOK_COLD: case SPELLBOOK_REMOVECURSE: - case SPELLBOOK_LIGHTNING: case SPELLBOOK_IDENTIFY: case SPELLBOOK_MAGICMAPPING: - case SPELLBOOK_SLEEP: - case SPELLBOOK_CONFUSE: - case SPELLBOOK_OPENING: - case SPELLBOOK_HEALING: - case SPELLBOOK_CUREAILMENT: - case SPELLBOOK_ACID_SPRAY: - case SPELLBOOK_CHARM_MONSTER: - case SPELLBOOK_SPIDER_FORM: - case SPELLBOOK_TROLL_FORM: + case SPELLBOOK_TELEPORTATION: + case SPELLBOOK_VAMPIRIC_AURA: case SPELLBOOK_FEAR: - case SPELLBOOK_STRIKE: - case SPELLBOOK_TELEPULL: case SPELLBOOK_FLUTTER: + case SPELLBOOK_SELF_POLYMORPH: + case SPELLBOOK_DIVINE_GUARD: + case SPELLBOOK_PROF_GREATER_MIGHT: + case SPELLBOOK_PROF_STURDINESS: + case SPELLBOOK_PINPOINT: + case SPELLBOOK_SCRY_TRAPS: + case SPELLBOOK_PSYCHIC_SPEAR: + case SPELLBOOK_COMMAND: + case SPELLBOOK_METALLURGY: + case SPELLBOOK_VOID_CHEST: + case SPELLBOOK_NUMBING_BOLT: + case SPELLBOOK_CURSE_FLESH: + case SPELLBOOK_PRESERVE: + case SPELLBOOK_SABOTAGE: + case SPELLBOOK_NULL_AREA: + case SPELLBOOK_SLAM: + case SPELLBOOK_KINETIC_FIELD: + case SPELLBOOK_TROLL_FORM: + case SCROLL_CHARGING: case SCROLL_CONJUREARROW: + case MAGICSTAFF_SCEPTER: *metal = 0; *magic = 6; break; - case SPELLBOOK_MAGICMISSILE: + case SPELLBOOK_CHARM_MONSTER: case SPELLBOOK_LEVITATION: - case SPELLBOOK_INVISIBILITY: case SPELLBOOK_EXTRAHEALING: - case SPELLBOOK_DIG: - case SPELLBOOK_SUMMON: - case SPELLBOOK_BLEED: - case SPELLBOOK_REFLECT_MAGIC: case SPELLBOOK_STONEBLOOD: + case SPELLBOOK_REFLECT_MAGIC: + case SPELLBOOK_STRIKE: + case SPELLBOOK_AMPLIFY_MAGIC: + case SPELLBOOK_METEOR: + case SPELLBOOK_ICE_WAVE: + case SPELLBOOK_SCRY_TREASURES: + case SPELLBOOK_BLOOD_WAVES: + case SPELLBOOK_MIST_FORM: + case SPELLBOOK_ABSORB_MAGIC: + case SPELLBOOK_TUNNEL: + case SPELLBOOK_LIGHTNING_BOLT: + case SPELLBOOK_SANCTUARY: + case SPELLBOOK_HOLY_BEAM: + case SPELLBOOK_DOMINATE: + + case SPELLBOOK_SUMMON: case SPELLBOOK_STEAL_WEAPON: case SPELLBOOK_DRAIN_SOUL: - case SPELLBOOK_VAMPIRIC_AURA: case SPELLBOOK_IMP_FORM: - case SPELLBOOK_TROLLS_BLOOD: - case SPELLBOOK_WEAKNESS: - case SPELLBOOK_AMPLIFY_MAGIC: case SPELLBOOK_DEMON_ILLU: - case SPELLBOOK_SELF_POLYMORPH: + case GEM_LUCK: case ENCHANTED_FEATHER: *metal = 0; @@ -9660,13 +11846,16 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case LEATHER_BREASTPIECE: case IRON_HELM: case TOOL_PICKAXE: - case TOOL_LANTERN: - case TOOL_GLASSES: case IRON_KNUCKLES: - case TOOL_BEARTRAP: case IRON_DAGGER: case MONOCLE: case MASK_BANDIT: + case TOOL_FRYING_PAN: + case IRON_PAULDRONS: + case QUILTED_GAMBESON: + case QUILTED_GLOVES: + case QUILTED_BOOTS: + case QUILTED_CAP: *metal = 2; *magic = 0; break; @@ -9696,6 +11885,26 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma break; case MIRROR_SHIELD: + case TOOL_FOCI_FIRE: + case TOOL_FOCI_SNOW: + case TOOL_FOCI_NEEDLES: + case TOOL_FOCI_ARCS: + case TOOL_FOCI_SAND: + case TOOL_FOCI_DARK_LIFE: + case TOOL_FOCI_DARK_RIFT: + case TOOL_FOCI_DARK_SILENCE: + case TOOL_FOCI_DARK_VENGEANCE: + case TOOL_FOCI_DARK_SUPPRESS: + case TOOL_FOCI_LIGHT_PEACE: + case TOOL_FOCI_LIGHT_JUSTICE: + case TOOL_FOCI_LIGHT_PROVIDENCE: + case TOOL_FOCI_LIGHT_PURITY: + case TOOL_FOCI_LIGHT_SANCTUARY: + case INSTRUMENT_FLUTE: + case INSTRUMENT_LYRE: + case INSTRUMENT_DRUM: + case INSTRUMENT_LUTE: + case INSTRUMENT_HORN: *metal = 2; *magic = 4; break; @@ -9716,6 +11925,16 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case MACHINIST_APRON: case LONGBOW: case MASK_HAZARD_GOGGLES: + case SHILLELAGH_MACE: + case CLAYMORE_SWORD: + case STEEL_FALSHION: + case STEEL_GREATAXE: + case BLACKIRON_DART: + case SILVER_PLUMBATA: + case ANELACE_SWORD: + case LANCE_SPEAR: + case RAPIER: + case STEEL_FLAIL: *metal = 3; *magic = 0; break; @@ -9726,6 +11945,19 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case HAT_STAG_HOOD: case HAT_BUNNY_HOOD: case HAT_TOPHAT: + case SILVER_AXE: + case SILVER_GLAIVE: + case SILVER_MACE: + case SILVER_SWORD: + case SILVER_BREASTPIECE: + case SILVER_GAUNTLETS: + case SILVER_BOOTS: + case SILVER_SHIELD: + case SILVER_HELM: + case CHAIN_HAUBERK: + case CHAIN_GLOVES: + case CHAIN_BOOTS: + case CHAIN_COIF: *metal = 3; *magic = 1; break; @@ -9744,6 +11976,7 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case STEEL_BREASTPIECE: case CRYSTAL_SHURIKEN: case HEAVY_CROSSBOW: + case SCUTUM: *metal = 4; *magic = 0; break; @@ -9758,6 +11991,16 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma case CRYSTAL_MACE: case CLOAK_BACKPACK: case COMPOUND_BOW: + case BLACKIRON_AXE: + case BLACKIRON_CROSSBOW: + case BLACKIRON_MACE: + case BLACKIRON_SWORD: + case BLACKIRON_TRIDENT: + case BLACKIRON_BREASTPIECE: + case BLACKIRON_GAUNTLETS: + case BLACKIRON_BOOTS: + case BLACKIRON_SHIELD: + case BLACKIRON_HELM: *metal = 4; *magic = 2; break; @@ -9840,7 +12083,7 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma } break; case TOOL_DECOY: - *metal = 2; + *metal = 1; *magic = 0; break; case TOOL_DETONATOR_CHARGE: @@ -9848,6 +12091,24 @@ bool GenericGUIMenu::tinkeringGetItemValue(const Item* item, int* metal, int* ma *magic = 4; break; + case KEY_IRON: + case KEY_BRONZE: + *metal = 1; + *magic = 0; + break; + + case KEY_SILVER: + case KEY_GOLD: + *metal = 2; + *magic = 0; + break; + + //case XXX: + // // TODOS + // *metal = 0; + // *magic = 0; + // break; + default: *metal = 0; *magic = 0; @@ -10112,9 +12373,16 @@ bool GenericGUIMenu::tinkeringKitDegradeOnUse(int player) return false; } } + if ( !toDegrade ) + { + return false; + } bool isEquipped = itemIsEquipped(toDegrade, gui_player); - + if ( isEquipped && players[player]->entity && players[player]->entity->spellEffectPreserveItem(toDegrade) ) + { + return false; + } toDegrade->status = std::max(BROKEN, static_cast(toDegrade->status - 1)); if ( toDegrade->status > BROKEN ) { @@ -10155,6 +12423,11 @@ Item* GenericGUIMenu::tinkeringKitFindInInventory() } else { + if ( workstationEntityUid != 0 ) + { + return nullptr; + } + for ( node_t* invnode = stats[gui_player]->inventory.first; invnode != NULL; invnode = invnode->next ) { Item* tinkerItem = (Item*)invnode->element; @@ -10630,6 +12903,14 @@ bool GenericGUIMenu::tinkeringConsumeMaterialsForRepair(Item* item, bool upgradi players[gui_player]->entity->increaseSkill(PRO_LOCKPICKING); } } + + if ( stats[gui_player]->getProficiency(PRO_LOCKPICKING) < 100 && tinkerGUI.bOpen ) + { + if ( workstationEntityUid != 0 && uidToEntity(workstationEntityUid) ) + { + Compendium_t::Events_t::eventUpdateWorld(gui_player, Compendium_t::CPDM_WORKBENCH_SKILLUPS, "workbench", 1); + } + } } for ( int c = 0; c < metal; ++c ) @@ -10885,7 +13166,7 @@ void GenericGUIMenu::scribingGetChargeCost(Item* itemUsedWith, int& outChargeCos int skillLVL = 0; if ( stats[gui_player] && players[gui_player] ) { - skillLVL = (stats[gui_player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[gui_player], players[gui_player]->entity)) / 20; // 0 to 5 + skillLVL = (stats[gui_player]->getModifiedProficiency(PRO_SORCERY) + statGetINT(stats[gui_player], players[gui_player]->entity)) / 20; // 0 to 5 } if ( scribingToolItem->beatitude > 0 ) { @@ -10958,7 +13239,7 @@ bool GenericGUIMenu::scribingWriteItem(Item* item) // request level up strcpy((char*)net_packet->data, "CSKL"); net_packet->data[4] = gui_player; - net_packet->data[5] = PRO_MAGIC; + net_packet->data[5] = PRO_SORCERY; net_packet->address.host = net_server.host; net_packet->address.port = net_server.port; net_packet->len = 6; @@ -10968,7 +13249,7 @@ bool GenericGUIMenu::scribingWriteItem(Item* item) { if ( players[gui_player] && players[gui_player]->entity ) { - players[gui_player]->entity->increaseSkill(PRO_MAGIC); + players[gui_player]->entity->increaseSkill(PRO_SORCERY); } } } @@ -11049,7 +13330,7 @@ bool GenericGUIMenu::scribingWriteItem(Item* item) // request level up strcpy((char*)net_packet->data, "CSKL"); net_packet->data[4] = gui_player; - net_packet->data[5] = PRO_MAGIC; + net_packet->data[5] = PRO_SORCERY; net_packet->address.host = net_server.host; net_packet->address.port = net_server.port; net_packet->len = 6; @@ -11059,7 +13340,7 @@ bool GenericGUIMenu::scribingWriteItem(Item* item) { if ( players[gui_player] && players[gui_player]->entity ) { - players[gui_player]->entity->increaseSkill(PRO_MAGIC); + players[gui_player]->entity->increaseSkill(PRO_SORCERY); } } } @@ -11135,10 +13416,33 @@ void EnemyHPDamageBarHandler::cullExpiredHPBars() } if ( ticks - (*it).second.enemy_timer >= tickLifetime ) { - (*it).second.expired = true; - if ( (*it).second.animator.fadeOut <= 0.01 ) + bool expire = true; + if ( (*it).second.detectMonsterCheckStatus ) + { + if ( Entity* parent = uidToEntity((*it).first) ) + { + if ( Stat* stats = parent->getStats() ) + { + if ( stats->getEffectActive(EFF_DETECT_ENEMY) ) + { + (*it).second.enemy_timer += tickLifetime / 2; // extend lifetime of bar while effect active + expire = false; + } + } + } + } + + if ( expire ) { - it = HPBars.erase(it); // no need to show this bar, delete it + (*it).second.expired = true; + if ( (*it).second.animator.fadeOut <= 0.01 ) + { + it = HPBars.erase(it); // no need to show this bar, delete it + } + else + { + ++it; + } } else { @@ -11394,7 +13698,7 @@ void EnemyHPDamageBarHandler::EnemyHPDetails::updateWorldCoordinates() } } } - if ( entity->behavior == &actDoor && entity->flags[PASSABLE] ) + if ( (entity->behavior == &actDoor || entity->behavior == &actIronDoor) && entity->flags[PASSABLE] ) { if ( entity->doorStartAng == 0 ) { @@ -11452,6 +13756,10 @@ EnemyHPDamageBarHandler::EnemyHPDetails* EnemyHPDamageBarHandler::addEnemyToList details->displayOnHUD = false; details->hasDistanceCheck = false; details->expired = false; + if ( gibDmgType == DMG_DETECT_MONSTER ) + { + details->detectMonsterCheckStatus = true; + } } else { @@ -11460,6 +13768,10 @@ EnemyHPDamageBarHandler::EnemyHPDetails* EnemyHPDamageBarHandler::addEnemyToList details = &(*find).second; details->animator.previousSetpoint = details->enemy_oldhp; details->animator.backgroundValue = details->enemy_oldhp; + if ( gibDmgType == DMG_DETECT_MONSTER ) + { + details->detectMonsterCheckStatus = true; + } } details->animator.maxValue = details->enemy_maxhp; @@ -11479,8 +13791,14 @@ EnemyHPDamageBarHandler::EnemyHPDetails* EnemyHPDamageBarHandler::addEnemyToList details->enemy_statusEffects1 = 0; details->enemy_statusEffects2 = 0; + details->enemy_statusEffects3 = 0; + details->enemy_statusEffects4 = 0; + details->enemy_statusEffects5 = 0; details->enemy_statusEffectsLowDuration1 = 0; details->enemy_statusEffectsLowDuration2 = 0; + details->enemy_statusEffectsLowDuration3 = 0; + details->enemy_statusEffectsLowDuration4 = 0; + details->enemy_statusEffectsLowDuration5 = 0; if ( entity && (entity->behavior == &actPlayer || entity->behavior == &actMonster) && multiplayer != CLIENT ) { @@ -11488,7 +13806,7 @@ EnemyHPDamageBarHandler::EnemyHPDetails* EnemyHPDamageBarHandler::addEnemyToList { for ( int i = 0; i < NUMEFFECTS; ++i ) { - if ( stat->EFFECTS[i] ) + if ( stat->getEffectActive(i) > 0 ) { if ( i < 32 ) { @@ -11503,7 +13821,31 @@ EnemyHPDamageBarHandler::EnemyHPDetails* EnemyHPDamageBarHandler::addEnemyToList details->enemy_statusEffects2 |= (1 << (i - 32)); if ( stat->EFFECTS_TIMERS[i] > 0 && stat->EFFECTS_TIMERS[i] < 5 * TICKS_PER_SECOND ) { - details->enemy_statusEffectsLowDuration2 |= (1 << i); + details->enemy_statusEffectsLowDuration2 |= (1 << (i - 32)); + } + } + else if ( i < 96 ) + { + details->enemy_statusEffects3 |= (1 << (i - 64)); + if ( stat->EFFECTS_TIMERS[i] > 0 && stat->EFFECTS_TIMERS[i] < 5 * TICKS_PER_SECOND ) + { + details->enemy_statusEffectsLowDuration3 |= (1 << (i - 64)); + } + } + else if ( i < 128 ) + { + details->enemy_statusEffects4 |= (1 << (i - 96)); + if ( stat->EFFECTS_TIMERS[i] > 0 && stat->EFFECTS_TIMERS[i] < 5 * TICKS_PER_SECOND ) + { + details->enemy_statusEffectsLowDuration4 |= (1 << (i - 96)); + } + } + else if ( i < 160 ) + { + details->enemy_statusEffects5 |= (1 << (i - 128)); + if ( stat->EFFECTS_TIMERS[i] > 0 && stat->EFFECTS_TIMERS[i] < 5 * TICKS_PER_SECOND ) + { + details->enemy_statusEffectsLowDuration5 |= (1 << (i - 128)); } } } @@ -11602,6 +13944,27 @@ void GenericGUIMenu::TinkerGUI_t::closeTinkerMenu() } tinkerSlotFrames.clear(); } + + if ( multiplayer != CLIENT ) + { + if ( Entity* workstation = uidToEntity(parentGUI.workstationEntityUid) ) + { + workstation->skill[6] = 0; + serverUpdateEntitySkill(workstation, 6); + } + } + + if ( multiplayer == CLIENT && parentGUI.workstationEntityUid > 0 ) + { + strcpy((char*)net_packet->data, "WRKC"); + net_packet->data[4] = clientnum; + SDLNet_Write32(parentGUI.workstationEntityUid, &net_packet->data[5]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + parentGUI.workstationEntityUid = 0; } void onTinkerChangeTabAction(const int playernum, bool changingToNewTab = true) @@ -11926,6 +14289,24 @@ void buttonTinkerUpdateSelectorOnHighlight(const int player, Button* button) } } +void buttonItemfxSelectorOnHighlight(const int player, Button* button) +{ + if ( button->isHighlighted() ) + { + players[player]->GUI.setHoveringOverModuleButton(Player::GUI_t::MODULE_ITEMEFFECTGUI); + if ( players[player]->GUI.activeModule != Player::GUI_t::MODULE_ITEMEFFECTGUI ) + { + players[player]->GUI.activateModule(Player::GUI_t::MODULE_ITEMEFFECTGUI); + } + SDL_Rect pos = button->getAbsoluteSize(); + // make sure to adjust absolute size to camera viewport + pos.x -= players[player]->camera_virtualx1(); + pos.y -= players[player]->camera_virtualy1(); + players[player]->hud.setCursorDisabled(false); + players[player]->hud.updateCursorAnimation(pos.x - 1, pos.y - 1, pos.w, pos.h, inputs.getVirtualMouse(player)->draw_cursor); + } +} + void GenericGUIMenu::TinkerGUI_t::updateTinkerMenu() { const int playernum = parentGUI.getPlayer(); @@ -11937,6 +14318,29 @@ void GenericGUIMenu::TinkerGUI_t::updateTinkerMenu() return; } + Entity* workStation = nullptr; + if ( bOpen ) + { + if ( parentGUI.workstationEntityUid != 0 ) + { + workStation = uidToEntity(parentGUI.workstationEntityUid); + if ( !workStation ) + { + parentGUI.closeGUI(); + return; + } + } + + if ( workStation ) + { + if ( player->entity && (entityDist(player->entity, workStation) > TOUCHRANGE) ) + { + parentGUI.closeGUI(); + return; + } + } + } + if ( !tinkerFrame ) { return; @@ -12259,11 +14663,22 @@ void GenericGUIMenu::TinkerGUI_t::updateTinkerMenu() } else { - tinkerKitTitle->setText(""); + if ( workStation ) + { + tinkerKitTitle->setText(Language::get(6978)); + } + else + { + tinkerKitTitle->setText(""); + } tinkerKitStatus->setText(""); } SDL_Rect textPos{ 0, 27, baseFrame->getSize().w, 24 }; + if ( workStation ) + { + textPos.y += 8; + } tinkerKitTitle->setSize(textPos); textPos.y += 20; tinkerKitStatus->setSize(textPos); @@ -13248,7 +15663,7 @@ void GenericGUIMenu::TinkerGUI_t::updateTinkerMenu() if ( isTinkerConstructItemSelected(item) ) { foundItem = true; - if ( parentGUI.tinkeringKitItem && parentGUI.tinkeringKitItem->status > BROKEN + if ( ((parentGUI.tinkeringKitItem && parentGUI.tinkeringKitItem->status > BROKEN) || workStation) && actionOK ) { parentGUI.executeOnItemClick(item); @@ -13273,8 +15688,8 @@ void GenericGUIMenu::TinkerGUI_t::updateTinkerMenu() bool finalAction = false; for ( int i = 0; i < numActions && parentGUI.tinkeringBulkSalvage - && parentGUI.tinkeringKitItem - && parentGUI.tinkeringKitItem->status > BROKEN; ++i ) + && ((parentGUI.tinkeringKitItem + && parentGUI.tinkeringKitItem->status > BROKEN) || workStation); ++i ) { if ( i == numActions - 1 ) { @@ -13300,7 +15715,7 @@ void GenericGUIMenu::TinkerGUI_t::updateTinkerMenu() checkConsumedItem = true; } } - if ( (parentGUI.tinkeringKitItem && parentGUI.tinkeringKitItem->status > BROKEN) + if ( ((parentGUI.tinkeringKitItem && parentGUI.tinkeringKitItem->status > BROKEN) || workStation) || (parentGUI.tinkeringKitItem == item && parentGUI.tinkeringFilter == TINKER_FILTER_REPAIRABLE)) { parentGUI.executeOnItemClick(item); @@ -13989,6 +16404,11 @@ GenericGUIMenu::TinkerGUI_t::TinkerActions_t GenericGUIMenu::TinkerGUI_t::setIte snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY, item->beatitude); } + else if ( item->type == MAGICSTAFF_SCEPTER && item->identified ) + { + snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), + item->getName(), item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX, item->beatitude); + } else { snprintf(buf, sizeof(buf), "%s %s (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->beatitude); @@ -14332,7 +16752,7 @@ void GenericGUIMenu::TinkerGUI_t::clearItemDisplayed() const int GenericGUIMenu::AlchemyGUI_t::MAX_ALCH_X = 4; const int GenericGUIMenu::AlchemyGUI_t::MAX_ALCH_Y = 6; -void GenericGUIMenu::AlchemyGUI_t::openAlchemyMenu() +void GenericGUIMenu::AlchemyGUI_t::openAlchemyMenu(GenericGUIMenu::AlchemyGUI_t::AlchemyView_t view) { const int playernum = parentGUI.getPlayer(); auto player = players[playernum]; @@ -14353,7 +16773,7 @@ void GenericGUIMenu::AlchemyGUI_t::openAlchemyMenu() player->hud.compactLayoutMode = Player::HUD_t::COMPACT_LAYOUT_INVENTORY; player->inventory_mode = INVENTORY_MODE_ITEM; bOpen = true; - currentView = ALCHEMY_VIEW_BREW; + currentView = view; } if ( inputs.getUIInteraction(playernum)->selectedItem ) { @@ -14364,6 +16784,25 @@ void GenericGUIMenu::AlchemyGUI_t::openAlchemyMenu() clearItemDisplayed(); } +void GenericGUIMenu::AlchemyGUI_t::changeCurrentView(GenericGUIMenu::AlchemyGUI_t::AlchemyView_t view) +{ + recipes.closeRecipePanel(); + + animTooltip = 0.0; + + animPotion1 = 0.0; + potion1Uid = 0; + animPotion2 = 0.0; + potion2Uid = 0; + alchemyResultPotion.type = POTION_EMPTY; + potionResultUid = 0; + animPotionResult = 0.0; + + itemRequiresTitleReflow = true; + + openAlchemyMenu(view); +} + void GenericGUIMenu::AlchemyGUI_t::closeAlchemyMenu() { const int playernum = parentGUI.getPlayer(); @@ -14424,6 +16863,27 @@ void GenericGUIMenu::AlchemyGUI_t::closeAlchemyMenu() recipesFrame = nullptr; notifications.clear(); recipes.stones.clear(); + + if ( multiplayer != CLIENT ) + { + if ( Entity* cauldron = uidToEntity(parentGUI.alembicEntityUid) ) + { + cauldron->skill[6] = 0; + serverUpdateEntitySkill(cauldron, 6); + } + } + + if ( multiplayer == CLIENT && parentGUI.alembicEntityUid > 0 ) + { + strcpy((char*)net_packet->data, "CAUC"); + net_packet->data[4] = clientnum; + SDLNet_Write32(parentGUI.alembicEntityUid, &net_packet->data[5]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + parentGUI.alembicEntityUid = 0; } int GenericGUIMenu::AlchemyGUI_t::heightOffsetWhenNotCompact = 150; @@ -14520,6 +16980,29 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() return; } + Entity* alembicStation = nullptr; + if ( bOpen ) + { + if ( parentGUI.alembicEntityUid != 0 ) + { + alembicStation = uidToEntity(parentGUI.alembicEntityUid); + if ( !alembicStation ) + { + parentGUI.closeGUI(); + return; + } + } + + if ( alembicStation ) + { + if (player->entity && (entityDist(player->entity, alembicStation) > TOUCHRANGE)) + { + parentGUI.closeGUI(); + return; + } + } + } + if ( !alchFrame ) { return; @@ -14655,6 +17138,30 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() auto baseFrame = alchFrame->findFrame("alchemy base"); baseFrame->setDisabled(false); + if ( auto baseFrameImg = baseFrame->findImage("alchemy base img") ) + { + if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + baseFrameImg->path = "*#images/ui/Alchemy/Alchemy_Base_01.png"; + } + else if ( currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + baseFrameImg->path = "*#images/ui/Alchemy/Alchemy_Cook_01.png"; + } + } + + /*if ( keystatus[SDLK_g] ) + { + keystatus[SDLK_g] = 0; + if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + changeCurrentView(AlchemyGUI_t::ALCHEMY_VIEW_COOK); + } + else + { + changeCurrentView(AlchemyGUI_t::ALCHEMY_VIEW_BREW); + } + }*/ alchFrame->setSize(alchFramePos); @@ -14781,13 +17288,29 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() if ( auto emptyBottleFrame = baseFrame->findFrame("empty bottles") ) { emptyBottleFrame->setUserData(&GAMEUI_FRAMEDATA_ALCHEMY_RECIPE_ENTRY); - updateSlotFrameFromItem(emptyBottleFrame, &emptyBottleCount); + if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + updateSlotFrameFromItem(emptyBottleFrame, &emptyBottleCount); + } + else + { + updateSlotFrameFromItem(emptyBottleFrame, &torchCount); + if ( torchCount.count < 0 ) + { + auto spriteImageFrame = emptyBottleFrame->findFrame("item sprite frame"); + auto spriteImage = spriteImageFrame->findImage("item sprite img"); + spriteImage->path = "*#images/ui/Alchemy/Campfire.png"; + } + } } - animPotion1DestX = baseFrame->getSize().x + 36; - animPotion1DestY = baseFrame->getSize().y + 128; - animPotion2DestX = baseFrame->getSize().x + 128; - animPotion2DestY = baseFrame->getSize().y + 128; + static ConsoleVariable cvar_alchemy_potion1_destx1("/alchemy_potion1_destx1", 36); + static ConsoleVariable cvar_alchemy_potion1_destx2("/alchemy_potion1_destx2", 128); + static ConsoleVariable cvar_alchemy_potion1_desty1("/alchemy_potion1_desty1", 128); + animPotion1DestX = baseFrame->getSize().x + *cvar_alchemy_potion1_destx1; + animPotion1DestY = baseFrame->getSize().y + *cvar_alchemy_potion1_desty1; + animPotion2DestX = baseFrame->getSize().x + *cvar_alchemy_potion1_destx2; + animPotion2DestY = baseFrame->getSize().y + *cvar_alchemy_potion1_desty1; animPotionResultStartX = baseFrame->getSize().x + 74; animPotionResultStartY = baseFrame->getSize().y + 286; @@ -14838,7 +17361,9 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() { if ( potion1Item = uidToItem(potion1Uid) ) { - if ( !potion1Item->identified || itemIsEquipped(potion1Item, playernum) ) + if ( !potion1Item->identified || itemIsEquipped(potion1Item, playernum) + || potion1Item->status == BROKEN + || (potion1Item->type == FOOD_TIN && !hasTinOpener) ) { potion1Item = nullptr; // if this got unidentified somehow, remove it potion1Uid = 0; @@ -14881,6 +17406,13 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() } else { + if ( alchemyMissingIngredientQty(potion1Item) ) + { + if ( animPotion1 < 0.001 ) + { + animPotion1Frame->setUserData(&GAMEUI_FRAMEDATA_ALCHEMY_MISSING_QTY); + } + } updateSlotFrameFromItem(animPotion1Frame, potion1Item); } } @@ -14904,7 +17436,9 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() { if ( potion2Item = uidToItem(potion2Uid) ) { - if ( !potion2Item->identified || itemIsEquipped(potion2Item, playernum) ) + if ( !potion2Item->identified || itemIsEquipped(potion2Item, playernum) + || potion2Item->status == BROKEN + || (potion2Item->type == FOOD_TIN && !hasTinOpener) ) { potion2Item = nullptr; // if this got unidentified somehow, remove it potion2Uid = 0; @@ -14936,6 +17470,13 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() } else { + if ( alchemyMissingIngredientQty(potion2Item) ) + { + if ( animPotion2 < 0.001 ) + { + animPotion2Frame->setUserData(&GAMEUI_FRAMEDATA_ALCHEMY_MISSING_QTY); + } + } updateSlotFrameFromItem(animPotion2Frame, potion2Item); } } @@ -15000,7 +17541,13 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() { potionResultFrame->setDisabled(false); potionResultFrame->setUserData(nullptr); + Status oldStatus = alchemyResultPotion.status; + if ( alchemyMissingIngredientQty(nullptr) ) + { + alchemyResultPotion.status = BROKEN; + } updateSlotFrameFromItem(potionResultFrame, &alchemyResultPotion); + alchemyResultPotion.status = oldStatus; if ( alchemyResultPotion.type == TOOL_BOMB ) { auto spriteImageFrame = potionResultFrame->findFrame("item sprite frame"); @@ -15009,11 +17556,25 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() } } + hasTinOpener = false; + if ( !bOpen ) { return; } + for ( node_t* node = stats[playernum]->inventory.first; node != NULL && !hasTinOpener; node = node->next ) + { + Item* item = (Item*)node->element; + if ( !item ) { continue; } + + if ( item->type == TOOL_TINOPENER && item->identified ) + { + hasTinOpener = true; + break; + } + } + if ( !parentGUI.isGUIOpen() || parentGUI.guiType != GUICurrentType::GUI_TYPE_ALCHEMY || !stats[playernum] @@ -15101,10 +17662,22 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() // alembic status { + auto alembicItemIcon = baseFrame->findImage("alchemy item icon"); + alembicItemIcon->disabled = true; + auto alembicItemBadge = baseFrame->findImage("alchemy badge"); + alembicItemBadge->disabled = false; + if ( alembicStation ) + { + alembicItemBadge->disabled = true; + } + auto alembicTitle = baseFrame->findField("alchemy alembic title"); auto alembicStatus = baseFrame->findField("alchemy alembic status"); if ( auto item = parentGUI.alembicItem ) { + alembicItemIcon->path = getItemSpritePath(playernum, *item); + alembicItemIcon->disabled = false; + char buf[128]; if ( !item->identified ) { @@ -15149,11 +17722,22 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() } else { - alembicTitle->setText(""); + if ( alembicStation ) + { + alembicTitle->setText(Language::get(6915)); + } + else + { + alembicTitle->setText(""); + } alembicStatus->setText(""); } SDL_Rect textPos{ 0, 21, baseFrame->getSize().w, 24 }; + if ( alembicStation ) + { + textPos.y -= 14; + } alembicTitle->setSize(textPos); textPos.y += 18; alembicStatus->setSize(textPos); @@ -15161,7 +17745,28 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() bool usingGamepad = inputs.hasController(playernum) && !inputs.getVirtualMouse(playernum)->draw_cursor; auto recipeBtn = baseFrame->findButton("recipe button"); + recipeBtn->setInvisible(false); + auto stationCookBtn = baseFrame->findButton("station cook button"); + auto stationCookText = baseFrame->findField("station cook text"); + auto stationBrewText = baseFrame->findField("station brew text"); + stationCookBtn->setInvisible(!alembicStation); + stationCookText->setDisabled(stationCookBtn->isInvisible()); + stationBrewText->setDisabled(stationCookBtn->isInvisible()); + if ( currentView == ALCHEMY_VIEW_COOK + || currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + recipeBtn->setInvisible(true); + stationCookText->setTextColor(hudColors.characterSheetLightNeutral); + stationBrewText->setTextColor(hudColors.characterSheetFaintText); + } + else + { + stationBrewText->setTextColor(hudColors.characterSheetLightNeutral); + stationCookText->setTextColor(hudColors.characterSheetFaintText); + } auto recipeGlyph = baseFrame->findImage("recipe glyph"); + auto stationToggleGlyph = baseFrame->findImage("station toggle glyph"); + auto stationToggleGlyph2 = baseFrame->findImage("station toggle glyph 2"); { // close btn auto closeBtn = baseFrame->findButton("close alchemy button"); @@ -15189,12 +17794,25 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() closeGlyph->pos.h = imgGet->getHeight(); closeGlyph->disabled = false; } - closeGlyph->pos.x = closeBtn->getSize().x + closeBtn->getSize().w / 2 - closeGlyph->pos.w / 2; - if ( closeGlyph->pos.x % 2 == 1 ) + + if ( stationBrewText->isDisabled() ) { - ++closeGlyph->pos.x; + closeGlyph->pos.x = closeBtn->getSize().x + closeBtn->getSize().w / 2 - closeGlyph->pos.w / 2; + if ( closeGlyph->pos.x % 2 == 1 ) + { + ++closeGlyph->pos.x; + } + closeGlyph->pos.y = closeBtn->getSize().y + closeBtn->getSize().h - 4; + } + else + { + closeGlyph->pos.x = closeBtn->getSize().x - closeGlyph->pos.w; + closeGlyph->pos.y = closeBtn->getSize().y + closeBtn->getSize().h / 2 - closeGlyph->pos.h / 2; + if ( closeGlyph->pos.y % 2 == 1 ) + { + ++closeGlyph->pos.y; + } } - closeGlyph->pos.y = closeBtn->getSize().y + closeBtn->getSize().h - 4; } recipeBtn->setDisabled(true); @@ -15207,7 +17825,7 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() { recipeBtn->setText(Language::get(4170)); } - if ( inputs.getVirtualMouse(playernum)->draw_cursor ) + if ( inputs.getVirtualMouse(playernum)->draw_cursor && !recipeBtn->isInvisible() ) { recipeBtn->setDisabled(!isInteractable); if ( isInteractable ) @@ -15219,7 +17837,7 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() { recipeBtn->deselect(); } - if ( recipeBtn->isDisabled() && usingGamepad ) + if ( recipeBtn->isDisabled() && usingGamepad && !recipeBtn->isInvisible() ) { recipeGlyph->path = Input::inputs[playernum].getGlyphPathForBinding("MenuPageRightAlt"); if ( auto imgGet = Image::get(recipeGlyph->path.c_str()) ) @@ -15236,8 +17854,59 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() recipeGlyph->pos.y = recipeBtn->getSize().y + recipeBtn->getSize().h - 8; } } + { + stationCookBtn->setDisabled(true); + //recipeGlyph->disabled = true; + if ( inputs.getVirtualMouse(playernum)->draw_cursor && !stationCookBtn->isInvisible() ) + { + stationCookBtn->setDisabled(!isInteractable); + if ( isInteractable ) + { + buttonAlchemyUpdateSelectorOnHighlight(playernum, stationCookBtn); + } + } + else if ( stationCookBtn->isSelected() ) + { + stationCookBtn->deselect(); + } + + stationToggleGlyph->disabled = true; + if ( stationCookBtn->isDisabled() && usingGamepad && !stationCookBtn->isInvisible() ) + { + stationToggleGlyph->path = Input::inputs[playernum].getGlyphPathForBinding("MenuPageLeft"); + if ( auto imgGet = Image::get(stationToggleGlyph->path.c_str()) ) + { + stationToggleGlyph->pos.w = imgGet->getWidth(); + stationToggleGlyph->pos.h = imgGet->getHeight(); + stationToggleGlyph->disabled = false; + } + stationToggleGlyph->pos.x = stationCookBtn->getSize().x - stationToggleGlyph->pos.w - 12; + if ( stationToggleGlyph->pos.x % 2 == 1 ) + { + ++stationToggleGlyph->pos.x; + } + stationToggleGlyph->pos.y = stationCookBtn->getSize().y + stationCookBtn->getSize().h - 6; + } + + stationToggleGlyph2->disabled = true; + if ( stationCookBtn->isDisabled() && usingGamepad && !stationCookBtn->isInvisible() ) + { + stationToggleGlyph2->path = Input::inputs[playernum].getGlyphPathForBinding("MenuPageRight"); + if ( auto imgGet = Image::get(stationToggleGlyph2->path.c_str()) ) + { + stationToggleGlyph2->pos.w = imgGet->getWidth(); + stationToggleGlyph2->pos.h = imgGet->getHeight(); + stationToggleGlyph2->disabled = false; + } + stationToggleGlyph2->pos.x = stationCookBtn->getSize().x + stationCookBtn->getSize().w + 12; + if ( stationToggleGlyph2->pos.x % 2 == 1 ) + { + ++stationToggleGlyph2->pos.x; + } + stationToggleGlyph2->pos.y = stationCookBtn->getSize().y + stationCookBtn->getSize().h - 6; + } + } - int skillLVL = (stats[playernum]->getModifiedProficiency(PRO_ALCHEMY) + statGetINT(stats[playernum], players[playernum]->entity)); Uint32 negativeColor = hudColors.characterSheetRed; Uint32 neutralColor = hudColors.characterSheetLightNeutral; Uint32 positiveColor = hudColors.characterSheetGreen; @@ -15263,7 +17932,57 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() // calculate resultant potion { - if ( potion1Item && potion2Item ) + if ( potion1Item && potion2Item && (currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK) ) + { + int createCount = 1; + Status status = SERVICABLE; + int missingPotion1Count = 0; + int missingPotion2Count = 0; + ItemType res = alchemyCookResult(playernum, potion1Item, potion2Item, createCount, status, missingPotion1Count, missingPotion2Count); + alchemyResultPotion.type = res; + alchemyResultPotion.count = createCount; + alchemyResultPotion.status = status; + alchemyResultPotion.identified = true; + int appearance = 0; + int blessing = 0; + if ( isItemRation(res) ) + { + if ( potion1Item->beatitude > 0 && potion2Item->beatitude > 0 ) + { + blessing = std::min(potion1Item->beatitude, potion2Item->beatitude); // take least blessed + } + else if ( potion1Item->beatitude < 0 && potion2Item->beatitude < 0 ) + { + blessing = std::min(potion1Item->beatitude, potion2Item->beatitude); // take most cursed + } + else if ( (potion1Item->beatitude < 0 && potion2Item->beatitude > 0) + || (potion2Item->beatitude < 0 && potion1Item->beatitude > 0) ) + { + blessing = 0; + } + else if ( potion1Item->beatitude < 0 && potion2Item->beatitude == 0 ) + { + blessing = potion1Item->beatitude; // curse the result + } + else if ( potion1Item->beatitude == 0 && potion2Item->beatitude < 0 ) + { + blessing = potion2Item->beatitude; // curse the result + } + else if ( potion1Item->beatitude > 0 && potion2Item->beatitude == 0 ) + { + blessing = 0; // negate the blessing + } + else if ( potion1Item->beatitude == 0 && potion2Item->beatitude > 0 ) + { + blessing = 0; // negate the blessing + } + } + alchemyResultPotion.beatitude = blessing; + alchemyResultPotion.appearance = 0; + alchemyResultPotion.appearance |= (missingPotion1Count & 0xFF) << 0; + alchemyResultPotion.appearance |= (missingPotion2Count & 0xFF) << 8; + } + else if ( potion1Item && potion2Item && (currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES) ) { bool tryDuplicatePotion = false; bool randomResult = false; @@ -15494,7 +18213,7 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() } if ( usingGamepad && !inputs.getVirtualMouse(playernum)->draw_cursor ) { - if ( potionResultFrame->isDisabled() ) + if ( potionResultFrame->isDisabled() || alchemyMissingIngredientQty(nullptr) ) { brewBtn->setTextColor(hudColors.characterSheetFaintText); } @@ -15502,9 +18221,16 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() { brewBtn->setTextColor(makeColor(255, 255, 255, 255)); } - brewBtn->setText(Language::get(4178)); + if ( currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + brewBtn->setText(Language::get(6773)); + } + else + { + brewBtn->setText(Language::get(4178)); + } brewBtn->setDisabled(true); - if ( !potionResultFrame->isDisabled() ) + if ( !potionResultFrame->isDisabled() && !alchemyMissingIngredientQty(nullptr) ) { brewGlyph->path = Input::inputs[playernum].getGlyphPathForBinding("MenuAlt2"); if ( auto imgGet = Image::get(brewGlyph->path.c_str()) ) @@ -15749,7 +18475,12 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() { color = hudColors.characterSheetFaintText; } - else if ( toCompare == Language::get(4160) || toCompare == Language::get(4165) || toCompare == Language::get(4168) ) + else if ( toCompare == Language::get(4160) + || toCompare == Language::get(4165) + || toCompare == Language::get(6772) + || toCompare == Language::get(6768) + || toCompare == Language::get(6774) + || toCompare == Language::get(4168) ) { color = hudColors.characterSheetRed; } @@ -15938,10 +18669,35 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() } else if ( Input::inputs[playernum].binaryToggle("MenuPageRightAlt") || Input::inputs[playernum].binaryToggle("MenuPageLeftAlt") ) { - Input::inputs[playernum].consumeBinaryToggle("MenuPageLeftAlt"); - Input::inputs[playernum].consumeBinaryToggle("MenuPageRightAlt"); - recipeBtn->activate(); - return; + bool left = Input::inputs[playernum].consumeBinaryToggle("MenuPageLeftAlt"); + bool right = Input::inputs[playernum].consumeBinaryToggle("MenuPageRightAlt"); + /*if ( left && !stationCookBtn->isInvisible() ) + { + stationCookBtn->activate(); + }*/ + if ( (left || right) && !recipeBtn->isInvisible() ) + { + recipeBtn->activate(); + return; + } + } + else if ( Input::inputs[playernum].binaryToggle("MenuPageRight") || Input::inputs[playernum].binaryToggle("MenuPageLeft") ) + { + bool left = Input::inputs[playernum].consumeBinaryToggle("MenuPageLeft"); + bool right = Input::inputs[playernum].consumeBinaryToggle("MenuPageRight"); + if ( (left || right) && !stationCookBtn->isInvisible() ) + { + if ( left && (currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES) ) + { + stationCookBtn->activate(); + return; + } + if ( right && (currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK) ) + { + stationCookBtn->activate(); + return; + } + } } else if ( Input::inputs[playernum].binaryToggle("MenuAlt2") ) { @@ -15982,7 +18738,6 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() && getSelectedAlchemySlotY() >= 0 && getSelectedAlchemySlotX() < MAX_ALCH_Y && !tryBrew && recipes.bOpen - /*&& currentView == ALCHEMY_VIEW_RECIPES*/ && itemActionType == ALCHEMY_ACTION_OK ) { if ( recipes.isInteractable @@ -16087,7 +18842,15 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() oldPotion1Type = potion1Item->type; oldPotion2Type = potion2Item->type; potionResultUid = 0; - parentGUI.alchemyCombinePotions(); + + if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + parentGUI.alchemyCombinePotions(); + } + else if ( currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + parentGUI.alchemyCookCombination(); + } if ( parentGUI.basePotion == nullptr ) { potion1Uid = 0; @@ -16222,29 +18985,133 @@ void GenericGUIMenu::AlchemyGUI_t::updateAlchemyMenu() animPotion2Frame->setDisabled(true); Player::soundCancel(); } - else if ( potion1Uid == 0 ) + else if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) { - if ( !parentGUI.isItemMixable(item) ) + if ( potion1Uid == 0 ) { - continue; + if ( !parentGUI.isItemMixable(item) ) + { + continue; + } + animPotion1 = 1.0; + getInventoryItemAlchemyAnimSlotPos(slotFrame, player, item->x, item->y, animPotion1StartX, animPotion1StartY, potionAnimOffsetY); + potion1Uid = item->uid; + //alchemyResultPotion.type = POTION_EMPTY; + playSound(139, 64); // click sound + } + else + { + if ( !parentGUI.isItemMixable(item) ) + { + continue; + } + animPotion2 = 1.0; + getInventoryItemAlchemyAnimSlotPos(slotFrame, player, item->x, item->y, animPotion2StartX, animPotion2StartY, potionAnimOffsetY); + potion2Uid = item->uid; + //alchemyResultPotion.type = POTION_EMPTY; + playSound(139, 64); // click sound } - animPotion1 = 1.0; - getInventoryItemAlchemyAnimSlotPos(slotFrame, player, item->x, item->y, animPotion1StartX, animPotion1StartY, potionAnimOffsetY); - potion1Uid = item->uid; - //alchemyResultPotion.type = POTION_EMPTY; - playSound(139, 64); // click sound } - else + else if ( currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK ) { if ( !parentGUI.isItemMixable(item) ) { continue; } - animPotion2 = 1.0; - getInventoryItemAlchemyAnimSlotPos(slotFrame, player, item->x, item->y, animPotion2StartX, animPotion2StartY, potionAnimOffsetY); - potion2Uid = item->uid; - //alchemyResultPotion.type = POTION_EMPTY; - playSound(139, 64); // click sound + Item* item1 = uidToItem(potion1Uid); + Item* item2 = uidToItem(potion2Uid); + int replaceSlot = 2; + if ( !item1 && !item2 ) + { + replaceSlot = 1; + } + else if ( item->type == FOOD_RATION ) + { + if ( item1 && item1->type == item->type ) + { + replaceSlot = 1; + } + else if ( item2 && item2->type == item->type ) + { + replaceSlot = 2; + } + else + { + if ( item1 && + (GenericGUIMenu::isItemRationSeasoning(item1->type) + || item1->type == TOOL_TOWEL + || item1->type == POTION_WATER) ) + { + replaceSlot = 2; + } + else if(item2 && + (GenericGUIMenu::isItemRationSeasoning(item2->type) + || item2->type == TOOL_TOWEL + || item2->type == POTION_WATER)) + { + replaceSlot = 1; + } + } + } + else if ( GenericGUIMenu::isItemRationSeasoning(item->type) + || item->type == TOOL_TOWEL + || item->type == POTION_WATER ) + { + if ( item1 && + (GenericGUIMenu::isItemRationSeasoning(item1->type) + || item1->type == TOOL_TOWEL + || item1->type == POTION_WATER)) + { + replaceSlot = 1; + } + else if ( item2 && + (GenericGUIMenu::isItemRationSeasoning(item2->type) + || item2->type == TOOL_TOWEL + || item2->type == POTION_WATER) ) + { + replaceSlot = 2; + } + else if ( !item1 ) + { + replaceSlot = 1; + } + } + else if ( itemCategory(item) == FOOD && !GenericGUIMenu::isItemRation(item->type) ) + { + if ( item1 && item1->type == item->type ) + { + replaceSlot = 1; + } + else if ( item2 && item2->type == item->type ) + { + replaceSlot = 2; + } + else if ( item1 && itemCategory(item1) == FOOD && !GenericGUIMenu::isItemRation(item1->type) ) + { + replaceSlot = 2; + } + else + { + replaceSlot = 1; + } + } + + if ( replaceSlot == 1 ) + { + animPotion1 = 1.0; + getInventoryItemAlchemyAnimSlotPos(slotFrame, player, item->x, item->y, animPotion1StartX, animPotion1StartY, potionAnimOffsetY); + potion1Uid = item->uid; + //alchemyResultPotion.type = POTION_EMPTY; + playSound(139, 64); // click sound + } + else if ( replaceSlot == 2 ) + { + animPotion2 = 1.0; + getInventoryItemAlchemyAnimSlotPos(slotFrame, player, item->x, item->y, animPotion2StartX, animPotion2StartY, potionAnimOffsetY); + potion2Uid = item->uid; + //alchemyResultPotion.type = POTION_EMPTY; + playSound(139, 64); // click sound + } } recipes.activateRecipeIndex = -1; animRandomPotionTicks = 0; @@ -16467,6 +19334,13 @@ void GenericGUIMenu::AlchemyGUI_t::createAlchemyMenu() makeColor(255, 255, 255, 255), "*#images/ui/Alchemy/Alchemy_Base_01.png", "alchemy base img"); + auto alembicItemIcon = bgFrame->addImage(SDL_Rect{ 11, 23, 36, 36 }, 0xFFFFFFFF, + "", "alchemy item icon"); + alembicItemIcon->disabled = true; + + auto alembicAlchemyBadge = bgFrame->addImage(SDL_Rect{ 8, 6, 190, 60 }, 0xFFFFFFFF, + "*#images/ui/Alchemy/Alchemy_Badge.png", "alchemy badge"); + auto headerFont = "fonts/pixel_maz_multiline.ttf#16#2"; auto alembicTitle = bgFrame->addField("alchemy alembic title", 128); alembicTitle->setFont(headerFont); @@ -16504,6 +19378,128 @@ void GenericGUIMenu::AlchemyGUI_t::createAlchemyMenu() 0xFFFFFFFF, "*#images/ui/Alchemy/Alchemy_LabelName_2Row_00.png", "item text img"); } } + + { + auto stationCookBtn = bgFrame->addButton("station cook button"); + stationCookBtn->setInvisible(true); + SDL_Rect btnPos{ 62, 28, 82, 30 }; + stationCookBtn->setSize(btnPos); + stationCookBtn->setColor(makeColor(255, 255, 255, 255)); + stationCookBtn->setHighlightColor(makeColor(255, 255, 255, 255)); + stationCookBtn->setText(""); + stationCookBtn->setFont(itemFont); + stationCookBtn->setHideGlyphs(true); + stationCookBtn->setHideKeyboardGlyphs(true); + stationCookBtn->setHideSelectors(true); + stationCookBtn->setMenuConfirmControlType(0); + stationCookBtn->setColor(makeColor(255, 255, 255, 255)); + stationCookBtn->setHighlightColor(makeColor(255, 255, 255, 255)); + //recipeBtn->setTextHighlightColor(makeColor(201, 162, 100, 255)); + stationCookBtn->setCallback([](Button& button) { + int player = button.getOwner(); + if ( GenericGUI[player].alchemyGUI.bOpen ) + { + if ( GenericGUI[player].alchemyGUI.currentView == GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_BREW + || GenericGUI[player].alchemyGUI.currentView == GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_RECIPES ) + { + GenericGUI[player].alchemyGUI.changeCurrentView(GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_COOK); + button.setBackground("*#images/ui/Alchemy/Button_CauldronToggleLeft.png"); + button.setBackgroundHighlighted("*#images/ui/Alchemy/Button_CauldronToggleLeft.png"); + button.setBackgroundActivated("*#images/ui/Alchemy/Button_CauldronToggleLeft.png"); + Player::soundActivate(); + } + else + { + GenericGUI[player].alchemyGUI.changeCurrentView(GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_BREW); + button.setBackground("*#images/ui/Alchemy/Button_CauldronToggleRight.png"); + button.setBackgroundHighlighted("*#images/ui/Alchemy/Button_CauldronToggleRight.png"); + button.setBackgroundActivated("*#images/ui/Alchemy/Button_CauldronToggleRight.png"); + Player::soundActivate(); + } + } + }); + stationCookBtn->setTickCallback(genericgui_deselect_fn); + + auto stationCookText = bgFrame->addField("station cook text", 32); + stationCookText->setFont(itemFont); + stationCookText->setText(Language::get(6773)); + stationCookText->setHJustify(Field::justify_t::RIGHT); + stationCookText->setVJustify(Field::justify_t::TOP); + stationCookText->setSize(SDL_Rect{ btnPos.x - 90 - 8, btnPos.y + 5, 90, 24 }); + stationCookText->setTextColor(hudColors.characterSheetLightNeutral); + stationCookText->setDisabled(true); + + auto stationBrewText = bgFrame->addField("station brew text", 32); + stationBrewText->setFont(itemFont); + stationBrewText->setText(Language::get(3339)); + stationBrewText->setHJustify(Field::justify_t::LEFT); + stationBrewText->setVJustify(Field::justify_t::TOP); + stationBrewText->setSize(SDL_Rect{ btnPos.x + btnPos.w + 8, btnPos.y + 5, 90, 24 }); + stationBrewText->setTextColor(hudColors.characterSheetLightNeutral); + stationBrewText->setDisabled(true); + + auto stationToggleGlyph = bgFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "", "station toggle glyph"); + stationToggleGlyph->disabled = true; + stationToggleGlyph->ontop = true; + + auto stationToggleGlyph2 = bgFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "", "station toggle glyph 2"); + stationToggleGlyph2->disabled = true; + stationToggleGlyph2->ontop = true; + + if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + stationCookBtn->setBackground("*#images/ui/Alchemy/Button_CauldronToggleRight.png"); + stationCookBtn->setBackgroundHighlighted("*#images/ui/Alchemy/Button_CauldronToggleRight.png"); + stationCookBtn->setBackgroundActivated("*#images/ui/Alchemy/Button_CauldronToggleRight.png"); + stationCookText->setTextColor(hudColors.characterSheetFaintText); + } + else + { + stationCookBtn->setBackground("*#images/ui/Alchemy/Button_CauldronToggleLeft.png"); + stationCookBtn->setBackgroundHighlighted("*#images/ui/Alchemy/Button_CauldronToggleLeft.png"); + stationCookBtn->setBackgroundActivated("*#images/ui/Alchemy/Button_CauldronToggleLeft.png"); + stationBrewText->setTextColor(hudColors.characterSheetFaintText); + } + + //auto stationBrewBtn = bgFrame->addButton("station brew button"); + //stationBrewBtn->setInvisible(true); + //btnPos.x += 92; + //stationBrewBtn->setSize(btnPos); + //stationBrewBtn->setColor(makeColor(255, 255, 255, 255)); + //stationBrewBtn->setHighlightColor(makeColor(255, 255, 255, 255)); + //stationBrewBtn->setText(Language::get(3339)); + //stationBrewBtn->setFont(itemFont); + //stationBrewBtn->setHideGlyphs(true); + //stationBrewBtn->setHideKeyboardGlyphs(true); + //stationBrewBtn->setHideSelectors(true); + //stationBrewBtn->setMenuConfirmControlType(0); + //stationBrewBtn->setBackground("*#images/ui/Alchemy/Cauldron_ButtonTop_Base_00.png"); + //stationBrewBtn->setColor(makeColor(255, 255, 255, 255)); + //stationBrewBtn->setHighlightColor(makeColor(255, 255, 255, 255)); + //stationBrewBtn->setBackgroundHighlighted("*#images/ui/Alchemy/Cauldron_ButtonTop_High_00.png"); + //stationBrewBtn->setBackgroundActivated("*#images/ui/Alchemy/Cauldron_ButtonTop_Press_00.png"); + ////recipeBtn->setTextHighlightColor(makeColor(201, 162, 100, 255)); + //stationBrewBtn->setCallback([](Button& button) { + // int player = button.getOwner(); + // if ( GenericGUI[player].alchemyGUI.bOpen ) + // { + // if ( GenericGUI[player].alchemyGUI.currentView == GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_COOK + // || GenericGUI[player].alchemyGUI.currentView == GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_RECIPES_COOK ) + // { + // GenericGUI[player].alchemyGUI.changeCurrentView(GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_BREW); + // Player::soundActivate(); + // } + // else + // { + // Player::soundActivate(); + // } + // } + // }); + //stationBrewBtn->setTickCallback(genericgui_deselect_fn); + } + { auto recipeBtn = bgFrame->addButton("recipe button"); SDL_Rect recipeBtnPos{ 12, 72, 182, 40 }; @@ -16526,15 +19522,19 @@ void GenericGUIMenu::AlchemyGUI_t::createAlchemyMenu() int player = button.getOwner(); if ( GenericGUI[player].alchemyGUI.bOpen ) { - if ( GenericGUI[player].alchemyGUI.recipes.bOpen ) - { - GenericGUI[player].alchemyGUI.recipes.closeRecipePanel(); - Player::soundCancel(); - } - else + if ( GenericGUI[player].alchemyGUI.currentView == GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_BREW + || GenericGUI[player].alchemyGUI.currentView == GenericGUIMenu::AlchemyGUI_t::ALCHEMY_VIEW_RECIPES ) { - GenericGUI[player].alchemyGUI.recipes.openRecipePanel(); - Player::soundActivate(); + if ( GenericGUI[player].alchemyGUI.recipes.bOpen ) + { + GenericGUI[player].alchemyGUI.recipes.closeRecipePanel(); + Player::soundCancel(); + } + else + { + GenericGUI[player].alchemyGUI.recipes.openRecipePanel(); + Player::soundActivate(); + } } } }); @@ -16689,7 +19689,7 @@ Frame* GenericGUIMenu::AlchemyGUI_t::getAlchemySlotFrame(int x, int y) const return nullptr; } -void GenericGUIMenu::AlchemyGUI_t::setItemDisplayNameAndPrice(Item* item, bool isTooltipForResultPotion, bool isTooltipForRecipe) +void GenericGUIMenu::AlchemyGUI_t::setItemDisplayNameAndPrice(Item* item, const bool isTooltipForResultPotion, const bool isTooltipForRecipe) { itemActionType = ALCHEMY_ACTION_NONE; if ( !item || item->type == SPELL_ITEM ) @@ -16701,6 +19701,96 @@ void GenericGUIMenu::AlchemyGUI_t::setItemDisplayNameAndPrice(Item* item, bool i return; } + if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + setItemDisplayNameAndPriceBrew(item, isTooltipForResultPotion, isTooltipForRecipe); + } + else + { + setItemDisplayNameAndPriceCook(item, isTooltipForResultPotion, isTooltipForRecipe); + } + return; +} + + +bool GenericGUIMenu::AlchemyGUI_t::inventoryItemAllowedInGUI(Item* item) +{ + if ( !item ) { return false; } + if ( item->status == BROKEN ) + { + return false; + } + if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + if ( itemCategory(item) == POTION && item->type != POTION_EMPTY ) + { + return true; + } + } + else if ( currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + Item* item1 = potion1Uid != 0 ? uidToItem(potion1Uid) : nullptr; + Item* item2 = potion2Uid != 0 ? uidToItem(potion2Uid) : nullptr; + + if ( GenericGUIMenu::isItemRationSeasoning(item->type) || item->type == TOOL_TOWEL || item->type == POTION_WATER ) + { + if ( !item1 && !item2 ) + { + return true; + } + if ( (item1 && item1->type == FOOD_RATION) || (item2 && item2->type == FOOD_RATION) ) + { + return true; + } + if ( (item->type == TOOL_TOWEL || item->type == POTION_WATER || GenericGUIMenu::isItemRationSeasoning(item->type)) + && + ((item1 && item1->type == TOOL_TOWEL) || (item2 && item2->type == TOOL_TOWEL) + || (item1 && item1->type == POTION_WATER) || (item2 && item2->type == POTION_WATER) + || (item1 && GenericGUIMenu::isItemRationSeasoning(item1->type)) + || (item2 && GenericGUIMenu::isItemRationSeasoning(item2->type))) ) + { + return true; + } + } + else if ( itemCategory(item) == FOOD ) + { + if ( item->type == FOOD_RATION ) + { + if ( !item1 && !item2 ) + { + return true; + } + if ( (item1 && itemCategory(item1) == FOOD && item1->type != FOOD_RATION) + || (item2 && itemCategory(item2) == FOOD && item2->type != FOOD_RATION) ) + { + return false; + } + return true; + } + else if ( GenericGUIMenu::isItemRation(item->type) ) + { + return false; + } + else + { + if ( !item1 && !item2 ) + { + return true; + } + if ( (item1 && itemCategory(item1) == FOOD && item1->type != FOOD_RATION) + || (item2 && itemCategory(item2) == FOOD && item2->type != FOOD_RATION) ) + { + return true; + } + } + } + } + + return false; +} + +void GenericGUIMenu::AlchemyGUI_t::setItemDisplayNameAndPriceBrew(Item* item, const bool isTooltipForResultPotion, const bool isTooltipForRecipe) +{ itemTooltipForRecipe = isTooltipForRecipe; char buf[1024]; @@ -16769,7 +19859,7 @@ void GenericGUIMenu::AlchemyGUI_t::setItemDisplayNameAndPrice(Item* item, bool i secondaryPotion = uidToItem(potion2Uid); if ( basePotion && secondaryPotion ) { - if ( basePotion->identified && secondaryPotion->identified + if ( basePotion->identified && secondaryPotion->identified && basePotion->type == secondaryPotion->type ) { isSameResult = true; @@ -16938,7 +20028,211 @@ void GenericGUIMenu::AlchemyGUI_t::setItemDisplayNameAndPrice(Item* item, bool i } } } - return; +} + +void GenericGUIMenu::AlchemyGUI_t::setItemDisplayNameAndPriceCook(Item* item, const bool isTooltipForResultPotion, const bool isTooltipForRecipe) +{ + itemTooltipForRecipe = isTooltipForRecipe; + + char buf[1024]; + if ( !item->identified ) + { + if ( isTooltipForResultPotion ) + { + snprintf(buf, sizeof(buf), "%s (?)", Language::get(4161)); + } + else + { + snprintf(buf, sizeof(buf), "%s %s (?)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName()); + } + } + else if ( item->type == TOOL_BOMB && isTooltipForResultPotion ) + { + snprintf(buf, sizeof(buf), "%s", Language::get(4167)); + } + else + { + snprintf(buf, sizeof(buf), "%s %s (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->beatitude); + } + + auto activateSelectionPrompt = alchFrame->findField("activate prompt"); + activateSelectionPrompt->setText(""); + + int player = parentGUI.getPlayer(); + //bool isSameResult = false; + //bool isDuplicationResult = false; + if ( inventoryItemAllowedInGUI(item) ) + { + bool isEquipped = itemIsEquipped(item, player); + if ( (!item->identified || isEquipped) && !isTooltipForResultPotion && !isTooltipForRecipe ) + { + itemActionType = ALCHEMY_ACTION_UNIDENTIFIED_POTION; + } + else + { + itemActionType = ALCHEMY_ACTION_OK; + } + + if ( isEquipped ) + { + strcat(buf, "\n"); + strcat(buf, Language::get(4165)); + } + else if ( item->identified && item->type == FOOD_TIN && !hasTinOpener ) + { + itemActionType = ALCHEMY_ACTION_UNIDENTIFIED_POTION; + strcat(buf, "\n"); + strcat(buf, Language::get(6772)); + } + //else if ( recipeMissingMaterials ) + //{ + // strcat(buf, "\n"); + // strcat(buf, Language::get(4168)); + //} + //else if ( isSameResult ) + //{ + // strcat(buf, "\n"); + // strcat(buf, Language::get(4166)); + //} + //else if ( isRandomResult ) + //{ + // strcat(buf, "\n"); + // strcat(buf, Language::get(4164)); + //} + //else if ( isDuplicationResult ) + //{ + // int skillLVL = 0; + // if ( stats[parentGUI.getPlayer()] ) + // { + // skillLVL = stats[parentGUI.getPlayer()]->getModifiedProficiency(PRO_ALCHEMY) / 20; // 0 to 5; + // } + // snprintf(buf, sizeof(buf), "%s\n%d%%", Language::get(4163), 50 + skillLVL * 10); + //} + /*else if ( item->identified && find != clientLearnedAlchemyIngredients[player].end() ) + { + if ( parentGUI.isItemBaseIngredient(item->type) ) + { + strcat(buf, "\n"); + strcat(buf, Language::get(4156)); + } + else if ( parentGUI.isItemSecondaryIngredient(item->type) ) + { + strcat(buf, "\n"); + strcat(buf, Language::get(4157)); + } + else + { + strcat(buf, "\n"); + strcat(buf, Language::get(4158)); + } + }*/ + else + { + if ( !item->identified ) + { + if ( isTooltipForResultPotion ) + { + strcat(buf, "\n"); + strcat(buf, Language::get(4162)); + } + else + { + strcat(buf, "\n"); + strcat(buf, Language::get(4160)); + } + } + /*else + { + strcat(buf, "\n"); + strcat(buf, Language::get(4155)); + }*/ + } + } + else + { + if ( (item->type == GREASE_BALL + || item->type == SLOP_BALL + || GenericGUIMenu::isItemRation(item->type)) + && isTooltipForResultPotion ) + { + if ( alchemyMissingIngredientQty(nullptr) ) + { + itemActionType = ALCHEMY_ACTION_UNIDENTIFIED_POTION; + if ( alchemyResultPotion.appearance != 0 ) + { + // missing ingredient + strcat(buf, "\n"); + strcat(buf, Language::get(6768)); + } + else + { + // missing fuel + strcat(buf, "\n"); + strcat(buf, Language::get(6774)); + } + } + else + { + itemActionType = ALCHEMY_ACTION_OK; // this is fine :) + } + } + else + { + itemActionType = ALCHEMY_ACTION_INVALID_ITEM; + } + } + if ( itemDesc != buf ) + { + itemRequiresTitleReflow = true; + } + itemDesc = buf; + itemType = item->type; + if ( itemActionType == ALCHEMY_ACTION_OK ) + { + if ( isTooltipForRecipe ) + { + int index = 0; + activateSelectionPrompt->setText(Language::get(4174)); + if ( recipes.activateRecipeIndex >= 0 ) + { + activateSelectionPrompt->setText(""); + } + else + { + activateSelectionPrompt->setText(Language::get(4174)); + } + } + else if ( !isTooltipForResultPotion ) + { + if ( item->uid == potion1Uid || item->uid == potion2Uid ) + { + activateSelectionPrompt->setText(Language::get(4173)); + } + else + { + activateSelectionPrompt->setText(Language::get(4172)); + } + } + else if ( isTooltipForResultPotion ) + { + bool usingGamepad = inputs.hasController(player) && !inputs.getVirtualMouse(player)->draw_cursor; + if ( !usingGamepad ) + { + /*if ( isSameResult ) + { + activateSelectionPrompt->setText(Language::get(4177)); + } + else if ( isDuplicationResult ) + { + activateSelectionPrompt->setText(Language::get(4178)); + } + else*/ + { + activateSelectionPrompt->setText(Language::get(6773)); + } + } + } + } } bool GenericGUIMenu::AlchemyGUI_t::warpMouseToSelectedAlchemyItem(Item* snapToItem, Uint32 flags) @@ -16994,7 +20288,14 @@ void GenericGUIMenu::AlchemyGUI_t::AlchemyRecipes_t::openRecipePanel() { return; } - alchemy.currentView = ALCHEMY_VIEW_RECIPES; + if ( alchemy.currentView == ALCHEMY_VIEW_BREW ) + { + alchemy.currentView = ALCHEMY_VIEW_RECIPES; + } + else if ( alchemy.currentView == ALCHEMY_VIEW_COOK ) + { + alchemy.currentView = ALCHEMY_VIEW_RECIPES_COOK; + } bool wasDisabled = alchemy.recipesFrame->isDisabled(); alchemy.recipesFrame->setDisabled(false); if ( wasDisabled ) @@ -17017,7 +20318,14 @@ void GenericGUIMenu::AlchemyGUI_t::AlchemyRecipes_t::closeRecipePanel() { alchemy.recipesFrame->setDisabled(true); } - alchemy.currentView = ALCHEMY_VIEW_BREW; + if ( alchemy.currentView == ALCHEMY_VIEW_RECIPES ) + { + alchemy.currentView = ALCHEMY_VIEW_BREW; + } + else if ( alchemy.currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + alchemy.currentView = ALCHEMY_VIEW_COOK; + } animx = 0.0; isInteractable = false; currentScrollRow = 0; @@ -17055,10 +20363,15 @@ void buildRecipeList(const int player) std::unordered_map, int>> inventoryPotions; bool hasEmptyBottle = false; + alchemy.torchCount.count = 0; for ( node_t* node = stats[player]->inventory.first; node; node = node->next ) { Item* item = (Item*)node->element; if ( !item ) { continue; } + if ( item->type == TOOL_TORCH ) + { + alchemy.torchCount.count += item->count; + } if ( itemCategory(item) == POTION ) { if ( item->type == POTION_EMPTY ) @@ -17081,6 +20394,34 @@ void buildRecipeList(const int player) } } + if ( alchemy.currentView == alchemy.ALCHEMY_VIEW_COOK || alchemy.currentView == alchemy.ALCHEMY_VIEW_RECIPES_COOK ) + { + if ( GenericGUI[player].alembicEntityUid != 0 && !GenericGUI[player].alembicItem && uidToEntity(GenericGUI[player].alembicEntityUid) ) + { + alchemy.torchCount.count = -1; + } + else if ( players[player] && players[player]->entity ) + { + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(players[player]->entity, 2); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + node_t* node; + for ( node = currentList->first; node != nullptr; node = node->next ) + { + if ( Entity* entity = ((Entity*)node->element) ) + { + if ( entity->behavior == &actCampfire && entity->skill[3] > 0 /*fire health */ && entityDist(entity, players[player]->entity) < 32.0 ) + { + alchemy.torchCount.count = -1; + break; + } + } + } + } + } + } + alchemy.emptyBottleCount.count = hasEmptyBottle ? inventoryPotions[POTION_EMPTY].second : 0; for ( auto& entry : clientLearnedAlchemyRecipes[player] ) @@ -18680,7 +22021,7 @@ void GenericGUIMenu::FeatherGUI_t::updateFeatherMenu() skillIcon->pos.y = 76 + heightOffsetCompact; for ( auto& skill : Player::SkillSheet_t::skillSheetData.skillEntries ) { - if ( skill.skillId == PRO_MAGIC ) + if ( skill.skillId == PRO_SORCERY ) { if ( skillCapstoneUnlocked(playernum, skill.skillId) ) { @@ -20853,13 +24194,376 @@ bool GenericGUIMenu::FeatherGUI_t::isInscribeOrRepairActive() const void GenericGUIMenu::ItemEffectGUI_t::clearItemDisplayed() { itemType = -1; + costEffectGoldAmount = 0; + costEffectMPAmount = 0; itemActionType = ITEMFX_ACTION_NONE; } +void GenericGUIMenu::ItemEffectGUI_t::getItemEffectCost(Item* itemUsedWith, int& goldCost, int& manaCost) +{ + goldCost = 0; + manaCost = 0; + if ( !itemUsedWith || modeHasCostEffect == COST_EFFECT_NONE ) + { + return; + } + + if ( currentMode == ITEMFX_MODE_RESTORE ) + { + if ( parentGUI.isItemRepairable(itemUsedWith, SCROLL_REPAIR) ) + { + goldCost = itemUsedWith->sellValue(-1); // get value without player CHR/trading influence + real_t goldRatio = getSpellDamageFromID(SPELL_RESTORE, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + goldRatio = std::max(goldRatio, getSpellEffectDurationSecondaryFromID(SPELL_RESTORE, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0); + goldCost = std::max(1, static_cast(goldCost * goldRatio)); + + manaCost = itemUsedWith->sellValue(-1); // get value without player CHR/trading influence + real_t manaRatio = getSpellEffectDurationFromID(SPELL_RESTORE, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + manaCost *= manaRatio; + int minMana = getSpellDamageSecondaryFromID(SPELL_RESTORE, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity); + manaCost = std::max(manaCost, minMana); + } + } + else if ( currentMode == ITEMFX_MODE_SANCTIFY_WATER ) + { + if ( parentGUI.isItemBlessWaterable(itemUsedWith) ) + { + manaCost = 2 * itemUsedWith->count; + manaCost += 2 * itemUsedWith->count * std::max(0, (int)itemUsedWith->beatitude); + } + } + else if ( currentMode == ITEMFX_MODE_SCROLL_ENCHANT_ARMOR ) + { + goldCost = 75; + if ( parentGUI.isItemEnchantArmorable(itemUsedWith) ) + { + int bless = std::max((int)itemUsedWith->beatitude, 0); + for ( int i = 0; i < std::min(10, bless); ++i ) + { + goldCost *= 2; + } + } + goldCost = std::min(10000, goldCost); + } + else if ( currentMode == ITEMFX_MODE_SCROLL_ENCHANT_WEAPON ) + { + goldCost = 75; + if ( parentGUI.isItemEnchantWeaponable(itemUsedWith) ) + { + int bless = std::max((int)itemUsedWith->beatitude, 0); + for ( int i = 0; i < std::min(10, bless); ++i ) + { + goldCost *= 2; + } + } + goldCost = std::min(10000, goldCost); + } + else if ( parentGUI.isItemAlterable(itemUsedWith) ) + { + if ( currentMode == ITEMFX_MODE_ALTER_INSTRUMENT ) + { + goldCost = getSpellDamageFromID(SPELL_ALTER_INSTRUMENT, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity); + goldCost = std::max(goldCost, getSpellDamageSecondaryFromID(SPELL_ALTER_INSTRUMENT, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity)); + manaCost = 15; + } + else if ( currentMode == ITEMFX_MODE_METALLURGY ) + { + int maxGold = itemUsedWith->sellValue(-1); // get value without player CHR/trading influence + real_t ratio = getSpellDamageFromID(SPELL_METALLURGY, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + ratio = std::min(ratio, 1.0); + goldCost = -(maxGold * ratio); + real_t minMana = getSpellDamageSecondaryFromID(SPELL_METALLURGY, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity); + manaCost = std::max(minMana, (maxGold * ratio) / 20.0); + } + else if ( currentMode == ITEMFX_MODE_GEOMANCY ) + { + int maxGold = itemUsedWith->sellValue(-1); // get value without player CHR/trading influence + real_t ratio = getSpellDamageFromID(SPELL_GEOMANCY, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + ratio = std::min(ratio, 1.0); + goldCost = -(maxGold * ratio); + real_t minMana = getSpellDamageSecondaryFromID(SPELL_GEOMANCY, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity); + manaCost = std::max(minMana, (maxGold * ratio) / 20.0); + } + else if ( currentMode == ITEMFX_MODE_VANDALISE ) + { + manaCost = std::max(20, itemUsedWith->getGoldValue() / 100); + goldCost = -itemUsedWith->sellValue(parentGUI.gui_player) / 20; + } + else if ( currentMode == ITEMFX_MODE_FORGE_KEY ) + { + manaCost = 15; + goldCost = 100; + //if ( itemUsedWith->type == KEY_IRON ) + //{ + // goldCost = 100; + //} + //else if ( itemUsedWith->type == KEY_BRONZE ) + //{ + // goldCost = 300; + // manaCost = 25; + //} + //else if ( itemUsedWith->type == KEY_SILVER ) + //{ + // goldCost = 900; + // manaCost = 40; + //} + ///*else if ( itemUsedWith->type == KEY_GOLD ) + //{ + // goldCost = 1200; + //}*/ + //else if ( (itemCategory(itemUsedWith) == GEM && itemUsedWith->type != GEM_ROCK && itemUsedWith->type != GEM_LUCK /*&& itemUsedWith->type != GEM_GLASS*/) ) + //{ + // goldCost = 300; + //} + + int result = parentGUI.getAlterItemResultAtCycle(itemUsedWith); + if ( result >= 0 ) + { + int inputValue = 0; + if ( itemUsedWith->type == KEY_IRON ) + { + inputValue = 300; + } + else if ( itemUsedWith->type == KEY_BRONZE ) + { + inputValue = 450; + } + else if ( itemUsedWith->type == KEY_SILVER ) + { + inputValue = 700; + } + else if ( itemUsedWith->type == KEY_GOLD ) + { + inputValue = 1500; + } + else if ( itemCategory(itemUsedWith) == GEM ) + { + inputValue = itemUsedWith->getGoldValue(); + } + inputValue /= 4; + + if ( result == KEY_IRON ) + { + manaCost = 10; + goldCost = 300; + goldCost = std::max(goldCost / 2, goldCost - inputValue); + } + else if ( result == KEY_BRONZE ) + { + manaCost = 20; + goldCost = 450; + goldCost = std::max(goldCost / 2, goldCost - inputValue); + } + else if ( result == KEY_SILVER ) + { + manaCost = 30; + goldCost = 700; + goldCost = std::max(goldCost / 2, goldCost - inputValue); + } + else if ( result == KEY_GOLD ) + { + manaCost = 50; + goldCost = 1500; + goldCost = std::max(goldCost / 2, goldCost - inputValue); + } + } + + real_t ratio = getSpellDamageFromID(SPELL_FORGE_KEY, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + real_t minRatio = getSpellDamageSecondaryFromID(SPELL_FORGE_KEY, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + goldCost *= std::max(minRatio, ratio); + } + else if ( currentMode == ITEMFX_MODE_FORGE_JEWEL ) + { + manaCost = 30; + if ( (itemCategory(itemUsedWith) == GEM + && itemUsedWith->type != GEM_ROCK + && itemUsedWith->type != GEM_LUCK + /*&& itemUsedWith->type != GEM_GLASS*/) + && !(itemUsedWith->type == GEM_JEWEL && itemUsedWith->status == EXCELLENT) ) + { + int value = itemUsedWith->getGoldValue(); + if ( value < 250 ) + { + manaCost = 20; + goldCost = std::max(100, 250 - value); + } + else if ( value < 500 ) + { + manaCost = 25; + goldCost = std::max(100, 500 - value); + } + else if ( value < 1000 ) + { + manaCost = 30; + goldCost = std::max(100, 1000 - value); + } + else if ( value >= 1000 ) + { + manaCost = 40; + goldCost = std::max(100, 2000 - value); + } + } + + real_t ratio = getSpellDamageFromID(SPELL_FORGE_JEWEL, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + real_t minRatio = getSpellDamageSecondaryFromID(SPELL_FORGE_JEWEL, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + goldCost *= std::max(minRatio, ratio); + } + else if ( currentMode == ITEMFX_MODE_ENHANCE_WEAPON ) + { + int skillTier = 0; + int skillLVL = 0; + if ( spell_t* spell = getSpellFromID(SPELL_ENHANCE_WEAPON) ) + { + skillLVL = stats[parentGUI.gui_player]->getModifiedProficiency(spell->skillID) + + statGetINT(stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity); + skillTier = std::max(0, (skillLVL - spell->difficulty)) / 20; + } + + int noSkillPenalty = 0; + if ( itemUsedWith->type == BRONZE_AXE + || itemUsedWith->type == BRONZE_MACE + || itemUsedWith->type == BRONZE_SWORD + || itemUsedWith->type == BRONZE_TOMAHAWK ) + { + goldCost = 200; + } + else if ( itemUsedWith->type == IRON_AXE + || itemUsedWith->type == IRON_MACE + || itemUsedWith->type == IRON_SWORD + || itemUsedWith->type == IRON_SPEAR + || itemUsedWith->type == IRON_DAGGER ) + { + goldCost = 400; + if ( skillTier < 1 ) + { + noSkillPenalty += 1000000; // denotes no skill lvl + } + } + else if ( itemUsedWith->type == STEEL_AXE + || itemUsedWith->type == STEEL_MACE + || itemUsedWith->type == STEEL_SWORD + || itemUsedWith->type == STEEL_HALBERD + || itemUsedWith->type == STEEL_CHAKRAM ) + { + goldCost = 1600; + if ( skillTier < 2 ) + { + noSkillPenalty += 1000000; // denotes no skill lvl + } + } + noSkillPenalty = 0; + real_t ratio = getSpellDamageFromID(SPELL_ENHANCE_WEAPON, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + real_t minRatio = getSpellDamageSecondaryFromID(SPELL_ENHANCE_WEAPON, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + goldCost *= std::max(minRatio, ratio); + goldCost += noSkillPenalty; + manaCost = 10; + } + else if ( currentMode == ITEMFX_MODE_RESHAPE_WEAPON ) + { + int skillTier = 0; + int skillLVL = 0; + if ( spell_t* spell = getSpellFromID(SPELL_RESHAPE_WEAPON) ) + { + skillLVL = stats[parentGUI.gui_player]->getModifiedProficiency(spell->skillID) + + statGetINT(stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity); + skillTier = std::max(0, (skillLVL - spell->difficulty)) / 20; + } + + int noSkillPenalty = 0; + if ( itemUsedWith->type == BRONZE_AXE + || itemUsedWith->type == BRONZE_MACE + || itemUsedWith->type == BRONZE_SWORD ) + { + goldCost = 100; + } + else if ( itemUsedWith->type == IRON_AXE + || itemUsedWith->type == IRON_MACE + || itemUsedWith->type == IRON_SWORD + || itemUsedWith->type == IRON_SPEAR ) + { + goldCost = 200; + if ( skillTier < 1 ) + { + noSkillPenalty += 1000000; // denotes no skill lvl + } + } + else if ( itemUsedWith->type == STEEL_AXE + || itemUsedWith->type == STEEL_MACE + || itemUsedWith->type == STEEL_SWORD + || itemUsedWith->type == STEEL_HALBERD ) + { + goldCost = 300; + if ( skillTier < 2 ) + { + noSkillPenalty += 1000000; // denotes no skill lvl + } + } + else if ( itemUsedWith->type == CRYSTAL_BATTLEAXE + || itemUsedWith->type == CRYSTAL_MACE + || itemUsedWith->type == CRYSTAL_SWORD + || itemUsedWith->type == CRYSTAL_SPEAR ) + { + goldCost = 400; + if ( skillTier < 3 ) + { + noSkillPenalty += 1000000; // denotes no skill lvl + } + } + noSkillPenalty = 0; + real_t ratio = getSpellDamageFromID(SPELL_RESHAPE_WEAPON, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + real_t minRatio = getSpellDamageSecondaryFromID(SPELL_RESHAPE_WEAPON, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + goldCost *= std::max(minRatio, ratio); + goldCost += noSkillPenalty; + manaCost = 10; + } + else if ( currentMode == ITEMFX_MODE_ALTER_ARROW ) + { + if ( itemTypeIsQuiver(itemUsedWith->type) || itemCategory(itemUsedWith) == GEM ) + { + int result = parentGUI.getAlterItemResultAtCycle(itemUsedWith); + if ( result >= 0 ) + { + ItemType prevType = itemUsedWith->type; + itemUsedWith->type = (ItemType)(result); + goldCost = 100 + itemUsedWith->sellValue(-1); + itemUsedWith->type = prevType; + } + } + real_t ratio = getSpellDamageFromID(SPELL_ALTER_ARROW, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + real_t minRatio = getSpellDamageSecondaryFromID(SPELL_ALTER_ARROW, players[parentGUI.gui_player]->entity, stats[parentGUI.gui_player], players[parentGUI.gui_player]->entity) / 100.0; + goldCost *= std::max(minRatio, ratio); + manaCost = 5 + itemUsedWith->count / 10; + } + + if ( modeHasTransmuteMenu() && parentGUI.transmuteItemTarget != itemUsedWith ) + { + goldCost = 0; + manaCost = 0; + } + } +} + +bool GenericGUIMenu::ItemEffectGUI_t::modeHasTransmuteMenu() +{ + if ( currentMode == ITEMFX_MODE_ALTER_ARROW + || currentMode == ITEMFX_MODE_ALTER_INSTRUMENT + || currentMode == ITEMFX_MODE_RESHAPE_WEAPON + || currentMode == ITEMFX_MODE_FORGE_KEY ) + { + return true; + } + + return false; +} + GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectGUI_t::setItemDisplayNameAndPrice(Item* item, bool checkResultOnly) { auto result = ITEMFX_ACTION_NONE; - + if ( !checkResultOnly ) + { + costEffectMPAmount = 0; + costEffectGoldAmount = 0; + } if ( item ) { if ( currentMode == ITEMFX_MODE_SCROLL_IDENTIFY @@ -20915,6 +24619,31 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG result = ITEMFX_ACTION_ITEM_FULLY_CHARGED; } } + else if ( item->type == MAGICSTAFF_SCEPTER ) + { + if ( item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX < (MAGICSTAFF_SCEPTER_CHARGE_MAX - 1) ) + { + if ( itemIsEquipped(item, parentGUI.gui_player) ) + { + result = ITEMFX_ACTION_MUST_BE_UNEQUIPPED; + } + else + { + result = ITEMFX_ACTION_OK; + } + } + else + { + if ( item->status == EXCELLENT ) + { + result = ITEMFX_ACTION_ITEM_FULLY_CHARGED; + } + else + { + result = ITEMFX_ACTION_OK; + } + } + } else if ( itemCategory(item) == MAGICSTAFF ) { if ( item->status == EXCELLENT ) @@ -20943,9 +24672,26 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG } else { + int goldCost = 0; + int manaCost = 0; + getItemEffectCost(item, goldCost, manaCost); + + if ( !checkResultOnly ) + { + costEffectGoldAmount = goldCost; + costEffectMPAmount = manaCost; + } + if ( parentGUI.isItemEnchantArmorable(item) ) { - result = ITEMFX_ACTION_OK; + if ( goldCost > 0 && goldCost > stats[parentGUI.gui_player]->GOLD ) + { + result = ITEMFX_ACTION_CANT_AFFORD_GOLD; + } + else + { + result = ITEMFX_ACTION_OK; + } } else { @@ -20965,9 +24711,26 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG } else { + int goldCost = 0; + int manaCost = 0; + getItemEffectCost(item, goldCost, manaCost); + + if ( !checkResultOnly ) + { + costEffectGoldAmount = goldCost; + costEffectMPAmount = manaCost; + } + if ( parentGUI.isItemEnchantWeaponable(item) ) { - result = ITEMFX_ACTION_OK; + if ( goldCost > 0 && goldCost > stats[parentGUI.gui_player]->GOLD ) + { + result = ITEMFX_ACTION_CANT_AFFORD_GOLD; + } + else + { + result = ITEMFX_ACTION_OK; + } } else { @@ -20975,7 +24738,15 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG } } } - else if ( currentMode == ITEMFX_MODE_SCROLL_REPAIR ) + else if ( currentMode == ITEMFX_MODE_ALTER_INSTRUMENT + || currentMode == ITEMFX_MODE_METALLURGY + || currentMode == ITEMFX_MODE_GEOMANCY + || currentMode == ITEMFX_MODE_FORGE_KEY + || currentMode == ITEMFX_MODE_FORGE_JEWEL + || currentMode == ITEMFX_MODE_ENHANCE_WEAPON + || currentMode == ITEMFX_MODE_RESHAPE_WEAPON + || currentMode == ITEMFX_MODE_ALTER_ARROW + || currentMode == ITEMFX_MODE_VANDALISE ) { if ( itemCategory(item) == SPELL_CAT ) { @@ -20985,67 +24756,290 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG { result = ITEMFX_ACTION_NOT_IDENTIFIED_YET; } - else + else { - switch ( itemCategory(item) ) + int goldCost = 0; + int manaCost = 0; + getItemEffectCost(item, goldCost, manaCost); + + bool lowSkill = false; + if ( goldCost >= 1000000 ) { - case WEAPON: + lowSkill = true; + goldCost = goldCost % 1000000; + } + + if ( !checkResultOnly ) + { + costEffectGoldAmount = goldCost; + costEffectMPAmount = manaCost; + } + + if ( parentGUI.isItemAlterable(item) ) + { + if ( (currentMode == ITEMFX_MODE_RESHAPE_WEAPON || currentMode == ITEMFX_MODE_ENHANCE_WEAPON) + && lowSkill ) + { + result = ITEMFX_ACTION_NEED_SKILL_LVLS; + } + else if ( goldCost > 0 && goldCost > stats[parentGUI.gui_player]->GOLD ) + { + result = ITEMFX_ACTION_CANT_AFFORD_GOLD; + } + else if ( manaCost > 0 && manaCost > stats[parentGUI.gui_player]->MP && stats[parentGUI.gui_player]->type != VAMPIRE ) + { + result = ITEMFX_ACTION_CANT_AFFORD_MANA; + } + else if ( itemIsEquipped(item, parentGUI.gui_player) ) + { + result = ITEMFX_ACTION_MUST_BE_UNEQUIPPED; + } + else + { result = ITEMFX_ACTION_OK; - break; - case ARMOR: + } + } + else + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + } + } + else if ( currentMode == ITEMFX_MODE_PUNCTURE_VOID ) + { + if ( itemCategory(item) == SPELL_CAT ) + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + else if ( !item->identified ) + { + result = ITEMFX_ACTION_NOT_IDENTIFIED_YET; + } + else + { + if ( !parentGUI.isItemVoidable(item) ) + { + result = ITEMFX_ACTION_UNVOIDABLE; + } + else if ( itemIsEquipped(item, parentGUI.gui_player) ) + { + result = ITEMFX_ACTION_MUST_BE_UNEQUIPPED; + } + else + { + result = ITEMFX_ACTION_OK; + } + } + } + else if ( currentMode == ITEMFX_MODE_SANCTIFY_WATER ) + { + if ( itemCategory(item) == SPELL_CAT ) + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + else if ( !item->identified ) + { + result = ITEMFX_ACTION_NOT_IDENTIFIED_YET; + } + else + { + if ( parentGUI.isItemBlessWaterable(item) ) + { + int goldCost = 0; + int manaCost = 0; + getItemEffectCost(item, goldCost, manaCost); + if ( !checkResultOnly ) + { + costEffectGoldAmount = goldCost; + costEffectMPAmount = manaCost; + } + + if ( manaCost > 0 && manaCost > stats[parentGUI.gui_player]->MP && stats[parentGUI.gui_player]->type != VAMPIRE ) + { + result = ITEMFX_ACTION_CANT_AFFORD_MANA; + } + if ( itemIsEquipped(item, parentGUI.gui_player) ) + { + result = ITEMFX_ACTION_MUST_BE_UNEQUIPPED; + } + else + { result = ITEMFX_ACTION_OK; - break; - case MAGICSTAFF: - result = ITEMFX_ACTION_INVALID_ITEM; - break; - case THROWN: - if ( item->type == BOOMERANG ) + } + } + else + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + } + } + else if ( currentMode == ITEMFX_MODE_CLEANSE_FOOD ) + { + if ( itemCategory(item) == SPELL_CAT ) + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + else if ( !item->identified ) + { + result = ITEMFX_ACTION_NOT_IDENTIFIED_YET; + } + else + { + if ( parentGUI.isItemCleaseFoodable(item) ) + { + /*if ( item->status == EXCELLENT && item->beatitude >= 1 ) + { + result = ITEMFX_ACTION_AT_MAX_BLESSING; + } + else */ + if ( itemIsEquipped(item, parentGUI.gui_player) ) + { + result = ITEMFX_ACTION_MUST_BE_UNEQUIPPED; + } + else + { + result = ITEMFX_ACTION_OK; + } + } + else + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + } + } + else if ( currentMode == ITEMFX_MODE_SANCTIFY ) + { + if ( itemCategory(item) == SPELL_CAT ) + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + else if ( !item->identified ) + { + result = ITEMFX_ACTION_NOT_IDENTIFIED_YET; + } + else + { + if ( parentGUI.isItemSanctifiable(item) ) + { + result = ITEMFX_ACTION_OK; + } + else + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + } + } + else if ( currentMode == ITEMFX_MODE_DESECRATE ) + { + if ( itemCategory(item) == SPELL_CAT ) + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + else if ( !item->identified ) + { + result = ITEMFX_ACTION_NOT_IDENTIFIED_YET; + } + else + { + if ( parentGUI.isItemDesecratable(item) ) + { + result = ITEMFX_ACTION_OK; + } + else + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + } + } + else if ( currentMode == ITEMFX_MODE_ADORCISE_WEAPON + || currentMode == ITEMFX_MODE_ADORCISE_INSTRUMENT ) + { + if ( itemCategory(item) == SPELL_CAT ) + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + else if ( !item->identified ) + { + result = ITEMFX_ACTION_NOT_IDENTIFIED_YET; + } + else + { + if ( !parentGUI.isItemAdorcisable(item) ) + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + else if ( itemIsEquipped(item, parentGUI.gui_player) ) + { + result = ITEMFX_ACTION_MUST_BE_UNEQUIPPED; + } + else + { + result = ITEMFX_ACTION_OK; + } + } + } + else if ( currentMode == ITEMFX_MODE_SCEPTER_CHARGE ) + { + if ( itemCategory(item) == SPELL_CAT ) + { + result = ITEMFX_ACTION_OK; + } + else + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + } + else if ( currentMode == ITEMFX_MODE_SCROLL_REPAIR + || currentMode == ITEMFX_MODE_RESTORE ) + { + if ( itemCategory(item) == SPELL_CAT ) + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + else if ( !item->identified ) + { + result = ITEMFX_ACTION_NOT_IDENTIFIED_YET; + } + else + { + if ( parentGUI.isItemRepairable(item, SCROLL_REPAIR) ) + { + result = ITEMFX_ACTION_OK; + } + else + { + result = ITEMFX_ACTION_INVALID_ITEM; + } + + if ( currentMode == ITEMFX_MODE_RESTORE ) + { + if ( result == ITEMFX_ACTION_OK ) + { + int goldCost = 0; + int manaCost = 0; + getItemEffectCost(item, goldCost, manaCost); + if ( !checkResultOnly ) { - result = ITEMFX_ACTION_OK; + costEffectGoldAmount = goldCost; + costEffectMPAmount = manaCost; } - else + + if ( goldCost > 0 && goldCost > stats[parentGUI.gui_player]->GOLD ) { - result = ITEMFX_ACTION_INVALID_ITEM; + result = ITEMFX_ACTION_CANT_AFFORD_GOLD; } - break; - case TOOL: - switch ( item->type ) - { - case TOOL_TOWEL: - case TOOL_MIRROR: - case TOOL_SKELETONKEY: - case TOOL_TINOPENER: - case TOOL_METAL_SCRAP: - case TOOL_MAGIC_SCRAP: - case TOOL_TINKERING_KIT: - case TOOL_SENTRYBOT: - case TOOL_DETONATOR_CHARGE: - case TOOL_BOMB: - case TOOL_SLEEP_BOMB: - case TOOL_FREEZE_BOMB: - case TOOL_TELEPORT_BOMB: - case TOOL_GYROBOT: - case TOOL_SPELLBOT: - case TOOL_DECOY: - case TOOL_DUMMYBOT: - case ENCHANTED_FEATHER: - result = ITEMFX_ACTION_INVALID_ITEM; - break; - default: - if ( itemTypeIsQuiver(item->type) ) - { - result = ITEMFX_ACTION_INVALID_ITEM; - } - else - { - result = ITEMFX_ACTION_OK; - } - break; + if ( manaCost > 0 && manaCost > stats[parentGUI.gui_player]->MP && stats[parentGUI.gui_player]->type != VAMPIRE ) + { + if ( result == ITEMFX_ACTION_CANT_AFFORD_GOLD ) + { + result = ITEMFX_ACTION_CANT_AFFORD_MANA_AND_GOLD; + } + else + { + result = ITEMFX_ACTION_CANT_AFFORD_MANA; + } } - break; - default: - result = ITEMFX_ACTION_INVALID_ITEM; - break; + } } if ( result == ITEMFX_ACTION_OK ) @@ -21068,8 +25062,26 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG { if ( result != ITEMFX_ACTION_NONE && item ) { + ItemType prevItemType = item->type; + if ( parentGUI.transmuteItemTarget == item ) + { + int result = parentGUI.getAlterItemResultAtCycle(item); + if ( result >= 0 ) + { + item->type = (ItemType)(result); + } + } + char buf[1024]; - if ( !item->identified ) + if ( item->type == SPELL_ITEM ) + { + if ( auto spell = getSpellFromItem(parentGUI.gui_player, item, false) ) + { + snprintf(buf, sizeof(buf), "%s%s", + ItemTooltips.adjectives["spell_prefixes"]["spell_of"].c_str(), spell->getSpellName()); + } + } + else if ( !item->identified ) { snprintf(buf, sizeof(buf), "%s %s (?)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName()); } @@ -21094,6 +25106,11 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG snprintf(buf, sizeof(buf), "%s %s %d%% (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY, item->beatitude); } + else if ( item->type == MAGICSTAFF_SCEPTER && item->identified ) + { + snprintf(buf, sizeof(buf), "%s %s %d%% (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), + item->getName(), item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX, item->beatitude); + } else { snprintf(buf, sizeof(buf), "%s %s (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->beatitude); @@ -21104,7 +25121,6 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG itemRequiresTitleReflow = true; } itemDesc = buf; - itemType = item->type; if ( itemEffectFrame ) { @@ -21117,15 +25133,27 @@ GenericGUIMenu::ItemEffectGUI_t::ItemEffectActions_t GenericGUIMenu::ItemEffectG } } } + + itemType = item->type; + item->type = prevItemType; } itemActionType = result; + if ( itemActionType == ITEMFX_ACTION_OK ) + { + if ( confirmActionOnItemSteps.first != item->uid ) + { + confirmActionOnItemSteps.second = 0; + } + confirmActionOnItemSteps.first = item->uid; + } } return result; } bool GenericGUIMenu::ItemEffectGUI_t::isItemSelectedToEffect(Item* item) { - if ( !item || itemCategory(item) == SPELL_CAT ) + if ( !item || (itemCategory(item) == SPELL_CAT && currentMode != ITEMFX_MODE_SCEPTER_CHARGE) + || (itemCategory(item) != SPELL_CAT && currentMode == ITEMFX_MODE_SCEPTER_CHARGE) ) { return false; } @@ -21135,7 +25163,26 @@ bool GenericGUIMenu::ItemEffectGUI_t::isItemSelectedToEffect(Item* item) return false; } - if ( players[parentGUI.getPlayer()]->GUI.activeModule == Player::GUI_t::MODULE_INVENTORY ) + if ( players[parentGUI.getPlayer()]->GUI.activeModule == Player::GUI_t::MODULE_SPELLS ) + { + auto& inventoryUI = players[parentGUI.getPlayer()]->inventoryUI; + if ( inventoryUI.getSelectedSpellX() >= 0 + && inventoryUI.getSelectedSpellX() < inventoryUI.MAX_SPELLS_X + && inventoryUI.getSelectedSpellY() >= 0 + && inventoryUI.getSelectedSpellY() < inventoryUI.MAX_SPELLS_Y + && item->x == inventoryUI.getSelectedSpellX() && item->y == inventoryUI.getSelectedSpellY() ) + { + if ( auto slotFrame = inventoryUI.getSpellSlotFrame(item->x, item->y) ) + { + return slotFrame->capturesMouse(); + } + else + { + return false; + } + } + } + else if ( players[parentGUI.getPlayer()]->GUI.activeModule == Player::GUI_t::MODULE_INVENTORY ) { auto& inventoryUI = players[parentGUI.getPlayer()]->inventoryUI; auto& paperDoll = players[parentGUI.getPlayer()]->paperDoll; @@ -21184,6 +25231,37 @@ void GenericGUIMenu::ItemEffectGUI_t::openItemEffectMenu(GenericGUIMenu::ItemEff auto player = players[playernum]; currentMode = mode; + modeHasCostEffect = COST_EFFECT_NONE; + switch ( currentMode ) + { + case ITEMFX_MODE_METALLURGY: + case ITEMFX_MODE_GEOMANCY: + modeHasCostEffect = COST_EFFECT_MANA_RETURN_GOLD; + break; + case ITEMFX_MODE_VANDALISE: + modeHasCostEffect = COST_EFFECT_MANA_RETURN_GOLD; + break; + case ITEMFX_MODE_SCROLL_ENCHANT_ARMOR: + case ITEMFX_MODE_SCROLL_ENCHANT_WEAPON: + modeHasCostEffect = COST_EFFECT_GOLD; + break; + case ITEMFX_MODE_SANCTIFY_WATER: + modeHasCostEffect = COST_EFFECT_MANA; + break; + case ITEMFX_MODE_FORGE_KEY: + case ITEMFX_MODE_FORGE_JEWEL: + case ITEMFX_MODE_RESTORE: + modeHasCostEffect = COST_EFFECT_MANA_AND_GOLD; + break; + case ITEMFX_MODE_ENHANCE_WEAPON: + case ITEMFX_MODE_ALTER_INSTRUMENT: + case ITEMFX_MODE_RESHAPE_WEAPON: + case ITEMFX_MODE_ALTER_ARROW: + modeHasCostEffect = COST_EFFECT_MANA_AND_GOLD; + break; + default: + break; + } if ( itemEffectFrame ) { bool wasDisabled = itemEffectFrame->isDisabled(); @@ -21199,6 +25277,10 @@ void GenericGUIMenu::ItemEffectGUI_t::openItemEffectMenu(GenericGUIMenu::ItemEff } player->hud.compactLayoutMode = Player::HUD_t::COMPACT_LAYOUT_INVENTORY; player->inventory_mode = INVENTORY_MODE_ITEM; + if ( currentMode == ITEMFX_MODE_SCEPTER_CHARGE ) + { + player->inventoryUI.cycleInventoryTab(); + } bOpen = true; } if ( inputs.getUIInteraction(playernum)->selectedItem ) @@ -21208,6 +25290,10 @@ void GenericGUIMenu::ItemEffectGUI_t::openItemEffectMenu(GenericGUIMenu::ItemEff } inputs.getUIInteraction(playernum)->selectedItemFromChest = 0; clearItemDisplayed(); + confirmActionOnItemSteps.first = 0; + confirmActionOnItemSteps.second = 0; + parentGUI.transmuteItemTarget = nullptr; + parentGUI.transmuteItemScroll = 0; } void GenericGUIMenu::ItemEffectGUI_t::closeItemEffectMenu() @@ -21226,6 +25312,14 @@ void GenericGUIMenu::ItemEffectGUI_t::closeItemEffectMenu() animInvalidAction = 0.0; animInvalidActionTicks = 0; panelJustifyInverted = false; + modeHasCostEffect = COST_EFFECT_NONE; + if ( currentMode == ItemEffectGUI_t::ITEMFX_MODE_SCEPTER_CHARGE ) + { + if ( players[playernum]->inventory_mode == INVENTORY_MODE_SPELL ) + { + players[playernum]->inventory_mode = INVENTORY_MODE_ITEM; + } + } currentMode = ITEMFX_MODE_NONE; invalidActionType = INVALID_ACTION_NONE; isInteractable = false; @@ -21241,7 +25335,9 @@ void GenericGUIMenu::ItemEffectGUI_t::closeItemEffectMenu() } inputs.getUIInteraction(playernum)->selectedItemFromChest = 0; } - if ( players[playernum]->GUI.activeModule == Player::GUI_t::MODULE_ITEMEFFECTGUI + + if ( (players[playernum]->GUI.activeModule == Player::GUI_t::MODULE_ITEMEFFECTGUI + || players[playernum]->GUI.activeModule == Player::GUI_t::MODULE_SPELLS) && !players[playernum]->shootmode ) { // reset to inventory mode if still hanging in itemeffect GUI @@ -21253,6 +25349,10 @@ void GenericGUIMenu::ItemEffectGUI_t::closeItemEffectMenu() } } clearItemDisplayed(); + confirmActionOnItemSteps.first = 0; + confirmActionOnItemSteps.second = 0; + parentGUI.transmuteItemTarget = nullptr; + parentGUI.transmuteItemScroll = 0; itemRequiresTitleReflow = true; if ( itemEffectFrame ) { @@ -21495,6 +25595,13 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() return; } + if ( currentMode == ITEMFX_MODE_SCEPTER_CHARGE && player->inventory_mode != INVENTORY_MODE_SPELL ) + { + closeItemEffectMenu(); + parentGUI.closeGUI(); + return; + } + if ( player->entity && player->entity->isBlind() ) { messagePlayer(playernum, MESSAGE_MISC, Language::get(4159)); @@ -21551,7 +25658,11 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() } else if ( item ) { - if ( !item->identified ) + if ( item->type == MAGICSTAFF_SCEPTER ) + { + snprintf(buf, sizeof(buf), "%s", Language::get(6833)); + } + else if ( !item->identified ) { std::string prefix = ItemTooltips.adjectives["scroll_prefixes"]["unknown_scroll"].c_str(); snprintf(buf, sizeof(buf), "%s (?)", prefix.c_str()); @@ -21584,7 +25695,15 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() scrollShortName = scrollShortName.substr(ItemTooltips.adjectives["scroll_prefixes"]["piece_of"].size()); } camelCaseString(scrollShortName); - snprintf(buf, sizeof(buf), "%s (%+d)", scrollShortName.c_str(), item->beatitude); + if ( item->type == MAGICSTAFF_SCEPTER ) + { + snprintf(buf, sizeof(buf), "%s (%d%%)", scrollShortName.c_str(), + item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX); + } + else + { + snprintf(buf, sizeof(buf), "%s (%+d)", scrollShortName.c_str(), item->beatitude); + } itemFxStatus->setText(buf); } else @@ -21610,16 +25729,16 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() skillIcon->pos.y = 56 + heightOffsetCompact; for ( auto& skill : Player::SkillSheet_t::skillSheetData.skillEntries ) { - if ( skill.skillId == PRO_MAGIC ) + if ( skill.skillId == PRO_LEGACY_MAGIC ) { - if ( skillCapstoneUnlocked(playernum, skill.skillId) ) + /*if ( skillCapstoneUnlocked(playernum, skill.skillId) ) { skillIcon->path = skill.skillIconPathLegend; } else { - skillIcon->path = skill.skillIconPath; - } + }*/ + skillIcon->path = skill.skillIconPath; skillIcon->disabled = false; break; } @@ -21693,6 +25812,29 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() } } } + else if ( currentMode == ITEMFX_MODE_SCEPTER_CHARGE ) + { + if ( parentGUI.itemEffectScrollItem ) + { + itemIcon->path = getItemSpritePath(parentGUI.gui_player, *parentGUI.itemEffectScrollItem); + if ( auto imgGet = Image::get(itemIcon->path.c_str()) ) + { + itemIcon->pos.w = imgGet->getWidth(); + itemIcon->pos.h = imgGet->getHeight(); + itemIcon->disabled = false; + itemIcon->pos.x = 48 - itemIcon->pos.w / 2; + itemIcon->pos.y = 68 + heightOffsetCompact - itemIcon->pos.h / 2; + if ( itemIcon->pos.x % 2 == 1 ) + { + ++itemIcon->pos.x; + } + if ( itemIcon->pos.y % 2 == 1 ) + { + ++itemIcon->pos.y; + } + } + } + } else { itemIcon->path = "*images/ui/ScrollSpells/Scroll_Icon_Scroll_00.png"; @@ -21737,13 +25879,13 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() bool usingGamepad = inputs.hasController(playernum) && !inputs.getVirtualMouse(playernum)->draw_cursor; + auto closeGlyph = baseFrame->findImage("close itemfx glyph"); { // close btn auto closeBtn = baseFrame->findButton("close itemfx button"); SDL_Rect closeBtnPos = closeBtn->getSize(); closeBtnPos.y = 34 + heightOffsetCompact; closeBtn->setSize(closeBtnPos); - auto closeGlyph = baseFrame->findImage("close itemfx glyph"); closeBtn->setDisabled(true); closeGlyph->disabled = true; if ( inputs.getVirtualMouse(playernum)->draw_cursor ) @@ -21751,7 +25893,7 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() closeBtn->setDisabled(!isInteractable); if ( isInteractable ) { - buttonTinkerUpdateSelectorOnHighlight(playernum, closeBtn); + buttonItemfxSelectorOnHighlight(playernum, closeBtn); } } else if ( closeBtn->isSelected() ) @@ -21782,6 +25924,12 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() auto actionPromptTxt = baseFrame->findField("action prompt txt"); actionPromptTxt->setDisabled(false); auto actionPromptImg = baseFrame->findImage("action prompt glyph"); + auto actionConfirmImg = baseFrame->findImage("action confirm glyph"); + actionConfirmImg->disabled = true; + auto actionRefreshImg = baseFrame->findImage("action refresh glyph"); + actionRefreshImg->disabled = true; + auto actionCancelImg = baseFrame->findImage("action cancel glyph"); + actionCancelImg->disabled = true; //auto actionModifierImg = baseFrame->findImage("action modifier glyph"); Uint32 negativeColor = hudColors.characterSheetRed; @@ -21792,6 +25940,12 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() auto displayItemName = itemDisplayTooltip->findField("item display name"); auto displayItemTextImg = itemDisplayTooltip->findImage("item text img"); auto itemSlotBg = itemDisplayTooltip->findImage("item bg img"); + auto itemTransmuteGlow = itemDisplayTooltip->findImage("item transmute gleam img"); + auto costEffectGoldText = itemDisplayTooltip->findField("item gold value"); + auto costEffectManaText = itemDisplayTooltip->findField("item mp value"); + itemTransmuteGlow->disabled = true; + itemTransmuteGlow->pos.x = 16; + itemTransmuteGlow->pos.y = 16; itemSlotBg->pos.x = 12; itemSlotBg->pos.y = 12; const int displayItemTextImgBaseX = itemSlotBg->pos.x + itemSlotBg->pos.w; @@ -21802,6 +25956,16 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() displayItemName->setSize(displayItemNamePos); static ConsoleVariable cvar_itemfxPromptY("/itemfx_action_prompt_y", -2); SDL_Rect actionPromptTxtPos{ 0, 205 + *cvar_itemfxPromptY + heightOffsetCompact, baseFrame->getSize().w - 18 - 8, 24 }; + if ( modeHasCostEffect == COST_EFFECT_GOLD + || modeHasCostEffect == COST_EFFECT_MANA ) + { + actionPromptTxtPos.y += 26; + } + else if ( modeHasCostEffect == COST_EFFECT_MANA_RETURN_GOLD + || modeHasCostEffect == COST_EFFECT_MANA_AND_GOLD ) + { + actionPromptTxtPos.y += 52; + } actionPromptTxt->setSize(actionPromptTxtPos); SDL_Rect tooltipPos = itemDisplayTooltip->getSize(); @@ -21811,6 +25975,78 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() tooltipPos.x = 18 - (tooltipPos.w + 18) * (0.0/*1.0 - animTooltip*/); itemDisplayTooltip->setSize(tooltipPos); + auto costBg = itemDisplayTooltip->findImage("item cost img"); + auto costLabel = itemDisplayTooltip->findField("item cost label"); + auto costBg2 = itemDisplayTooltip->findImage("item cost img 2"); + auto costLabel2 = itemDisplayTooltip->findField("item cost label 2"); + if ( modeHasCostEffect == COST_EFFECT_NONE ) + { + costBg->disabled = true; + costLabel->setDisabled(true); + costEffectGoldText->setDisabled(true); + costEffectManaText->setDisabled(true); + } + else + { + int align_x = displayItemTextImgBaseX + displayItemTextImg->pos.w - costBg->pos.w; + int align_y = displayItemTextImg->pos.y + displayItemTextImg->pos.h + 4; + + if ( modeHasCostEffect == COST_EFFECT_MANA + || modeHasCostEffect == COST_EFFECT_MANA_RETURN_GOLD + || modeHasCostEffect == COST_EFFECT_MANA_AND_GOLD ) + { + costBg2->pos.x = align_x; + costBg2->pos.y = align_y; + costBg2->disabled = false; + + SDL_Rect goldPos{ costBg2->pos.x + 28, costBg2->pos.y + 9, 66, 24 }; + costEffectManaText->setSize(goldPos); + costEffectManaText->setDisabled(false); + + SDL_Rect costLabelTxtPos = costLabel2->getSize(); + costLabelTxtPos.w = costBg2->pos.x - 4; + costLabelTxtPos.x = 0; + costLabelTxtPos.y = goldPos.y; + costLabelTxtPos.h = 24; + costLabel2->setSize(costLabelTxtPos); + costLabel2->setDisabled(false); + align_y += 28; + + costLabel2->setText(Language::get(6555)); + } + + if ( modeHasCostEffect == COST_EFFECT_GOLD + || modeHasCostEffect == COST_EFFECT_MANA_RETURN_GOLD + || modeHasCostEffect == COST_EFFECT_MANA_AND_GOLD ) + { + costBg->pos.x = align_x; + costBg->pos.y = align_y; + costBg->disabled = false; + + SDL_Rect goldPos{ costBg->pos.x + 28, costBg->pos.y + 9, 66, 24 }; + costEffectGoldText->setSize(goldPos); + costEffectGoldText->setDisabled(false); + + SDL_Rect costLabelTxtPos = costLabel->getSize(); + costLabelTxtPos.w = costBg->pos.x - 4; + costLabelTxtPos.x = 0; + costLabelTxtPos.y = goldPos.y; + costLabelTxtPos.h = 24; + costLabel->setSize(costLabelTxtPos); + costLabel->setDisabled(false); + + if ( modeHasCostEffect == COST_EFFECT_MANA_RETURN_GOLD ) + { + costLabel->setText(Language::get(6544)); + } + else + { + costLabel->setText(Language::get(6538)); + } + } + + } + auto itemSlotFrame = itemDisplayTooltip->findFrame("item slot frame"); bool modifierPressed = false; if ( usingGamepad && Input::inputs[playernum].binary("MenuPageLeftAlt") ) @@ -21823,6 +26059,223 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() modifierPressed = true; } + if ( itemActionType != ITEMFX_ACTION_OK ) + { + confirmActionOnItemSteps.first = 0; + confirmActionOnItemSteps.second = 0; + } + + auto actionButtonRefresh = baseFrame->findButton("action button refresh"); + auto actionButtonCancel = baseFrame->findButton("action button cancel"); + { + if ( parentGUI.transmuteItemTarget && itemIsEquipped(parentGUI.transmuteItemTarget, playernum) ) + { + parentGUI.transmuteItemTarget = nullptr; + } + + // action btn + SDL_Rect btnPos = actionButtonRefresh->getSize(); + btnPos.x = 38; + btnPos.y = 196; + actionButtonRefresh->setSize(btnPos); + //auto closeGlyph = baseFrame->findImage("close itemfx glyph"); + actionButtonRefresh->setDisabled(true); + //closeGlyph->disabled = true; + if ( inputs.getVirtualMouse(playernum)->draw_cursor && parentGUI.transmuteItemTarget && isInteractable ) + { + actionButtonRefresh->setDisabled(!isInteractable); + if ( isInteractable ) + { + buttonItemfxSelectorOnHighlight(playernum, actionButtonRefresh); + } + } + else if ( actionButtonRefresh->isSelected() ) + { + actionButtonRefresh->deselect(); + } + + actionButtonRefresh->setInvisible(actionButtonRefresh->isDisabled()); + actionButtonRefresh->setStyle(Button::STYLE_NORMAL); + if ( usingGamepad ) + { + actionButtonRefresh->setStyle(Button::STYLE_TOGGLE); + if ( parentGUI.transmuteItemTarget && isInteractable ) + { + actionButtonRefresh->setInvisible(false); + actionButtonRefresh->setDisabled(true); + + btnPos = actionButtonRefresh->getSize(); + btnPos.x += 16; + actionButtonRefresh->setSize(btnPos); + + actionRefreshImg->path = Input::inputs[playernum].getGlyphPathForBinding("MenuAlt2"); + if ( auto imgGet = Image::get(actionRefreshImg->path.c_str()) ) + { + actionRefreshImg->pos.w = imgGet->getWidth(); + actionRefreshImg->pos.h = imgGet->getHeight(); + actionRefreshImg->disabled = false; + } + actionRefreshImg->pos.x = actionButtonRefresh->getSize().x - actionRefreshImg->pos.w - 4; + if ( actionRefreshImg->pos.x % 2 == 1 ) + { + ++actionRefreshImg->pos.x; + } + actionRefreshImg->pos.y = actionButtonRefresh->getSize().y + actionButtonRefresh->getSize().h / 2 - actionRefreshImg->pos.h / 2; + if ( actionRefreshImg->pos.y % 2 == 1 ) + { + --actionRefreshImg->pos.y; + } + } + } + + if ( !actionButtonRefresh->isInvisible() && parentGUI.transmuteItemTarget ) + { + setItemDisplayNameAndPrice(parentGUI.transmuteItemTarget); + + itemTransmuteGlow->disabled = false; + itemTransmuteGlow->path = "*#images/ui/ScrollSpells/Gleam_00.png"; + const int gleam = ((ticks % TICKS_PER_SECOND) / 5) % 5; + switch ( gleam ) + { + case 0: + itemTransmuteGlow->path = "*#images/ui/ScrollSpells/Gleam_00.png"; + break; + case 1: + itemTransmuteGlow->path = "*#images/ui/ScrollSpells/Gleam_01.png"; + break; + case 2: + itemTransmuteGlow->path = "*#images/ui/ScrollSpells/Gleam_02.png"; + break; + case 3: + itemTransmuteGlow->path = "*#images/ui/ScrollSpells/Gleam_03.png"; + break; + case 4: + itemTransmuteGlow->path = "*#images/ui/ScrollSpells/Gleam_04.png"; + break; + default: + break; + } + } + + { + actionButtonCancel->setInvisible(true); + actionButtonCancel->setDisabled(true); + actionButtonCancel->setText(Language::get(6889)); + // cancel btn + SDL_Rect btnPos = actionButtonCancel->getSize(); + btnPos.x = 26; + btnPos.y = actionPromptTxtPos.y - 3; + actionButtonCancel->setSize(btnPos); + //auto closeGlyph = baseFrame->findImage("close itemfx glyph"); + actionButtonCancel->setDisabled(true); + //closeGlyph->disabled = true; + if ( inputs.getVirtualMouse(playernum)->draw_cursor && parentGUI.transmuteItemTarget && isInteractable ) + { + actionButtonCancel->setDisabled(!isInteractable); + if ( isInteractable ) + { + buttonItemfxSelectorOnHighlight(playernum, actionButtonCancel); + } + } + else if ( actionButtonCancel->isSelected() ) + { + actionButtonCancel->deselect(); + } + + actionButtonCancel->setInvisible(actionButtonCancel->isDisabled()); + + if ( usingGamepad ) + { + if ( parentGUI.transmuteItemTarget && isInteractable ) + { + actionButtonCancel->setInvisible(false); + actionButtonCancel->setDisabled(true); + + actionCancelImg->path = Input::inputs[playernum].getGlyphPathForBinding("MenuCancel"); + if ( auto imgGet = Image::get(actionCancelImg->path.c_str()) ) + { + actionCancelImg->pos.w = imgGet->getWidth(); + actionCancelImg->pos.h = imgGet->getHeight(); + actionCancelImg->disabled = false; + closeGlyph->disabled = true; + } + actionCancelImg->pos.x = actionButtonCancel->getSize().x + actionButtonCancel->getSize().w + 4; + if ( actionCancelImg->pos.x % 2 == 1 ) + { + ++actionCancelImg->pos.x; + } + actionCancelImg->pos.y = actionButtonCancel->getSize().y + actionButtonCancel->getSize().h / 2 - actionCancelImg->pos.h / 2 + 2; + if ( actionCancelImg->pos.y % 2 == 1 ) + { + --actionCancelImg->pos.y; + } + } + } + } + } + + auto actionButtonConfirm = baseFrame->findButton("action button confirm"); + { + // action btn + actionButtonConfirm->setText(Language::get(6888)); + SDL_Rect btnPos = actionButtonConfirm->getSize(); + btnPos.x = actionPromptTxtPos.x + actionPromptTxtPos.w - btnPos.w; + btnPos.y = actionPromptTxtPos.y - 3; + actionButtonConfirm->setSize(btnPos); + //auto closeGlyph = baseFrame->findImage("close itemfx glyph"); + actionButtonConfirm->setDisabled(true); + //closeGlyph->disabled = true; + if ( inputs.getVirtualMouse(playernum)->draw_cursor && parentGUI.transmuteItemTarget && itemActionType == ITEMFX_ACTION_OK && isInteractable ) + { + actionButtonConfirm->setDisabled(!isInteractable); + if ( isInteractable ) + { + buttonItemfxSelectorOnHighlight(playernum, actionButtonConfirm); + } + } + else if ( actionButtonConfirm->isSelected() ) + { + actionButtonConfirm->deselect(); + } + + actionButtonConfirm->setInvisible(actionButtonConfirm->isDisabled()); + /*if ( usingGamepad ) + { + if ( parentGUI.transmuteItemTarget && itemActionType == ITEMFX_ACTION_OK && isInteractable ) + { + actionButtonConfirm->setInvisible(false); + actionButtonConfirm->setDisabled(true); + + actionConfirmImg->path = Input::inputs[playernum].getGlyphPathForBinding("MenuConfirm"); + if ( auto imgGet = Image::get(actionConfirmImg->path.c_str()) ) + { + actionConfirmImg->pos.w = imgGet->getWidth(); + actionConfirmImg->pos.h = imgGet->getHeight(); + actionConfirmImg->disabled = false; + } + actionConfirmImg->pos.x = actionButtonConfirm->getSize().x - actionConfirmImg->pos.w - 4; + if ( actionConfirmImg->pos.x % 2 == 1 ) + { + ++actionConfirmImg->pos.x; + } + actionConfirmImg->pos.y = actionButtonConfirm->getSize().y + actionButtonConfirm->getSize().h / 2 - actionConfirmImg->pos.h / 2 + 2; + if ( actionConfirmImg->pos.y % 2 == 1 ) + { + --actionConfirmImg->pos.y; + } + } + actionButtonConfirm->setHJustify(Field::justify_t::RIGHT); + actionButtonConfirm->setColor(makeColor(255, 255, 255, 0)); + actionButtonConfirm->setHighlightColor(makeColor(255, 255, 255, 0)); + } + else + { + actionButtonConfirm->setHJustify(Field::justify_t::CENTER); + actionButtonConfirm->setColor(makeColor(255, 255, 255, 255)); + actionButtonConfirm->setHighlightColor(makeColor(255, 255, 255, 255)); + }*/ + } + if ( itemActionType != ITEMFX_ACTION_NONE && itemDesc.size() > 1 ) { if ( isInteractable ) @@ -21877,12 +26330,17 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() actionModifierImg->disabled = true; }*/ } + + actionPromptTxt->setDisabled(!actionButtonConfirm->isInvisible()); + actionPromptImg->disabled = !actionButtonConfirm->isInvisible(); + switch ( currentMode ) { case ITEMFX_MODE_NONE: actionPromptTxt->setText(""); break; case ITEMFX_MODE_SCROLL_REPAIR: + case ITEMFX_MODE_RESTORE: actionPromptTxt->setText(Language::get(4202)); break; case ITEMFX_MODE_SCROLL_CHARGING: @@ -21900,16 +26358,82 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() case ITEMFX_MODE_SPELL_IDENTIFY: actionPromptTxt->setText(Language::get(4208)); break; + case ITEMFX_MODE_ALTER_INSTRUMENT: + actionPromptTxt->setText(Language::get(6512)); + break; case ITEMFX_MODE_SCROLL_REMOVECURSE: actionPromptTxt->setText(Language::get(4204)); break; case ITEMFX_MODE_SPELL_REMOVECURSE: actionPromptTxt->setText(Language::get(4204)); break; + case ITEMFX_MODE_METALLURGY: + actionPromptTxt->setText(Language::get(6545)); + break; + case ITEMFX_MODE_GEOMANCY: + actionPromptTxt->setText(Language::get(6546)); + break; + case ITEMFX_MODE_FORGE_KEY: + actionPromptTxt->setText(Language::get(6547)); + break; + case ITEMFX_MODE_FORGE_JEWEL: + actionPromptTxt->setText(Language::get(6548)); + break; + case ITEMFX_MODE_ENHANCE_WEAPON: + actionPromptTxt->setText(Language::get(6549)); + break; + case ITEMFX_MODE_RESHAPE_WEAPON: + actionPromptTxt->setText(Language::get(6550)); + break; + case ITEMFX_MODE_ALTER_ARROW: + actionPromptTxt->setText(Language::get(6551)); + break; + case ITEMFX_MODE_PUNCTURE_VOID: + actionPromptTxt->setText(Language::get(6561)); + break; + case ITEMFX_MODE_ADORCISE_WEAPON: + actionPromptTxt->setText(Language::get(6616)); + break; + case ITEMFX_MODE_VANDALISE: + actionPromptTxt->setText(Language::get(6708)); + break; + case ITEMFX_MODE_DESECRATE: + actionPromptTxt->setText(Language::get(6709)); + break; + case ITEMFX_MODE_SANCTIFY: + actionPromptTxt->setText(Language::get(6710)); + break; + case ITEMFX_MODE_SANCTIFY_WATER: + actionPromptTxt->setText(Language::get(6711)); + break; + case ITEMFX_MODE_CLEANSE_FOOD: + actionPromptTxt->setText(Language::get(6712)); + break; + case ITEMFX_MODE_ADORCISE_INSTRUMENT: + actionPromptTxt->setText(Language::get(6616)); + break; + case ITEMFX_MODE_SCEPTER_CHARGE: + actionPromptTxt->setText(Language::get(6831)); + if ( confirmActionOnItemSteps.second > 0 ) + { + char buf[128]; + snprintf(buf, sizeof(buf), Language::get(6835), 3 - confirmActionOnItemSteps.second); + actionPromptTxt->setText(buf); + } + break; default: actionPromptTxt->setText(""); break; } + + if ( modeHasTransmuteMenu() ) + { + if ( parentGUI.transmuteItemTarget ) + { + actionPromptTxt->setText(Language::get(6888)); + } + } + actionPromptTxt->setColor(defaultPromptColor); } else @@ -21934,7 +26458,7 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() actionPromptTxt->setText(Language::get(4212)); break; case ITEMFX_ACTION_MUST_BE_UNEQUIPPED: - actionPromptTxt->setText(Language::get(4136)); + actionPromptTxt->setText(Language::get(4132)); break; case ITEMFX_ACTION_NOT_IDENTIFIED_YET: actionPromptTxt->setText(Language::get(4153)); @@ -21942,6 +26466,45 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() case ITEMFX_ACTION_NOT_CURSED: actionPromptTxt->setText(Language::get(4213)); break; + case ITEMFX_ACTION_CANT_AFFORD_GOLD: + actionPromptTxt->setText(Language::get(6539)); + break; + case ITEMFX_ACTION_CANT_AFFORD_MANA: + actionPromptTxt->setText(Language::get(6552)); + break; + case ITEMFX_ACTION_CANT_AFFORD_MANA_AND_GOLD: + actionPromptTxt->setText(Language::get(6855)); + break; + case ITEMFX_ACTION_UNVOIDABLE: + actionPromptTxt->setText(Language::get(6563)); + break; + case ITEMFX_ACTION_AT_MAX_BLESSING: + actionPromptTxt->setText(Language::get(6726)); + break; + case ITEMFX_ACTION_NEED_SKILL_LVLS: + if ( currentMode == ITEMFX_MODE_RESHAPE_WEAPON ) + { + if ( auto spell = getSpellFromID(SPELL_RESHAPE_WEAPON) ) + { + char buf[128] = ""; + snprintf(buf, sizeof(buf), Language::get(6804), getSkillLangEntry(spell->skillID)); + actionPromptTxt->setText(buf); + } + } + else if ( currentMode == ITEMFX_MODE_ENHANCE_WEAPON ) + { + if ( auto spell = getSpellFromID(SPELL_ENHANCE_WEAPON) ) + { + char buf[128] = ""; + snprintf(buf, sizeof(buf), Language::get(6804), getSkillLangEntry(spell->skillID)); + actionPromptTxt->setText(buf); + } + } + else + { + actionPromptTxt->setText(Language::get(6805)); + } + break; default: actionPromptTxt->setText("-"); break; @@ -22004,6 +26567,52 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() itemRequiresTitleReflow = false; } } + + { + // charge costs + costEffectGoldText->setColor(neutralColor); + costEffectManaText->setColor(neutralColor); + costEffectGoldText->setText(""); + if ( itemActionType == ITEMFX_ACTION_OK + || itemActionType == ITEMFX_ACTION_CANT_AFFORD_GOLD + || itemActionType == ITEMFX_ACTION_CANT_AFFORD_MANA + || itemActionType == ITEMFX_ACTION_CANT_AFFORD_MANA_AND_GOLD + || itemActionType == ITEMFX_ACTION_NEED_SKILL_LVLS + || itemActionType == ITEMFX_ACTION_MUST_BE_UNEQUIPPED ) + { + char buf[32]; + if ( modeHasCostEffect == COST_EFFECT_MANA_RETURN_GOLD ) + { + snprintf(buf, sizeof(buf), "+%dG", -costEffectGoldAmount); + costEffectGoldText->setColor(hudColors.characterSheetLightNeutral); + } + else + { + snprintf(buf, sizeof(buf), "%dG", costEffectGoldAmount); + if ( itemActionType == ITEMFX_ACTION_CANT_AFFORD_GOLD || itemActionType == ITEMFX_ACTION_CANT_AFFORD_MANA_AND_GOLD ) + { + costEffectGoldText->setColor(negativeColor); + } + } + costEffectGoldText->setText(buf); + + snprintf(buf, sizeof(buf), "%dMP", costEffectMPAmount); + if ( itemActionType == ITEMFX_ACTION_CANT_AFFORD_MANA || itemActionType == ITEMFX_ACTION_CANT_AFFORD_MANA_AND_GOLD ) + { + costEffectManaText->setColor(negativeColor); + } + costEffectManaText->setText(buf); + + if ( modeHasTransmuteMenu() ) + { + if ( !parentGUI.transmuteItemTarget ) + { + costEffectGoldText->setText("-"); + costEffectManaText->setText("-"); + } + } + } + } } else { @@ -22020,7 +26629,9 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() auto actionPromptUnselectedTxt = baseFrame->findField("action prompt unselected txt"); auto actionPromptCoverLeftImg = baseFrame->findImage("action prompt lcover"); + actionPromptCoverLeftImg->disabled = true; auto actionPromptCoverRightImg = baseFrame->findImage("action prompt rcover"); + actionPromptCoverRightImg->disabled = true; actionPromptCoverLeftImg->pos.x = 0; actionPromptCoverRightImg->pos.x = baseFrame->getSize().w - actionPromptCoverLeftImg->pos.w; actionPromptCoverLeftImg->pos.y = 90 + heightOffsetCompact; @@ -22035,6 +26646,7 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() actionPromptUnselectedTxt->setText(""); break; case ITEMFX_MODE_SCROLL_REPAIR: + case ITEMFX_MODE_RESTORE: actionPromptUnselectedTxt->setText(Language::get(4151)); break; case ITEMFX_MODE_SCROLL_CHARGING: @@ -22055,14 +26667,73 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() case ITEMFX_MODE_SPELL_IDENTIFY: actionPromptUnselectedTxt->setText(Language::get(4209)); break; + case ITEMFX_MODE_ALTER_INSTRUMENT: + actionPromptUnselectedTxt->setText(Language::get(6513)); + break; case ITEMFX_MODE_SPELL_REMOVECURSE: actionPromptUnselectedTxt->setText(Language::get(4205)); break; + case ITEMFX_MODE_METALLURGY: + actionPromptUnselectedTxt->setText(Language::get(6542)); + break; + case ITEMFX_MODE_GEOMANCY: + actionPromptUnselectedTxt->setText(Language::get(6543)); + break; + case ITEMFX_MODE_FORGE_KEY: + actionPromptUnselectedTxt->setText(Language::get(6542)); + break; + case ITEMFX_MODE_FORGE_JEWEL: + actionPromptUnselectedTxt->setText(Language::get(6543)); + break; + case ITEMFX_MODE_RESHAPE_WEAPON: + actionPromptUnselectedTxt->setText(Language::get(6541)); + break; + case ITEMFX_MODE_ENHANCE_WEAPON: + actionPromptUnselectedTxt->setText(Language::get(6541)); + break; + case ITEMFX_MODE_ALTER_ARROW: + actionPromptUnselectedTxt->setText(Language::get(6542)); + break; + case ITEMFX_MODE_PUNCTURE_VOID: + actionPromptUnselectedTxt->setText(Language::get(6560)); + break; + case ITEMFX_MODE_ADORCISE_WEAPON: + actionPromptUnselectedTxt->setText(Language::get(6617)); + break; + case ITEMFX_MODE_VANDALISE: + actionPromptUnselectedTxt->setText(Language::get(6714)); + break; + case ITEMFX_MODE_DESECRATE: + actionPromptUnselectedTxt->setText(Language::get(6715)); + break; + case ITEMFX_MODE_SANCTIFY: + actionPromptUnselectedTxt->setText(Language::get(6717)); + break; + case ITEMFX_MODE_SANCTIFY_WATER: + actionPromptUnselectedTxt->setText(Language::get(6716)); + break; + case ITEMFX_MODE_CLEANSE_FOOD: + actionPromptUnselectedTxt->setText(Language::get(6718)); + break; + case ITEMFX_MODE_ADORCISE_INSTRUMENT: + actionPromptUnselectedTxt->setText(Language::get(6713)); + break; + case ITEMFX_MODE_SCEPTER_CHARGE: + actionPromptUnselectedTxt->setText(Language::get(6832)); + break; default: actionPromptUnselectedTxt->setText(""); break; } + if ( modeHasTransmuteMenu() ) + { + if ( parentGUI.transmuteItemTarget ) + { + actionPromptUnselectedTxt->setText(Language::get(6887)); + } + } + { SDL_Rect pos = actionPromptTxt->getSize(); pos.x = 26; @@ -22112,11 +26783,24 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() color.a = (Uint8)(255 * animTooltip); displayItemName->setColor(makeColor(color.r, color.g, color.b, color.a)); } + { + SDL_Color color; + getColor(costEffectGoldText->getColor(), &color.r, &color.g, &color.b, &color.a); + color.a = (Uint8)(255 * animTooltip); + costEffectGoldText->setColor(makeColor(color.r, color.g, color.b, color.a)); + } + { + SDL_Color color; + getColor(costEffectManaText->getColor(), &color.r, &color.g, &color.b, &color.a); + color.a = (Uint8)(255 * animTooltip); + costEffectManaText->setColor(makeColor(color.r, color.g, color.b, color.a)); + } //itemDisplayTooltip->setOpacity(100.0 * animTooltip); itemSlotFrame->setOpacity(100.0 * animTooltip); bool activateSelection = false; + bool transmuteActivate = false; if ( isInteractable ) { if ( !inputs.getUIInteraction(playernum)->selectedItem @@ -22128,15 +26812,26 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() if ( Input::inputs[playernum].binaryToggle("MenuCancel") ) { Input::inputs[playernum].consumeBinaryToggle("MenuCancel"); - parentGUI.closeGUI(); - Player::soundCancel(); - return; + if ( !actionButtonCancel->isInvisible() ) + { + actionButtonCancel->getCallback()(*actionButtonCancel); + } + else + { + parentGUI.closeGUI(); + Player::soundCancel(); + return; + } } else { if ( usingGamepad && Input::inputs[playernum].binaryToggle("MenuConfirm") ) { activateSelection = true; + if ( parentGUI.transmuteItemTarget ) + { + transmuteActivate = true; + } Input::inputs[playernum].consumeBinaryToggle("MenuConfirm"); } else if ( !usingGamepad && Input::inputs[playernum].binaryToggle("MenuRightClick") ) @@ -22144,10 +26839,34 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() activateSelection = true; Input::inputs[playernum].consumeBinaryToggle("MenuRightClick"); } + + if ( usingGamepad ) + { + actionButtonRefresh->setPressed(Input::inputs[playernum].binary("MenuAlt2")); + if ( Input::inputs[playernum].binaryToggle("MenuAlt2") ) + { + Input::inputs[playernum].consumeBinaryToggle("MenuAlt2"); + if ( !actionButtonRefresh->isInvisible() ) + { + actionButtonRefresh->getCallback()(*actionButtonRefresh); + } + } + } } } + + if ( actionButtonConfirm->getUserData() ) + { + transmuteActivate = true; + } + if ( parentGUI.transmuteItemTarget && transmuteActivate ) + { + activateSelection = true; + } } + actionButtonConfirm->setUserData(nullptr); + if ( activateSelection && players[playernum] && players[playernum]->entity ) { node_t* nextnode = nullptr; @@ -22163,16 +26882,72 @@ void GenericGUIMenu::ItemEffectGUI_t::updateItemEffectMenu() if ( node->element ) { Item* item = (Item*)node->element; - if ( isItemSelectedToEffect(item) ) + if ( (!transmuteActivate && isItemSelectedToEffect(item)) || (transmuteActivate && item == parentGUI.transmuteItemTarget) ) { foundItem = true; if ( itemCategory(item) == SPELLBOOK ) { //repairingSpellbook = true; } + + if ( modeHasTransmuteMenu() ) + { + if ( !transmuteActivate ) + { + if ( parentGUI.transmuteItemTarget == nullptr ) + { + if ( itemActionOK ) + { + Player::soundActivate(); + parentGUI.transmuteItemTarget = item; + animPrompt = 1.0; + animPromptTicks = ticks; + animPromptMoveLeft = false; + } + } + else + { + Player::soundCancel(); + parentGUI.transmuteItemTarget = nullptr; + animPrompt = 1.0; + animPromptTicks = ticks; + animPromptMoveLeft = true; + animTooltip = 0.0; + itemActionOK = true; + } + break; + } + } + if ( itemActionOK ) { - parentGUI.executeOnItemClick(item); + if ( currentMode == ITEMFX_MODE_SCEPTER_CHARGE ) + { + if ( confirmActionOnItemSteps.first == item->uid ) + { + if ( confirmActionOnItemSteps.second < 2 ) + { + ++confirmActionOnItemSteps.second; + } + else + { + Player::soundActivate(); + playSoundEntityLocal(players[parentGUI.gui_player]->entity, 167, 128); + parentGUI.executeOnItemClick(item); + confirmActionOnItemSteps.second = 0; + } + } + else + { + confirmActionOnItemSteps.second = 0; + } + } + else + { + Player::soundActivate(); + playSoundEntityLocal(players[parentGUI.gui_player]->entity, 167, 128); + parentGUI.executeOnItemClick(item); + } } break; } @@ -22233,6 +27008,20 @@ void GenericGUIMenu::ItemEffectGUI_t::createItemEffectMenu() } SDL_Rect basePos{ 0, 0, itemEffectBaseWidth, 242 }; + const char* baseWindow = "*images/ui/ScrollSpells/Scroll_Window_00.png"; + if ( modeHasCostEffect == COST_EFFECT_GOLD + || modeHasCostEffect == COST_EFFECT_MANA ) + { + baseWindow = "*images/ui/ScrollSpells/Scroll_Window_GoldCost_00.png"; + basePos.h = 268; + } + else if ( modeHasCostEffect == COST_EFFECT_MANA_RETURN_GOLD + || modeHasCostEffect == COST_EFFECT_MANA_AND_GOLD ) + { + baseWindow = "*images/ui/ScrollSpells/Scroll_Window_GoldManaCost_00.png"; + basePos.h = 296; + } + { auto bgFrame = itemEffectFrame->addFrame("itemfx base"); bgFrame->setSize(basePos); @@ -22240,7 +27029,7 @@ void GenericGUIMenu::ItemEffectGUI_t::createItemEffectMenu() bgFrame->setDisabled(true); auto bg = bgFrame->addImage(SDL_Rect{ 0, 0, basePos.w, basePos.h }, makeColor(255, 255, 255, 255), - "*images/ui/ScrollSpells/Scroll_Window_00.png", "itemfx base img"); + baseWindow, "itemfx base img"); auto skillIcon = bgFrame->addImage(SDL_Rect{ 270, 36, 24, 24 }, makeColor(255, 255, 255, 255), @@ -22290,6 +27079,56 @@ void GenericGUIMenu::ItemEffectGUI_t::createItemEffectMenu() auto itemBgImg = itemDisplayTooltip->addImage(SDL_Rect{ 0, 0, 54, 54 }, 0xFFFFFFFF, "*images/ui/ScrollSpells/Scroll_ItemBGSurround_00.png", "item bg img"); + auto itemBgTransmuteGlow = itemDisplayTooltip->addImage(SDL_Rect{ 0, 0, 44, 44 }, 0xFFFFFFFF, + "*images/ui/ScrollSpells/Gleam_00.png", "item transmute gleam img"); + itemBgTransmuteGlow->disabled = true; + + { + auto itemCostBg = itemDisplayTooltip->addImage(SDL_Rect{ 0, 0, 104, 34 }, + 0xFFFFFFFF, "*images/ui/ScrollSpells/Scroll_CostBacking_00.png", "item cost img"); + itemCostBg->disabled = true; + + auto itemCostBg2 = itemDisplayTooltip->addImage(SDL_Rect{ 0, 0, 104, 34 }, + 0xFFFFFFFF, "*images/ui/ScrollSpells/Scroll_MPBacking_00.png", "item cost img 2"); + itemCostBg2->disabled = true; + + auto costEffectGoldText = itemDisplayTooltip->addField("item gold value", 32); + costEffectGoldText->setFont(itemFont); + costEffectGoldText->setText(""); + costEffectGoldText->setHJustify(Field::justify_t::RIGHT); + costEffectGoldText->setVJustify(Field::justify_t::TOP); + costEffectGoldText->setSize(SDL_Rect{ 0, 0, 0, 0 }); + costEffectGoldText->setColor(hudColors.characterSheetLightNeutral); + costEffectGoldText->setDisabled(true); + + auto costEffectMPText = itemDisplayTooltip->addField("item mp value", 32); + costEffectMPText->setFont(itemFont); + costEffectMPText->setText(""); + costEffectMPText->setHJustify(Field::justify_t::RIGHT); + costEffectMPText->setVJustify(Field::justify_t::TOP); + costEffectMPText->setSize(SDL_Rect{ 0, 0, 0, 0 }); + costEffectMPText->setColor(hudColors.characterSheetLightNeutral); + costEffectMPText->setDisabled(true); + + auto costLabel = itemDisplayTooltip->addField("item cost label", 64); + costLabel->setFont(itemFont); + costLabel->setText(""); + costLabel->setHJustify(Field::justify_t::RIGHT); + costLabel->setVJustify(Field::justify_t::TOP); + costLabel->setSize(SDL_Rect{ 0, 0, 90, 0 }); + costLabel->setColor(hudColors.characterSheetLightNeutral); + costLabel->setDisabled(true); + + auto costLabel2 = itemDisplayTooltip->addField("item cost label 2", 64); + costLabel2->setFont(itemFont); + costLabel2->setText(""); + costLabel2->setHJustify(Field::justify_t::RIGHT); + costLabel2->setVJustify(Field::justify_t::TOP); + costLabel2->setSize(SDL_Rect{ 0, 0, 90, 0 }); + costLabel2->setColor(hudColors.characterSheetLightNeutral); + costLabel2->setDisabled(true); + } + auto slotFrame = itemDisplayTooltip->addFrame("item slot frame"); SDL_Rect slotPos{ 0, 0, players[player]->inventoryUI.getSlotSize(), players[player]->inventoryUI.getSlotSize() }; slotFrame->setSize(slotPos); @@ -22345,6 +27184,116 @@ void GenericGUIMenu::ItemEffectGUI_t::createItemEffectMenu() actionModifierGlyph->disabled = true; } + { + auto actionButtonConfirm = bgFrame->addButton("action button confirm"); + SDL_Rect actionBtnPos{ 0, 0, 86, 26 }; + actionButtonConfirm->setSize(actionBtnPos); + actionButtonConfirm->setColor(makeColor(255, 255, 255, 255)); + actionButtonConfirm->setHighlightColor(makeColor(255, 255, 255, 255)); + actionButtonConfirm->setText(""); + actionButtonConfirm->setFont(itemFont); + actionButtonConfirm->setHideGlyphs(true); + actionButtonConfirm->setHideKeyboardGlyphs(true); + actionButtonConfirm->setHideSelectors(true); + actionButtonConfirm->setMenuConfirmControlType(0); + actionButtonConfirm->setBackground("*images/ui/ScrollSpells/Button_TakeAll_00.png"); + actionButtonConfirm->setBackgroundHighlighted("*images/ui/ScrollSpells/Button_TakeAllHigh_00.png"); + actionButtonConfirm->setBackgroundActivated("*images/ui/ScrollSpells/Button_TakeAllPress_00.png"); + actionButtonConfirm->setTextHighlightColor(makeColor(201, 162, 100, 255)); + actionButtonConfirm->setCallback([](Button& button) { + button.setUserData((void*)(intptr_t)(1)); + }); + actionButtonConfirm->setTickCallback(genericgui_deselect_fn); + actionButtonConfirm->setInvisible(true); + actionButtonConfirm->setDisabled(true); + + auto actionPromptGlyph = bgFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "", "action confirm glyph"); + actionPromptGlyph->ontop = true; + actionPromptGlyph->disabled = true; + + actionPromptGlyph = bgFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "", "action cancel glyph"); + actionPromptGlyph->ontop = true; + actionPromptGlyph->disabled = true; + + actionPromptGlyph = bgFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "", "action refresh glyph"); + actionPromptGlyph->ontop = true; + actionPromptGlyph->disabled = true; + + auto actionButtonCancel = bgFrame->addButton("action button cancel"); + actionBtnPos = SDL_Rect{ 0, 0, 86, 26 }; + actionButtonCancel->setSize(actionBtnPos); + actionButtonCancel->setColor(makeColor(255, 255, 255, 255)); + actionButtonCancel->setHighlightColor(makeColor(255, 255, 255, 255)); + actionButtonCancel->setText(""); + actionButtonCancel->setFont(itemFont); + actionButtonCancel->setHideGlyphs(true); + actionButtonCancel->setHideKeyboardGlyphs(true); + actionButtonCancel->setHideSelectors(true); + actionButtonCancel->setMenuConfirmControlType(0); + actionButtonCancel->setBackground("*images/ui/ScrollSpells/Button_Cancel_00.png"); + actionButtonCancel->setBackgroundHighlighted("*images/ui/ScrollSpells/Button_CancelHigh_00.png"); + actionButtonCancel->setBackgroundActivated("*images/ui/ScrollSpells/Button_CancelPress_00.png"); + //actionButtonCancel->setTextColor(hudColors.characterSheetRed); + actionButtonCancel->setTextHighlightColor(makeColor(201, 162, 100, 255)); + actionButtonCancel->setCallback([](Button& button) { + if ( button.getOwner() >= 0 && button.getOwner() < MAXPLAYERS ) + { + if ( GenericGUI[button.getOwner()].transmuteItemTarget ) + { + GenericGUI[button.getOwner()].transmuteItemTarget = nullptr; + GenericGUI[button.getOwner()].itemfxGUI.animPrompt = 1.0; + GenericGUI[button.getOwner()].itemfxGUI.animPromptTicks = ticks; + GenericGUI[button.getOwner()].itemfxGUI.animPromptMoveLeft = true; + GenericGUI[button.getOwner()].itemfxGUI.animTooltip = 0.0; + } + + if ( players[button.getOwner()]->GUI.activeModule == Player::GUI_t::MODULE_ITEMEFFECTGUI ) + { + // reset to inventory mode if still hanging in itemfx GUI + players[button.getOwner()]->hud.compactLayoutMode = Player::HUD_t::COMPACT_LAYOUT_INVENTORY; + players[button.getOwner()]->GUI.activateModule(Player::GUI_t::MODULE_INVENTORY); + if ( !inputs.getVirtualMouse(button.getOwner())->draw_cursor ) + { + players[button.getOwner()]->GUI.warpControllerToModule(false); + } + } + } + Player::soundCancel(); + }); + actionButtonCancel->setTickCallback(genericgui_deselect_fn); + actionButtonCancel->setInvisible(true); + actionButtonCancel->setDisabled(true); + + auto actionButtonRefresh = bgFrame->addButton("action button refresh"); + actionBtnPos = SDL_Rect{ 0, 0, 40, 40 }; + actionButtonRefresh->setSize(actionBtnPos); + actionButtonRefresh->setColor(makeColor(255, 255, 255, 255)); + actionButtonRefresh->setHighlightColor(makeColor(255, 255, 255, 255)); + actionButtonRefresh->setText(""); + actionButtonRefresh->setFont(itemFont); + actionButtonRefresh->setHideGlyphs(true); + actionButtonRefresh->setHideKeyboardGlyphs(true); + actionButtonRefresh->setHideSelectors(true); + actionButtonRefresh->setMenuConfirmControlType(0); + actionButtonRefresh->setBackground("*images/ui/ScrollSpells/Button_Refresh00.png"); + actionButtonRefresh->setBackgroundHighlighted("*images/ui/ScrollSpells/Button_RefreshHigh00.png"); + actionButtonRefresh->setBackgroundActivated("*images/ui/ScrollSpells/Button_RefreshPress00.png"); + actionButtonRefresh->setTextHighlightColor(makeColor(201, 162, 100, 255)); + actionButtonRefresh->setCallback([](Button& button) { + Player::soundModuleNavigation(); + if ( button.getOwner() >= 0 && button.getOwner() < MAXPLAYERS ) + { + GenericGUI[button.getOwner()].transmuteItemScroll++; + } + }); + actionButtonRefresh->setTickCallback(genericgui_deselect_fn); + actionButtonRefresh->setInvisible(true); + actionButtonRefresh->setDisabled(true); + } + { auto actionPromptUnselectedTxt = bgFrame->addField("action prompt unselected txt", 64); actionPromptUnselectedTxt->setFont(itemFont); @@ -23127,7 +28076,7 @@ std::string CalloutRadialMenu::setCalloutText(Field* field, const char* iconName { break; } - if ( stats[player]->EFFECTS[i] + if ( stats[player]->getEffectActive(i) > 0 && (stats[player]->statusEffectRemovedByCureAilment(i, players[player]->entity) || i == EFF_WITHDRAWAL && stats[player]->EFFECTS_TIMERS[EFF_WITHDRAWAL] == -2) ) { @@ -23389,6 +28338,57 @@ std::string CalloutRadialMenu::setCalloutText(Field* field, const char* iconName case CALLOUT_TYPE_SWITCH: key = "switch"; break; + break; + case CALLOUT_TYPE_WALL_LOCK: + case CALLOUT_TYPE_WALL_LOCK_ON: + case CALLOUT_TYPE_WALL_LOCK_OFF: + { + if ( calloutType == CALLOUT_TYPE_WALL_LOCK ) + { + key = "wall_lock"; + } + else if ( calloutType == CALLOUT_TYPE_WALL_LOCK_ON ) + { + key = "wall_lock_on"; + } + else if ( calloutType == CALLOUT_TYPE_WALL_LOCK_OFF ) + { + key = "wall_lock_off"; + } + if ( setType == SET_CALLOUT_ICON_KEY ) + { + return key; + } + int wallLockMaterial = entity->wallLockMaterial; + if ( entity->sprite >= 1585 && entity->sprite <= 1592 ) + { + if ( Entity* parent = uidToEntity(entity->parent) ) + { + wallLockMaterial = parent->wallLockMaterial; + } + } + + std::string objectName = Language::get(6383 + wallLockMaterial); + + auto& textMap = text_map[key]; + if ( setType == SET_CALLOUT_BANNER_TEXT ) + { + setCalloutBannerTextFormatted(player, field, color, textMap.bannerHighlights, + textMap.bannerText.c_str(), objectName.c_str()); + } + else + { + return getCalloutMessage(textMap, objectName.c_str(), targetPlayer); + } + return ""; + break; + } + case CALLOUT_TYPE_WALL_BUTTON_ON: + key = "wall_button_on"; + break; + case CALLOUT_TYPE_WALL_BUTTON_OFF: + key = "wall_button_off"; + break; case CALLOUT_TYPE_CHEST: key = "chest"; break; @@ -23692,6 +28692,18 @@ std::string CalloutRadialMenu::setCalloutText(Field* field, const char* iconName { objectName = Language::get(4365); } + else if ( entity->behavior == &actCauldron ) + { + objectName = Language::get(6974); + } + else if ( entity->behavior == &actWorkbench ) + { + objectName = Language::get(6981); + } + else if ( entity->behavior == &actMailbox ) + { + objectName = Language::get(6986); + } else if ( entity->behavior == &actPowerCrystal ) { objectName = Language::get(4356); @@ -24170,6 +29182,58 @@ CalloutRadialMenu::CalloutType CalloutRadialMenu::getCalloutTypeForEntity(const type = CALLOUT_TYPE_SWITCH_OFF; } } + else if ( parent->behavior == &::actWallLock + || (parent->sprite >= 1585 && parent->sprite <= 1592) ) + { + int wallLockState = parent->wallLockState; + if ( parent->sprite >= 1585 && parent->sprite <= 1592 ) + { + if ( Entity* lock = uidToEntity(parent->parent) ) + { + wallLockState = lock->wallLockState; + } + } + if ( wallLockState == Entity::WallLockStates::LOCK_NO_KEY ) + { + type = CALLOUT_TYPE_WALL_LOCK; + } + else if ( wallLockState == Entity::WallLockStates::LOCK_KEY_ACTIVE_START + || wallLockState == Entity::WallLockStates::LOCK_KEY_ACTIVE ) + { + type = CALLOUT_TYPE_WALL_LOCK_ON; + } + else if ( wallLockState == Entity::WallLockStates::LOCK_KEY_INACTIVE_START + || wallLockState == Entity::WallLockStates::LOCK_KEY_INACTIVE ) + { + type = CALLOUT_TYPE_WALL_LOCK_OFF; + } + else + { + type = CALLOUT_TYPE_NO_TARGET; + } + } + else if ( parent->behavior == &::actWallButton + || parent->sprite == 1151 + || parent->sprite == 1152 ) + { + int wallLockState = parent->wallLockState; + if ( parent->sprite == 1151 + || parent->sprite == 1152 ) + { + if ( Entity* lock = uidToEntity(parent->parent) ) + { + wallLockState = lock->wallLockState; + } + } + if ( wallLockState == 0 ) + { + type = CALLOUT_TYPE_WALL_BUTTON_OFF; + } + else + { + type = CALLOUT_TYPE_WALL_BUTTON_ON; + } + } else if ( parent->behavior == &actPowerCrystal ) { type = CALLOUT_TYPE_GENERIC_INTERACTABLE; @@ -24182,6 +29246,12 @@ CalloutRadialMenu::CalloutType CalloutRadialMenu::getCalloutTypeForEntity(const { type = CALLOUT_TYPE_GENERIC_INTERACTABLE; } + else if ( parent->behavior == &actCauldron + || parent->behavior == &actWorkbench + || parent->behavior == &actMailbox ) + { + type = CALLOUT_TYPE_GENERIC_INTERACTABLE; + } else if ( parent->behavior == &actBoulderTrapHole ) { type = CALLOUT_TYPE_TRAP; @@ -24267,7 +29337,7 @@ CalloutRadialMenu::CalloutType CalloutRadialMenu::getCalloutTypeForEntity(const } else if ( parent->behavior == &actTeleporter ) { - if ( parent->teleporterType == 2 ) // portal + if ( parent->teleporterType == 2 || parent->teleporterType == 3 ) // portal { type = CALLOUT_TYPE_TELEPORTER_PORTAL; } @@ -24511,7 +29581,7 @@ void CalloutRadialMenu::drawCallouts(const int playernum) bool selfCallout = false; if ( uidMatchesPlayer(playernum, callout.second.entityUid) ) { - if ( i == playernum && players[i]->entity && players[i]->entity->skill[3] == 1 ) + if ( i == playernum && players[i]->entity && players[i]->entity->skill[3] != 0 && !players[i]->ghost.isActive() ) { // debug/thirdperson cam. } @@ -25589,7 +30659,10 @@ void CalloutRadialMenu::drawCalloutMenu() { if ( modifierPressed ) { - if ( stats[gui_player]->shield && itemCategory(stats[gui_player]->shield) == SPELLBOOK ) + if ( stats[gui_player]->shield + && (itemCategory(stats[gui_player]->shield) == SPELLBOOK + || itemTypeIsFoci(stats[gui_player]->shield->type) + || itemTypeIsInstrument(stats[gui_player]->shield->type)) ) { input.consumeBinaryToggle("Defend"); // don't try cast when menu closes. } @@ -26452,6 +31525,66 @@ bool CalloutRadialMenu::allowedInteractEntity(Entity& selectedEntity, bool updat strcat(interactText, Language::get(6353)); // "assist shrine" } } + else if ( (selectedEntity.behavior == &::actWallButton + || selectedEntity.sprite == 1151 + || selectedEntity.sprite == 1152) && interactWorld ) + { + if ( updateInteractText ) + { + strcat(interactText, Language::get(6393)); // "button" + } + } + else if ( (selectedEntity.behavior == &::actWallLock + || (selectedEntity.sprite >= 1585 && selectedEntity.sprite <= 1592)) && interactWorld ) + { + int wallLockState = selectedEntity.wallLockState; + int wallLockMaterial = selectedEntity.wallLockMaterial; + if ( selectedEntity.sprite >= 1585 && selectedEntity.sprite <= 1592 ) + { + if ( Entity* parent = uidToEntity(selectedEntity.parent) ) + { + wallLockState = parent->wallLockState; + wallLockMaterial = parent->wallLockMaterial; + } + } + if ( wallLockState == Entity::WallLockStates::LOCK_NO_KEY ) + { + if ( updateInteractText ) + { + strcat(interactText, Language::get(6383 + wallLockMaterial)); + strcat(interactText, " "); + strcat(interactText, Language::get(6392)); // "%s lock" + } + } + else if ( wallLockState == Entity::WallLockStates::LOCK_KEY_ACTIVE_START + || wallLockState == Entity::WallLockStates::LOCK_KEY_ACTIVE ) + { + if ( updateInteractText ) + { + strcat(interactText, Language::get(6383 + wallLockMaterial)); + strcat(interactText, " "); + strcat(interactText, Language::get(6391)); // "%s key" + } + } + else if ( wallLockState == Entity::WallLockStates::LOCK_KEY_INACTIVE_START + || wallLockState == Entity::WallLockStates::LOCK_KEY_INACTIVE ) + { + if ( updateInteractText ) + { + strcat(interactText, Language::get(6383 + wallLockMaterial)); + strcat(interactText, " "); + strcat(interactText, Language::get(6391)); // "%s key" + } + } + else + { + if ( updateInteractText ) + { + strcpy(interactText, ""); + } + return false; + } + } else if ( (selectedEntity.behavior == &actTeleporter) && interactWorld ) { if ( updateInteractText ) @@ -26463,6 +31596,7 @@ bool CalloutRadialMenu::allowedInteractEntity(Entity& selectedEntity, bool updat strcat(interactText, Language::get(4310)); // "ladder" break; case 2: + case 3: strcat(interactText, Language::get(4311)); // "portal" break; default: @@ -26652,6 +31786,27 @@ bool CalloutRadialMenu::allowedInteractEntity(Entity& selectedEntity, bool updat strcat(interactText, Language::get(4365)); // "campfire" } } + else if ( selectedEntity.behavior == &actCauldron ) + { + if ( updateInteractText ) + { + strcat(interactText, Language::get(6974)); // "cauldron" + } + } + else if ( selectedEntity.behavior == &actWorkbench ) + { + if ( updateInteractText ) + { + strcat(interactText, Language::get(6981)); // "workbench" + } + } + else if ( selectedEntity.behavior == &actMailbox ) + { + if ( updateInteractText ) + { + strcat(interactText, Language::get(6986)); // "mailbox" + } + } else if ( selectedEntity.behavior == &actPowerCrystal || selectedEntity.behavior == &actPowerCrystalBase ) { if ( updateInteractText ) @@ -26731,6 +31886,7 @@ void GenericGUIMenu::AssistShrineGUI_t::openAssistShrine(Entity* shrine) if ( shrine ) { shrineUID = shrine->getUID(); + Compendium_t::Events_t::eventUpdateWorld(parentGUI.gui_player, Compendium_t::CPDM_ASSIST_INTERACTS, "assist shrine", 1); } const int playernum = parentGUI.getPlayer(); auto player = players[playernum]; @@ -26834,6 +31990,14 @@ void GenericGUIMenu::AssistShrineGUI_t::changeCurrentView(GenericGUIMenu::Assist raceSlots.push_back(RACE_GOBLIN); raceSlots.push_back(RACE_INSECTOID); } + if ( enabledDLCPack3 ) + { + raceSlots.push_back(RACE_GNOME); + raceSlots.push_back(RACE_GREMLIN); + raceSlots.push_back(RACE_DRYAD); + raceSlots.push_back(RACE_MYCONID); + raceSlots.push_back(RACE_SALAMANDER); + } if ( currentView == ASSIST_SHRINE_VIEW_RACE ) { @@ -27023,10 +32187,16 @@ void GenericGUIMenu::AssistShrineGUI_t::updateRaceSlots() { dlcType = MainMenu::DLC::LegendsAndPariahs; } + if ( race > RACE_IMP + && race < RACE_ENUM_END ) + { + dlcType = MainMenu::DLC::DesertersAndDisciples; + } std::string str = getMonsterLocalizedName(getMonsterFromPlayerRace(race)).c_str(); uppercaseString(str); slotTxt->setText(str.c_str()); switch ( dlcType ) { + default: case MainMenu::DLC::Base: slotTxt->setColor(hudColors.characterBaseClassText); break; @@ -27036,6 +32206,9 @@ void GenericGUIMenu::AssistShrineGUI_t::updateRaceSlots() case MainMenu::DLC::LegendsAndPariahs: slotTxt->setColor(hudColors.characterDLC2ClassText); break; + case MainMenu::DLC::DesertersAndDisciples: + slotTxt->setColor(hudColors.characterDLC3ClassText); + break; } if ( selected ) { @@ -27206,6 +32379,13 @@ void GenericGUIMenu::AssistShrineGUI_t::updateClassSlots() slotBg->path = (prefix + "ClassSelect_IconBGLegendsHigh_00.png"); } break; + case MainMenu::DLC::DesertersAndDisciples: + slotBg->path = (prefix + "ClassSelect_IconBGDeserters_00.png"); + if ( selected ) + { + slotBg->path = (prefix + "ClassSelect_IconBGDesertersHigh_00.png"); + } + break; } } } @@ -28004,16 +33184,46 @@ void GenericGUIMenu::AssistShrineGUI_t::createAssistShrine() assistItemFrame->setHollow(true); assistItemFrame->setDisabled(true); + { + auto titleText = assistItemFrame->addField("title txt", 64); + titleText->setFont("fonts/pixel_maz_multiline.ttf#16"); + titleText->setText(Language::get(6701)); + titleText->setHJustify(Field::justify_t::CENTER); + titleText->setVJustify(Field::justify_t::TOP); + titleText->setSize(SDL_Rect{ 0, 59, basePos.w - 8, 24 }); + titleText->setColor(makeColor(221, 206, 189, 255)); + } + auto classFrame = assistShrineFrame->addFrame("assist classes"); classFrame->setSize(basePos); classFrame->setHollow(true); classFrame->setDisabled(true); + { + auto titleText = classFrame->addField("title txt", 64); + titleText->setFont("fonts/pixel_maz_multiline.ttf#16"); + titleText->setText(Language::get(6702)); + titleText->setHJustify(Field::justify_t::CENTER); + titleText->setVJustify(Field::justify_t::TOP); + titleText->setSize(SDL_Rect{ 0, 59, basePos.w - 8, 24 }); + titleText->setColor(makeColor(221, 206, 189, 255)); + } + auto raceFrame = assistShrineFrame->addFrame("assist races"); raceFrame->setSize(basePos); raceFrame->setHollow(true); raceFrame->setDisabled(true); + { + auto titleText = raceFrame->addField("title txt", 64); + titleText->setFont("fonts/pixel_maz_multiline.ttf#16"); + titleText->setText(Language::get(6703)); + titleText->setHJustify(Field::justify_t::CENTER); + titleText->setVJustify(Field::justify_t::TOP); + titleText->setSize(SDL_Rect{ 0, 59, basePos.w - 8, 24 }); + titleText->setColor(makeColor(221, 206, 189, 255)); + } + { int numGrids = (MAX_ASSISTSHRINE_Y / kNumClassesToDisplayVertical) + 1; const int baseSlotOffsetX = 0; @@ -28548,6 +33758,54 @@ void GenericGUIMenu::AssistShrineGUI_t::createAssistShrine() widget.isPressed() ? makeColorRGB(255, 255, 255) : makeColorRGB(128, 128, 128)); } } + else if ( (gui.selectedRace == -1 && gui.savedRace == -1 && stats[widget.getOwner()]->playerRace == RACE_MYCONID) + || (gui.savedRace == RACE_MYCONID && gui.selectedRace == -1) + || gui.selectedRace == RACE_MYCONID) + { + if ( auto img = Image::get("*images/ui/CharSheet/HUD_CharSheet_Height_S_00.png") ) + { + SDL_Rect pos2 = pos; + pos2.x -= img->getWidth(); + pos2.x -= 8; + pos2.w = img->getWidth(); + pos2.h = img->getHeight(); + img->drawColor(nullptr, pos2, SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + !widget.isPressed() ? makeColorRGB(255, 255, 255) : makeColorRGB(128, 128, 128)); + } + if ( auto img = Image::get("*images/ui/CharSheet/HUD_CharSheet_Height_T_00.png") ) + { + SDL_Rect pos2 = pos; + pos2.x += button->getSize().w + 8; + pos2.w = img->getWidth(); + pos2.h = img->getHeight(); + img->drawColor(nullptr, pos2, SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + widget.isPressed() ? makeColorRGB(255, 255, 255) : makeColorRGB(128, 128, 128)); + } + } + else if ( (gui.selectedRace == -1 && gui.savedRace == -1 && stats[widget.getOwner()]->playerRace == RACE_DRYAD) + || (gui.savedRace == RACE_DRYAD && gui.selectedRace == -1) + || gui.selectedRace == RACE_DRYAD ) + { + if ( auto img = Image::get("*images/ui/CharSheet/HUD_CharSheet_Height_T_00.png") ) + { + SDL_Rect pos2 = pos; + pos2.x -= img->getWidth(); + pos2.x -= 8; + pos2.w = img->getWidth(); + pos2.h = img->getHeight(); + img->drawColor(nullptr, pos2, SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + !widget.isPressed() ? makeColorRGB(255, 255, 255) : makeColorRGB(128, 128, 128)); + } + if ( auto img = Image::get("*images/ui/CharSheet/HUD_CharSheet_Height_S_00.png") ) + { + SDL_Rect pos2 = pos; + pos2.x += button->getSize().w + 8; + pos2.w = img->getWidth(); + pos2.h = img->getHeight(); + img->drawColor(nullptr, pos2, SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + widget.isPressed() ? makeColorRGB(255, 255, 255) : makeColorRGB(128, 128, 128)); + } + } else { if ( auto img = Image::get("*images/ui/CharSheet/HUD_CharSheet_Sex_M_02.png") ) @@ -29347,6 +34605,7 @@ bool GenericGUIMenu::AssistShrineGUI_t::claimItems(bool* isEquipped) } } claimedItems.insert(pickedUp->type); + Compendium_t::Events_t::eventUpdateWorld(parentGUI.gui_player, Compendium_t::CPDM_ASSIST_MASKS, "assist shrine", 1); } free(item); } @@ -29367,6 +34626,7 @@ bool GenericGUIMenu::AssistShrineGUI_t::claimItems(bool* isEquipped) } } claimedItems.insert(pickedUp->type); + Compendium_t::Events_t::eventUpdateWorld(parentGUI.gui_player, Compendium_t::CPDM_ASSIST_AMULETS, "assist shrine", 1); } free(item); } @@ -29387,6 +34647,7 @@ bool GenericGUIMenu::AssistShrineGUI_t::claimItems(bool* isEquipped) } } claimedItems.insert(pickedUp->type); + Compendium_t::Events_t::eventUpdateWorld(parentGUI.gui_player, Compendium_t::CPDM_ASSIST_CLOAKS, "assist shrine", 1); } free(item); } @@ -29407,6 +34668,7 @@ bool GenericGUIMenu::AssistShrineGUI_t::claimItems(bool* isEquipped) } } claimedItems.insert(pickedUp->type); + Compendium_t::Events_t::eventUpdateWorld(parentGUI.gui_player, Compendium_t::CPDM_ASSIST_RINGS, "assist shrine", 1); } free(item); } @@ -30067,6 +35329,10 @@ void GenericGUIMenu::AssistShrineGUI_t::updateAssistShrine() { color = hudColors.characterDLC2ClassText; } + else if ( classnum >= CLASS_BARD && classnum <= CLASS_PALADIN ) + { + color = hudColors.characterDLC3ClassText; + } } std::string classname = playerClassLangEntry(classnum, parentGUI.gui_player); uppercaseString(classname); @@ -30097,6 +35363,10 @@ void GenericGUIMenu::AssistShrineGUI_t::updateAssistShrine() { classNewText->addColorToLine(1, hudColors.characterDLC2ClassText); } + else if ( selectedClass >= CLASS_BARD && selectedClass <= CLASS_PALADIN ) + { + classNewText->addColorToLine(1, hudColors.characterDLC3ClassText); + } } classNewText->setText(txt.c_str()); } @@ -30290,6 +35560,11 @@ void GenericGUIMenu::AssistShrineGUI_t::updateAssistShrine() { color = hudColors.characterDLC2ClassText; } + if ( race > RACE_IMP + && race < RACE_ENUM_END ) + { + color = hudColors.characterDLC3ClassText; + } } std::string racename = getMonsterLocalizedName(getMonsterFromPlayerRace(race)).c_str(); uppercaseString(racename); @@ -32239,6 +37514,10 @@ void GenericGUIMenu::AssistShrineGUI_t::updateAssistShrine() { txt->setColor(hudColors.characterDLC2ClassText); } + else if ( itemType >= CLASS_BARD && itemType <= CLASS_PALADIN ) + { + txt->setColor(hudColors.characterDLC3ClassText); + } else { txt->setColor(hudColors.characterBaseClassText); @@ -32432,6 +37711,11 @@ void GenericGUIMenu::AssistShrineGUI_t::updateAssistShrine() { txt->setColor(hudColors.characterDLC2ClassText); } + else if ( itemType > RACE_IMP + && itemType < RACE_ENUM_END ) + { + txt->setColor(hudColors.characterDLC3ClassText); + } else { txt->setColor(hudColors.characterBaseClassText); @@ -33576,4 +38860,2069 @@ bool GenericGUIMenu::AssistShrineGUI_t::isSlotVisible(int x, int y) const return false; } return false; +} + +void GenericGUIMenu::MailboxGui_t::closeMailMenu() +{ + const int playernum = parentGUI.getPlayer(); + auto& player = *players[playernum]; + + if ( mailFrame ) + { + mailFrame->setDisabled(true); + } + animx = 0.0; + animTooltip = 0.0; + + animSendItem1 = 0.0; + sendItem1Uid = 0; + mailReceiveItem.type = POTION_EMPTY; + recvItemUid = 0; + animRecvItem = 0.0; + + isInteractable = false; + bool wasOpen = bOpen; + bOpen = false; + bFirstTimeSnapCursor = false; + if ( wasOpen ) + { + if ( inputs.getUIInteraction(playernum)->selectedItem ) + { + inputs.getUIInteraction(playernum)->selectedItem = nullptr; + inputs.getUIInteraction(playernum)->toggleclick = false; + } + inputs.getUIInteraction(playernum)->selectedItemFromChest = 0; + } + if ( players[playernum]->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX + && !players[playernum]->shootmode ) + { + // reset to inventory mode if still hanging in alchemy GUI + players[playernum]->hud.compactLayoutMode = Player::HUD_t::COMPACT_LAYOUT_INVENTORY; + players[playernum]->GUI.activateModule(Player::GUI_t::MODULE_INVENTORY); + if ( !inputs.getVirtualMouse(playernum)->draw_cursor ) + { + players[playernum]->GUI.warpControllerToModule(false); + } + } + clearItemDisplayed(); + itemRequiresTitleReflow = true; + if ( mailFrame ) + { + for ( auto f : mailFrame->getFrames() ) + { + f->removeSelf(); + } + mailSlotFrames.clear(); + } + //notifications.clear(); + + if ( multiplayer != CLIENT ) + { + if ( Entity* mailbox = uidToEntity(parentGUI.mailboxEntityUid) ) + { + mailbox->skill[6] = 0; + serverUpdateEntitySkill(mailbox, 6); + } + } + + if ( multiplayer == CLIENT && parentGUI.mailboxEntityUid > 0 ) + { + strcpy((char*)net_packet->data, "MBXC"); + net_packet->data[4] = clientnum; + SDLNet_Write32(parentGUI.mailboxEntityUid, &net_packet->data[5]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + parentGUI.mailboxEntityUid = 0; +} + +int GenericGUIMenu::MailboxGui_t::heightOffsetWhenNotCompact = 150; +const int mailBaseWidth = 206; + +void getInventoryItemMailboxAnimSlotPos(Frame* slotFrame, Player* player, int itemx, int itemy, int& outPosX, int& outPosY, int yOffset) +{ + outPosX = slotFrame->getSize().x + slotFrame->getParent()->getSize().x; + outPosY = slotFrame->getSize().y + (player->inventoryUI.bCompactView ? 8 : 0) + yOffset; + if ( itemy >= player->inventoryUI.DEFAULT_INVENTORY_SIZEY ) + { + // backpack slots, add another offset. + if ( auto invSlotsFrame = player->inventoryUI.frame->findFrame("inventory slots") ) + { + outPosY += invSlotsFrame->getSize().h; + } + } +} + +bool GenericGUIMenu::MailboxGui_t::mailGUIHasBeenCreated() const +{ + if ( mailFrame ) + { + if ( !mailFrame->getFrames().empty() ) + { + for ( auto f : mailFrame->getFrames() ) + { + if ( !f->isToBeDeleted() ) + { + return true; + } + } + return false; + } + else + { + return false; + } + } + return false; +} + +void buttonMailUpdateSelectorOnHighlight(const int player, Button* button) +{ + if ( button->isHighlighted() ) + { + players[player]->GUI.setHoveringOverModuleButton(Player::GUI_t::MODULE_MAILBOX); + if ( players[player]->GUI.activeModule != Player::GUI_t::MODULE_MAILBOX ) + { + players[player]->GUI.activateModule(Player::GUI_t::MODULE_MAILBOX); + } + SDL_Rect pos = button->getAbsoluteSize(); + // make sure to adjust absolute size to camera viewport + pos.x -= players[player]->camera_virtualx1(); + pos.y -= players[player]->camera_virtualy1(); + players[player]->hud.setCursorDisabled(false); + players[player]->hud.updateCursorAnimation(pos.x - 1, pos.y - 1, pos.w, pos.h, inputs.getVirtualMouse(player)->draw_cursor); + } +} + +void GenericGUIMenu::MailboxGui_t::openMailMenu() +{ + const int playernum = parentGUI.getPlayer(); + auto player = players[playernum]; + + if ( mailFrame ) + { + bool wasDisabled = mailFrame->isDisabled(); + mailFrame->setDisabled(false); + if ( wasDisabled ) + { + //notifications.clear(); + animx = 0.0; + animTooltip = 0.0; + isInteractable = false; + bFirstTimeSnapCursor = false; + } + selectMailSlot(MAIL_SLOT_SEND, 0); + player->hud.compactLayoutMode = Player::HUD_t::COMPACT_LAYOUT_INVENTORY; + player->inventory_mode = INVENTORY_MODE_ITEM; + bOpen = true; + } + if ( inputs.getUIInteraction(playernum)->selectedItem ) + { + inputs.getUIInteraction(playernum)->selectedItem = nullptr; + inputs.getUIInteraction(playernum)->toggleclick = false; + } + inputs.getUIInteraction(playernum)->selectedItemFromChest = 0; + clearItemDisplayed(); +} + +void GenericGUIMenu::MailboxGui_t::updateMailMenu() +{ + const int playernum = parentGUI.getPlayer(); + auto player = players[playernum]; + + if ( !player->isLocalPlayer() ) + { + closeMailMenu(); + return; + } + + Entity* mailboxStation = nullptr; + if ( bOpen ) + { + if ( parentGUI.mailboxEntityUid != 0 ) + { + mailboxStation = uidToEntity(parentGUI.mailboxEntityUid); + if ( !mailboxStation ) + { + parentGUI.closeGUI(); + return; + } + } + + if ( mailboxStation ) + { + if ( player->entity && (entityDist(player->entity, mailboxStation) > TOUCHRANGE) ) + { + parentGUI.closeGUI(); + return; + } + } + } + + if ( !mailFrame ) + { + return; + } + + mailFrame->setSize(SDL_Rect{ players[playernum]->camera_virtualx1(), + players[playernum]->camera_virtualy1(), + mailBaseWidth, + players[playernum]->camera_virtualHeight() }); + + if ( !mailFrame->isDisabled() && bOpen ) + { + if ( !mailGUIHasBeenCreated() ) + { + createMailMenu(); + } + + const real_t fpsScale = getFPSScale(50.0); // ported from 50Hz + real_t setpointDiffX = fpsScale * std::max(.01, (1.0 - animx)) / 2.0; + animx += setpointDiffX; + animx = std::min(1.0, animx); + if ( animx >= .9999 ) + { + if ( !bFirstTimeSnapCursor ) + { + bFirstTimeSnapCursor = true; + if ( !inputs.getUIInteraction(playernum)->selectedItem + && player->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX ) + { + //warpMouseToSelectedAlchemyItem(nullptr, (Inputs::SET_CONTROLLER)); + } + } + isInteractable = true; + } + } + else + { + animx = 0.0; + animTooltip = 0.0; + isInteractable = false; + } + + auto mailFramePos = mailFrame->getSize(); + if ( player->inventoryUI.inventoryPanelJustify == Player::PANEL_JUSTIFY_LEFT ) + { + if ( !player->inventoryUI.bCompactView ) + { + const int fullWidth = mailFramePos.w + 210; // inventory width 210 + mailFramePos.x = -mailFramePos.w + animx * fullWidth; + if ( player->bUseCompactGUIWidth() ) + { + if ( player->inventoryUI.slideOutPercent >= .0001 ) + { + isInteractable = false; + } + mailFramePos.x -= player->inventoryUI.slideOutWidth * player->inventoryUI.slideOutPercent; + } + } + else + { + if ( player->bAlignGUINextToInventoryCompact() ) + { + const int fullWidth = mailFramePos.w + 210; // inventory width 210 + mailFramePos.x = -mailFramePos.w + animx * fullWidth; + } + else + { + mailFramePos.x = player->camera_virtualWidth() - animx * mailFramePos.w; + } + if ( player->bUseCompactGUIWidth() ) + { + if ( player->inventoryUI.slideOutPercent >= .0001 ) + { + isInteractable = false; + } + mailFramePos.x -= -player->inventoryUI.slideOutWidth * player->inventoryUI.slideOutPercent; + } + } + } + else if ( player->inventoryUI.inventoryPanelJustify == Player::PANEL_JUSTIFY_RIGHT ) + { + if ( !player->inventoryUI.bCompactView ) + { + const int fullWidth = mailFramePos.w + 210; // inventory width 210 + mailFramePos.x = player->camera_virtualWidth() - animx * fullWidth; + if ( player->bUseCompactGUIWidth() ) + { + if ( player->inventoryUI.slideOutPercent >= .0001 ) + { + isInteractable = false; + } + mailFramePos.x -= -player->inventoryUI.slideOutWidth * player->inventoryUI.slideOutPercent; + } + } + else + { + if ( player->bAlignGUINextToInventoryCompact() ) + { + const int fullWidth = mailFramePos.w + 210; // inventory width 210 + mailFramePos.x = player->camera_virtualWidth() - animx * fullWidth; + } + else + { + mailFramePos.x = -mailFramePos.w + animx * mailFramePos.w; + } + if ( player->bUseCompactGUIWidth() ) + { + if ( player->inventoryUI.slideOutPercent >= .0001 ) + { + isInteractable = false; + } + mailFramePos.x -= player->inventoryUI.slideOutWidth * player->inventoryUI.slideOutPercent; + mailFramePos.w = player->camera_virtualWidth(); + } + } + } + + int mailItemAnimOffsetY = 0; // all animations tested at heightOffsetWhenNotCompact = 200, so needs offset + if ( !player->bUseCompactGUIHeight() && !player->bUseCompactGUIWidth() ) + { + mailFramePos.y = heightOffsetWhenNotCompact; + mailItemAnimOffsetY = 200 - heightOffsetWhenNotCompact; + } + else + { + mailFramePos.y = 0; + } + + if ( !mailGUIHasBeenCreated() ) + { + return; + } + + auto baseFrame = mailFrame->findFrame("mail base"); + baseFrame->setDisabled(false); + if ( auto baseFrameImg = baseFrame->findImage("mail base img") ) + { + baseFrameImg->path = "*#images/ui/Shrines/pillar_box/PillarBox_Base_00.png"; + } + + mailFrame->setSize(mailFramePos); + + SDL_Rect baseFramePos = baseFrame->getSize(); + baseFramePos.x = 0; + baseFramePos.w = mailBaseWidth; + baseFrame->setSize(baseFramePos); + + mailFramePos.h = baseFramePos.y + baseFramePos.h; + if ( animx >= .9999 ) + { + baseFramePos.x += mailFramePos.x; + mailFramePos.w += mailFramePos.x; + mailFramePos.w = std::min(player->camera_virtualWidth(), mailFramePos.w); + mailFramePos.x = 0; + baseFrame->setSize(baseFramePos); + } + mailFrame->setSize(mailFramePos); + + /*if ( bOpen ) + { + for ( int x = 0; x < MAX_ALCH_X; ++x ) + { + for ( int y = 0; y < MAX_ALCH_Y; ++y ) + { + if ( auto slotFrame = getMailSlotFrame(x, y) ) + { + slotFrame->setDisabled(true); + } + } + } + }*/ + + /*if ( animx >= 0.999 ) + { + SDL_Rect recipePos = recipesFrame->getSize(); + if ( !player->inventoryUI.bCompactView ) + { + if ( player->inventoryUI.inventoryPanelJustify == Player::PANEL_JUSTIFY_LEFT ) + { + alchFramePos.w += recipePos.w; + alchFrame->setSize(alchFramePos); + } + } + else + { + if ( player->inventoryUI.inventoryPanelJustify == Player::PANEL_JUSTIFY_LEFT + && player->bAlignGUINextToInventoryCompact() ) + { + alchFramePos.w += recipePos.w; + alchFrame->setSize(alchFramePos); + } + } + }*/ + + /*if ( auto emptyBottleFrame = baseFrame->findFrame("empty bottles") ) + { + emptyBottleFrame->setUserData(&GAMEUI_FRAMEDATA_ALCHEMY_RECIPE_ENTRY); + if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + updateSlotFrameFromItem(emptyBottleFrame, &emptyBottleCount); + } + else + { + updateSlotFrameFromItem(emptyBottleFrame, &torchCount); + if ( torchCount.count < 0 ) + { + auto spriteImageFrame = emptyBottleFrame->findFrame("item sprite frame"); + auto spriteImage = spriteImageFrame->findImage("item sprite img"); + spriteImage->path = "*#images/ui/Alchemy/Campfire.png"; + } + } + }*/ + + static ConsoleVariable cvar_mail_send_item_destx1("/mail_send_item_destx1", 36); + static ConsoleVariable cvar_mail_send_item_desty1("/mail_send_item_desty1", 128); + animSendItem1DestX = baseFrame->getSize().x + *cvar_mail_send_item_destx1; + animSendItem1DestY = baseFrame->getSize().y + *cvar_mail_send_item_desty1; + animRecvItemStartX = baseFrame->getSize().x + 74; + animRecvItemStartY = baseFrame->getSize().y + 286; + + { + const real_t fpsScale = getFPSScale(50.0); // ported from 50Hz + real_t setpointDiffX = fpsScale * std::max(.05, (animRecvItem)) / 3.0; + animRecvItem -= setpointDiffX; + animRecvItem = std::max(0.0, animRecvItem); + } + + auto animSendItem1Frame = getMailSlotFrame(MAIL_SLOT_SEND, 0); + animSendItem1Frame->setDisabled(true); + Item* mailSendItem1 = nullptr; + + { + const real_t fpsScale = getFPSScale(50.0); // ported from 50Hz + real_t setpointDiffX = fpsScale * std::max(.05, (animSendItem1)) / 3.0; + animSendItem1 -= setpointDiffX; + animSendItem1 = std::max(0.0, animSendItem1); + } + if ( sendItem1Uid != 0 ) + { + if ( mailSendItem1 = uidToItem(sendItem1Uid) ) + { + if ( !mailSendItem1->identified || itemIsEquipped(mailSendItem1, playernum) + || mailSendItem1->status == BROKEN ) + { + mailSendItem1 = nullptr; // if this got unidentified somehow, remove it + sendItem1Uid = 0; + animSendItem1 = 0.0; + } + else + { + animSendItem1Frame->setDisabled(false); + if ( animSendItem1 < 0.001 ) + { + animSendItem1Frame->setUserData(nullptr); + } + else + { + animSendItem1Frame->setUserData(&GAMEUI_FRAMEDATA_ANIMATING_ITEM); + } + //if ( potionResultUid == potion1Uid && animPotionResult > 0.001 ) // we're animating to this potion + //{ + // animPotion1Frame->setUserData(&GAMEUI_FRAMEDATA_ALCHEMY_ITEM); + // int oldCount = potion1Item->count; + // potion1Item->count = std::max(0, potion1Item->count - animPotionResultCount); + // updateSlotFrameFromItem(animPotion1Frame, potion1Item); + // potion1Item->count = oldCount; + //} + //else + //{ + // if ( alchemyMissingIngredientQty(potion1Item) ) + // { + // if ( animPotion1 < 0.001 ) + // { + // animPotion1Frame->setUserData(&GAMEUI_FRAMEDATA_ALCHEMY_MISSING_QTY); + // } + // } + //} + updateSlotFrameFromItem(animSendItem1Frame, mailSendItem1); + } + } + } + auto animSendItem1Pos = animSendItem1Frame->getSize(); + animSendItem1Pos.x = animSendItem1StartX + (1.0 - animSendItem1) * (animSendItem1DestX - animSendItem1StartX); + animSendItem1Pos.y = animSendItem1StartY + (1.0 - animSendItem1) * (animSendItem1DestY - animSendItem1StartY); + animSendItem1Frame->setSize(animSendItem1Pos); + + auto recvItemFrame = getMailSlotFrame(MAIL_SLOT_RECV, 0); + recvItemFrame->setDisabled(true); + + if ( keystatus[SDLK_g] ) + { + mailReceiveItem.type = IRON_AXE; + } + + { + if ( animRecvItem < 0.001 ) + { + if ( auto item = uidToItem(recvItemUid) ) + { + if ( auto slotFrame = player->inventoryUI.getInventorySlotFrame(item->x, item->y) ) + { + // one-off update to prevent a blank frame + slotFrame->setUserData(nullptr); + updateSlotFrameFromItem(slotFrame, item); + } + } + recvItemUid = 0; + } + } + auto animRecvItemPos = recvItemFrame->getSize(); + if ( recvItemUid == 0 ) + { + animRecvItemPos.x = animRecvItemStartX; + animRecvItemPos.y = animRecvItemStartY; + } + else + { + animRecvItemPos.x = animRecvItemStartX + (1.0 - animRecvItem) * (animRecvItemDestX - animRecvItemStartX); + animRecvItemPos.y = animRecvItemStartY + (1.0 - animRecvItem) * (animRecvItemDestY - animRecvItemStartY); + } + recvItemFrame->setSize(animRecvItemPos); + recvItemFrame->setOpacity(100.0); + if ( recvItemUid != 0 ) + { + if ( auto item = uidToItem(recvItemUid) ) + { + recvItemFrame->setDisabled(false); + int oldCount = item->count; + item->count = 1; + recvItemFrame->setUserData(&GAMEUI_FRAMEDATA_ANIMATING_ITEM); + updateSlotFrameFromItem(recvItemFrame, item); + if ( animRecvItem < .25 && itemIsEquipped(item, playernum) ) + { + recvItemFrame->setOpacity(100.0 * (animRecvItem / .25)); + } + item->count = oldCount; + } + } + else if ( mailReceiveItem.type != POTION_EMPTY ) + { + recvItemFrame->setDisabled(false); + recvItemFrame->setUserData(nullptr); + updateSlotFrameFromItem(recvItemFrame, &mailReceiveItem); + } + + if ( !bOpen ) + { + return; + } + + if ( !parentGUI.isGUIOpen() + || parentGUI.guiType != GUICurrentType::GUI_TYPE_MAILBOX + || !stats[playernum] + || stats[playernum]->HP <= 0 + || !player->entity + || player->shootmode ) + { + closeMailMenu(); + return; + } + + if ( player->entity && player->entity->isBlind() ) + { + messagePlayer(playernum, MESSAGE_MISC, Language::get(4159)); + parentGUI.closeGUI(); + return; // I can't see! + } + + /*if ( keystatus[SDLK_B] && enableDebugKeys ) + { + keystatus[SDLK_B] = 0; + notifications.push_back(std::make_pair(ticks, AlchNotification_t("Wow a title!", "This is a body", "items/images/Alembic.png"))); + }*/ + //auto notificationFrame = alchFrame->findFrame("notification"); + //notificationFrame->setDisabled(true); + //if ( !notifications.empty() ) + //{ + // auto& n = notifications.front(); + // SDL_Rect notifPos = notificationFrame->getSize(); + // if ( (player->inventoryUI.inventoryPanelJustify == Player::PanelJustify_t::PANEL_JUSTIFY_LEFT + // && !player->inventoryUI.bCompactView) + // || (player->inventoryUI.inventoryPanelJustify == Player::PanelJustify_t::PANEL_JUSTIFY_RIGHT + // && player->inventoryUI.bCompactView) ) + // { + // notifPos.x = baseFrame->getSize().x + baseFrame->getSize().w - notifPos.w * (1.0 - n.second.animx); + // } + // else + // { + // notifPos.x = baseFrame->getSize().x - notifPos.w * (n.second.animx); + // } + // notifPos.y = 4; + // notificationFrame->setSize(notifPos); + // notificationFrame->setOpacity(100.0 * n.second.animx); + // notificationFrame->setDisabled(false); + + // auto notifTitle = notificationFrame->findField("notif title"); + // notifTitle->setText(n.second.title.c_str()); + // auto notifBody = notificationFrame->findField("notif body"); + // notifBody->setText(n.second.body.c_str()); + // auto notifIcon = notificationFrame->findImage("notif icon"); + // notifIcon->path = n.second.img; + + // if ( n.second.state == 0 ) + // { + // const real_t fpsScale = getFPSScale(50.0); // ported from 50Hz + // real_t setpointDiffX = fpsScale * std::max(.01, (1.0 - n.second.animx)) / 2.0; + // n.second.animx += setpointDiffX; + // n.second.animx = std::min(1.0, n.second.animx); + // if ( n.second.animx >= 0.999 ) + // { + // n.second.state = 1; + // n.first = ticks; + // } + // } + // else if ( n.second.state == 1 ) + // { + // if ( ticks - n.first > TICKS_PER_SECOND * 3 ) + // { + // n.second.state = 2; + // } + // } + // else if ( n.second.state == 2 ) + // { + // const real_t fpsScale = getFPSScale(50.0); // ported from 50Hz + // real_t setpointDiffX = fpsScale * std::max(.05, (n.second.animx)) / 3.0; + // n.second.animx -= setpointDiffX; + // n.second.animx = std::max(0.0, n.second.animx); + // if ( n.second.animx <= 0.001 ) + // { + // n.second.state = 3; + // notifications.erase(notifications.begin()); + // } + // } + //} + + // alembic status + { + auto mailTitle = baseFrame->findField("mail title"); + if ( mailboxStation ) + { + mailTitle->setText(Language::get(6983)); + } + else + { + mailTitle->setText(""); + } + + SDL_Rect textPos{ 0, 21, baseFrame->getSize().w, 24 }; + textPos.y -= 14; + mailTitle->setSize(textPos); + } + + bool usingGamepad = inputs.hasController(playernum) && !inputs.getVirtualMouse(playernum)->draw_cursor; + + { + // close btn + auto closeBtn = baseFrame->findButton("close mail button"); + auto closeGlyph = baseFrame->findImage("close mail glyph"); + closeBtn->setDisabled(true); + closeGlyph->disabled = true; + if ( inputs.getVirtualMouse(playernum)->draw_cursor ) + { + closeBtn->setDisabled(!isInteractable); + if ( isInteractable ) + { + buttonMailUpdateSelectorOnHighlight(playernum, closeBtn); + } + } + else if ( closeBtn->isSelected() ) + { + closeBtn->deselect(); + } + if ( closeBtn->isDisabled() && usingGamepad && sendItem1Uid == 0 ) + { + closeGlyph->path = Input::inputs[playernum].getGlyphPathForBinding("MenuCancel"); + if ( auto imgGet = Image::get(closeGlyph->path.c_str()) ) + { + closeGlyph->pos.w = imgGet->getWidth(); + closeGlyph->pos.h = imgGet->getHeight(); + closeGlyph->disabled = false; + } + + closeGlyph->pos.x = closeBtn->getSize().x - closeGlyph->pos.w; + closeGlyph->pos.y = closeBtn->getSize().y + closeBtn->getSize().h / 2 - closeGlyph->pos.h / 2; + if ( closeGlyph->pos.y % 2 == 1 ) + { + ++closeGlyph->pos.y; + } + } + } + + //{ + // stationCookBtn->setDisabled(true); + // //recipeGlyph->disabled = true; + // if ( inputs.getVirtualMouse(playernum)->draw_cursor && !stationCookBtn->isInvisible() ) + // { + // stationCookBtn->setDisabled(!isInteractable); + // if ( isInteractable ) + // { + // buttonAlchemyUpdateSelectorOnHighlight(playernum, stationCookBtn); + // } + // } + // else if ( stationCookBtn->isSelected() ) + // { + // stationCookBtn->deselect(); + // } + + // stationToggleGlyph->disabled = true; + // if ( stationCookBtn->isDisabled() && usingGamepad && !stationCookBtn->isInvisible() ) + // { + // stationToggleGlyph->path = Input::inputs[playernum].getGlyphPathForBinding("MenuPageLeft"); + // if ( auto imgGet = Image::get(stationToggleGlyph->path.c_str()) ) + // { + // stationToggleGlyph->pos.w = imgGet->getWidth(); + // stationToggleGlyph->pos.h = imgGet->getHeight(); + // stationToggleGlyph->disabled = false; + // } + // stationToggleGlyph->pos.x = stationCookBtn->getSize().x - stationToggleGlyph->pos.w - 12; + // if ( stationToggleGlyph->pos.x % 2 == 1 ) + // { + // ++stationToggleGlyph->pos.x; + // } + // stationToggleGlyph->pos.y = stationCookBtn->getSize().y + stationCookBtn->getSize().h - 6; + // } + + // stationToggleGlyph2->disabled = true; + // if ( stationCookBtn->isDisabled() && usingGamepad && !stationCookBtn->isInvisible() ) + // { + // stationToggleGlyph2->path = Input::inputs[playernum].getGlyphPathForBinding("MenuPageRight"); + // if ( auto imgGet = Image::get(stationToggleGlyph2->path.c_str()) ) + // { + // stationToggleGlyph2->pos.w = imgGet->getWidth(); + // stationToggleGlyph2->pos.h = imgGet->getHeight(); + // stationToggleGlyph2->disabled = false; + // } + // stationToggleGlyph2->pos.x = stationCookBtn->getSize().x + stationCookBtn->getSize().w + 12; + // if ( stationToggleGlyph2->pos.x % 2 == 1 ) + // { + // ++stationToggleGlyph2->pos.x; + // } + // stationToggleGlyph2->pos.y = stationCookBtn->getSize().y + stationCookBtn->getSize().h - 6; + // } + //} + + Uint32 negativeColor = hudColors.characterSheetRed; + Uint32 neutralColor = hudColors.characterSheetLightNeutral; + Uint32 positiveColor = hudColors.characterSheetGreen; + Uint32 secondaryPositiveColor = hudColors.characterSheetHighlightText; + Uint32 defaultPromptColor = makeColor(255, 255, 255, 255); + + auto itemDisplayTooltip = baseFrame->findFrame("mail display tooltip"); + itemDisplayTooltip->setDisabled(false); + auto displayItemName = itemDisplayTooltip->findField("item display name"); + auto displayItemTextImg = itemDisplayTooltip->findImage("item text img"); + const int displayItemTextImgBaseX = 0; + displayItemTextImg->pos.x = displayItemTextImgBaseX; + displayItemTextImg->pos.y = 0; + SDL_Rect displayItemNamePos{ displayItemTextImg->pos.x + 8, displayItemTextImg->pos.y - 4, 170, displayItemName->getSize().h }; + displayItemName->setSize(displayItemNamePos); + + SDL_Rect tooltipPos = itemDisplayTooltip->getSize(); + tooltipPos.w = 186; + tooltipPos.h = baseFrame->getSize().h - 100; + tooltipPos.y = 152; + tooltipPos.x = 10;// 18 - (tooltipPos.w + 18) * (1.0 - animTooltip); + itemDisplayTooltip->setSize(tooltipPos); + + // calculate resultant potion + //{ + // if ( potion1Item && potion2Item && (currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK) ) + // { + // int createCount = 1; + // Status status = SERVICABLE; + // int missingPotion1Count = 0; + // int missingPotion2Count = 0; + // ItemType res = alchemyCookResult(playernum, potion1Item, potion2Item, createCount, status, missingPotion1Count, missingPotion2Count); + // alchemyResultPotion.type = res; + // alchemyResultPotion.count = createCount; + // alchemyResultPotion.status = status; + // alchemyResultPotion.identified = true; + // int appearance = 0; + // int blessing = 0; + // alchemyResultPotion.beatitude = 0; + // alchemyResultPotion.appearance = 0; + // alchemyResultPotion.appearance |= (missingPotion1Count & 0xFF) << 0; + // alchemyResultPotion.appearance |= (missingPotion2Count & 0xFF) << 8; + // } + // else if ( potion1Item && potion2Item && (currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES) ) + // { + // bool tryDuplicatePotion = false; + // bool randomResult = false; + // bool explodeSelf = false; + // bool samePotion = false; + // ItemType res = alchemyMixResult(potion1Item->type, potion2Item->type, randomResult, tryDuplicatePotion, samePotion, explodeSelf); + // Status status = EXCELLENT; + // alchemyResultPotion.identified = false; + // alchemyResultPotion.type = POTION_EMPTY; + // if ( samePotion || tryDuplicatePotion ) + // { + // alchemyResultPotion.count = 2; + // } + // else + // { + // alchemyResultPotion.count = 1; + // } + // int appearance = -1; + // int blessing = 0; + // if ( potion1Item->beatitude > 0 && potion2Item->beatitude > 0 ) + // { + // blessing = std::min(potion1Item->beatitude, potion2Item->beatitude); // take least blessed + // } + // else if ( potion1Item->beatitude < 0 && potion2Item->beatitude < 0 ) + // { + // blessing = std::min(potion1Item->beatitude, potion2Item->beatitude); // take most cursed + // } + // else if ( (potion1Item->beatitude < 0 && potion2Item->beatitude > 0) + // || (potion2Item->beatitude < 0 && potion1Item->beatitude > 0) ) + // { + // blessing = 0; + // } + // else if ( potion1Item->beatitude < 0 && potion2Item->beatitude == 0 ) + // { + // blessing = potion1Item->beatitude; // curse the result + // } + // else if ( potion1Item->beatitude == 0 && potion2Item->beatitude < 0 ) + // { + // blessing = potion2Item->beatitude; // curse the result + // } + // else if ( potion1Item->beatitude > 0 && potion2Item->beatitude == 0 ) + // { + // blessing = 0; // negate the blessing + // } + // else if ( potion1Item->beatitude == 0 && potion2Item->beatitude > 0 ) + // { + // blessing = 0; // negate the blessing + // } + // if ( samePotion ) + // { + // // same potion, keep the first potion only. + // res = potion1Item->type; + // if ( potion1Item->beatitude == potion2Item->beatitude ) + // { + // blessing = potion1Item->beatitude; + // } + // appearance = potion1Item->appearance; + // status = potion1Item->status; + // } + + // if ( tryDuplicatePotion && !explodeSelf && !randomResult ) + // { + // // duplicate chance + // if ( potion1Item->type == POTION_WATER ) + // { + // res = potion2Item->type; + // status = potion2Item->status; + // blessing = potion2Item->beatitude; + // appearance = potion2Item->appearance; + // } + // else if ( potion2Item->type == POTION_WATER ) + // { + // res = potion1Item->type; + // status = potion1Item->status; + // blessing = potion1Item->beatitude; + // appearance = potion1Item->appearance; + // } + // } + // else + // { + // tryDuplicatePotion = false; + // } + // alchemyResultPotion.status = status; + // if ( !(potion1Item->identified && potion2Item->identified) || randomResult ) + // { + // alchemyResultPotion.identified = false; + // } + // else + // { + // alchemyResultPotion.identified = tryDuplicatePotion || samePotion || + // playerKnowsRecipe(playernum, potion1Item->type, potion2Item->type, res); + // } + // bool doRandomAppearances = false; + // if ( alchemyResultPotion.identified ) + // { + // alchemyResultPotion.type = res; + // if ( res == POTION_SICKNESS && !samePotion ) + // { + // doRandomAppearances = true; + // } + // else + // { + // animRandomPotionTicks = 0; + // } + // } + // else + // { + // alchemyResultPotion.type = POTION_BOOZE; + // doRandomAppearances = true; + // } + + // if ( doRandomAppearances ) + // { + // if ( animRandomPotionTicks == 0 ) + // { + // animRandomPotionTicks = ticks; + // animRandomPotionUpdatedThisTick = ticks; + // } + // else if ( (ticks != animRandomPotionTicks) && (ticks - animRandomPotionTicks) % TICKS_PER_SECOND == 0 ) + // { + // if ( animRandomPotionUpdatedThisTick != ticks ) + // { + // ++animRandomPotionVariation; + // animRandomPotionUpdatedThisTick = ticks; + // } + // if ( animRandomPotionVariation >= items[alchemyResultPotion.type].variations ) + // { + // animRandomPotionVariation = 0; + // } + // } + // } + + // // blessings + // if ( !tryDuplicatePotion && !samePotion ) + // { + // if ( parentGUI.alembicItem ) + // { + // if ( parentGUI.alembicItem->beatitude >= 1 ) + // { + // blessing = 1; + // } + // else if ( parentGUI.alembicItem->beatitude <= -1 ) + // { + // blessing = parentGUI.alembicItem->beatitude; + // } + // } + // if ( skillCapstoneUnlocked(playernum, PRO_ALCHEMY) ) + // { + // blessing = 2; + // if ( parentGUI.alembicItem ) + // { + // if ( parentGUI.alembicItem->beatitude <= -1 ) + // { + // blessing = std::min(-2, (int)parentGUI.alembicItem->beatitude); + // } + // else + // { + // blessing = std::max(2, (int)parentGUI.alembicItem->beatitude); + // } + // } + // } + // } + + // if ( explodeSelf && alchemyResultPotion.identified ) + // { + // alchemyResultPotion.beatitude = 0; + // } + // else + // { + // alchemyResultPotion.beatitude = blessing; + // } + + // // appearances + // for ( auto it = potionStandardAppearanceMap.begin(); it != potionStandardAppearanceMap.end(); ++it ) + // { + // if ( (*it).first == alchemyResultPotion.type ) + // { + // if ( appearance == -1 ) + // { + // appearance = (*it).second; + // } + // } + // } + // alchemyResultPotion.appearance = std::max(0, appearance); + // if ( doRandomAppearances ) + // { + // alchemyResultPotion.appearance = animRandomPotionVariation; + // } + // } + // else + // { + // animRandomPotionTicks = 0; + // } + //} + + bool modifierPressed = false; + if ( usingGamepad && Input::inputs[playernum].binary("MenuPageLeftAlt") ) + { + modifierPressed = true; + } + else if ( inputs.bPlayerUsingKeyboardControl(playernum) + && (keystatus[SDLK_LSHIFT] || keystatus[SDLK_RSHIFT]) ) + { + modifierPressed = true; + } + + /*auto brewBtn = baseFrame->findButton("brew button"); + auto brewGlyph = baseFrame->findImage("brew glyph"); + brewBtn->setDisabled(true); + brewGlyph->disabled = true; + if ( inputs.getVirtualMouse(playernum)->draw_cursor ) + { + brewBtn->setText(Language::get(4175)); + if ( (potion1Uid != 0 || potion2Uid != 0) && isInteractable ) + { + brewBtn->setDisabled(false); + brewBtn->setTextColor(makeColor(255, 255, 255, 255)); + buttonAlchemyUpdateSelectorOnHighlight(playernum, brewBtn); + } + else + { + brewBtn->setTextColor(hudColors.characterSheetFaintText); + } + } + else if ( brewBtn->isSelected() ) + { + brewBtn->deselect(); + } + if ( usingGamepad && !inputs.getVirtualMouse(playernum)->draw_cursor ) + { + if ( potionResultFrame->isDisabled() || alchemyMissingIngredientQty(nullptr) ) + { + brewBtn->setTextColor(hudColors.characterSheetFaintText); + } + else + { + brewBtn->setTextColor(makeColor(255, 255, 255, 255)); + } + if ( currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + brewBtn->setText(Language::get(6773)); + } + else + { + brewBtn->setText(Language::get(4178)); + } + brewBtn->setDisabled(true); + if ( !potionResultFrame->isDisabled() && !alchemyMissingIngredientQty(nullptr) ) + { + brewGlyph->path = Input::inputs[playernum].getGlyphPathForBinding("MenuAlt2"); + if ( auto imgGet = Image::get(brewGlyph->path.c_str()) ) + { + brewGlyph->pos.w = imgGet->getWidth(); + brewGlyph->pos.h = imgGet->getHeight(); + brewGlyph->disabled = false; + } + brewGlyph->pos.x = brewBtn->getSize().x + brewBtn->getSize().w - 16; + if ( brewGlyph->pos.x % 2 == 1 ) + { + ++brewGlyph->pos.x; + } + brewGlyph->pos.y = brewBtn->getSize().y + brewBtn->getSize().h - 16; + } + }*/ + + bool inventoryControlActive = player->bControlEnabled + && !gamePaused + && !player->usingCommand() + && !player->GUI.isDropdownActive(); + + if ( isInteractable && !inputs.getUIInteraction(playernum)->selectedItem + && inventoryControlActive + && player->GUI.bModuleAccessibleWithMouse(Player::GUI_t::MODULE_MAILBOX) + && player->GUI.bActiveModuleUsesInventory() + && player->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX ) + { + if ( getSelectedMailSlotX() >= MAIL_SLOT_RECV && getSelectedMailSlotX() < 0 + && getSelectedMailSlotY() == 0 ) + { + if ( auto slotFrame = getMailSlotFrame(getSelectedMailSlotX(), getSelectedMailSlotY()) ) + { + if ( !slotFrame->isDisabled() && slotFrame->capturesMouse() ) + { + switch ( getSelectedMailSlotX() ) + { + case MAIL_SLOT_SEND: + setItemDisplayNameAndPrice(mailSendItem1, false); + break; + case MAIL_SLOT_RECV: + setItemDisplayNameAndPrice(&mailReceiveItem, true); + break; + default: + break; + } + } + } + } + } + + auto activateSelectionGlyph = mailFrame->findImage("activate glyph"); + auto activateSelectionPrompt = mailFrame->findField("activate prompt"); + if ( !strcmp(activateSelectionPrompt->getText(), "") ) + { + activateSelectionGlyph->disabled = true; + activateSelectionPrompt->setDisabled(true); + } + + if ( itemType != -1 && itemDesc.size() > 1 ) + { + if ( isInteractable ) + { + //const real_t fpsScale = getFPSScale(50.0); // ported from 50Hz + //real_t setpointDiffX = fpsScale * std::max(.01, (1.0 - animTooltip)) / 2.0; + //animTooltip += setpointDiffX; + //animTooltip = std::min(1.0, animTooltip); + animTooltip = 1.0; + animTooltipTicks = ticks; + } + + itemDisplayTooltip->setDisabled(false); + + { + // item name + text bg + displayItemName->setVJustify(Field::justify_t::CENTER); + displayItemName->setHJustify(Field::justify_t::CENTER); + if ( itemRequiresTitleReflow ) + { + displayItemName->setText(itemDesc.c_str()); + displayItemName->reflowTextToFit(0); + + if ( displayItemName->getNumTextLines() > 2 || true ) + { + auto pos = displayItemName->getSize(); + pos.h = 74; + displayItemName->setSize(pos); + displayItemTextImg->path = "*#images/ui/Alchemy/Alchemy_LabelName_3Row_00.png"; + displayItemTextImg->pos.h = 64; + } + else + { + auto pos = displayItemName->getSize(); + pos.h = 50; + displayItemName->setSize(pos); + displayItemTextImg->path = "*#images/ui/Alchemy/Alchemy_LabelName_2Row_00.png"; + displayItemTextImg->pos.h = 42; + } + if ( displayItemName->getNumTextLines() > 3 ) + { + // more than 2 lines, append ... + std::string copiedName = displayItemName->getText(); + auto lastNewline = copiedName.find_last_of('\n'); + copiedName = copiedName.substr(0U, lastNewline); + copiedName += "..."; + displayItemName->setText(copiedName.c_str()); + displayItemName->reflowTextToFit(0); + if ( displayItemName->getNumTextLines() > 3 ) + { + // ... doesn't fit, replace last 3 characters with ... + copiedName = copiedName.substr(0U, copiedName.size() - 6); + copiedName += "..."; + displayItemName->setText(copiedName.c_str()); + displayItemName->reflowTextToFit(0); + } + } + + // do highlights + displayItemName->clearWordsToHighlight(); + std::string str = displayItemName->getText(); + if ( str == Language::get(4167) ) + { + displayItemName->setTextColor(hudColors.characterSheetRed); + } + else + { + displayItemName->setTextColor(hudColors.characterSheetLightNeutral); + } + int wordIndex = 0; + bool prevCharWasWordSeparator = false; + int numLines = 0; + for ( size_t c = 0; c < str.size(); ++c ) + { + if ( str[c] == '\n' ) + { + wordIndex = -1; + ++numLines; + prevCharWasWordSeparator = true; + continue; + } + + if ( prevCharWasWordSeparator && !charIsWordSeparator(str[c]) ) + { + ++wordIndex; + if ( str[c] == '[' ) + { + std::string toCompare = str.substr(str.find('[', c)); + Uint32 color = 0; + if ( toCompare == Language::get(4155) ) + { + // unknown + color = hudColors.characterSheetLightNeutral; + } + else if ( toCompare == Language::get(4162) + || toCompare == Language::get(4164) || toCompare == Language::get(4166) ) + { + color = makeColor(54, 144, 171, 255); + } + else if ( toCompare == Language::get(4156) ) + { + // base pot + color = hudColors.characterSheetGreen; + } + else if ( toCompare == Language::get(4163) ) + { + // duplication chance + color = hudColors.characterSheetGreen; + } + else if ( toCompare == Language::get(4157) ) + { + // secondary pot + color = hudColors.characterSheetHighlightText; + } + else if ( toCompare == Language::get(4158) ) + { + color = hudColors.characterSheetFaintText; + } + else if ( toCompare == Language::get(4160) + || toCompare == Language::get(4165) + || toCompare == Language::get(6772) + || toCompare == Language::get(6768) + || toCompare == Language::get(6774) + || toCompare == Language::get(4168) ) + { + color = hudColors.characterSheetRed; + } + displayItemName->addWordToHighlight(wordIndex + numLines * Field::TEXT_HIGHLIGHT_WORDS_PER_LINE, + color); + displayItemName->addWordToHighlight((wordIndex + 1) + numLines * Field::TEXT_HIGHLIGHT_WORDS_PER_LINE, + color); + displayItemName->addWordToHighlight((wordIndex + 2) + numLines * Field::TEXT_HIGHLIGHT_WORDS_PER_LINE, + color); + displayItemName->addWordToHighlight((wordIndex + 3) + numLines * Field::TEXT_HIGHLIGHT_WORDS_PER_LINE, + color); + } + prevCharWasWordSeparator = false; + if ( !(c + 1 < str.size() && charIsWordSeparator(str[c + 1])) ) + { + continue; + } + } + + if ( charIsWordSeparator(str[c]) ) + { + prevCharWasWordSeparator = true; + } + else + { + prevCharWasWordSeparator = false; + } + } + itemRequiresTitleReflow = false; + } + } + + if ( itemActionType == MAIL_ACTION_OK && strcmp(activateSelectionPrompt->getText(), "") ) + { + if ( usingGamepad ) + { + activateSelectionGlyph->path = Input::inputs[playernum].getGlyphPathForBinding("MenuConfirm"); + } + else + { + activateSelectionGlyph->path = Input::inputs[playernum].getGlyphPathForBinding("MenuRightClick"); + } + if ( auto imgGet = Image::get(activateSelectionGlyph->path.c_str()) ) + { + activateSelectionGlyph->pos.w = imgGet->getWidth(); + activateSelectionGlyph->pos.h = imgGet->getHeight(); + } + + SDL_Rect pos{ 0, 0, activateSelectionGlyph->pos.w, activateSelectionGlyph->pos.h }; + pos.x += baseFrame->getSize().x + itemDisplayTooltip->getSize().x; + pos.y += baseFrame->getSize().y + itemDisplayTooltip->getSize().y; + pos.x += displayItemTextImg->pos.x + displayItemTextImg->pos.w / 2; + pos.y += displayItemTextImg->pos.y + displayItemTextImg->pos.h; + pos.y += 4; + + auto activateSelectionPromptPos = SDL_Rect{ pos.x, pos.y + 1, baseFrame->getSize().w, 24 }; + if ( auto textGet = activateSelectionPrompt->getTextObject() ) + { + activateSelectionPromptPos.x -= textGet->getWidth() / 2; + activateSelectionPromptPos.x += (8 + pos.w) / 2; + pos.x = activateSelectionPromptPos.x - 8 - pos.w; + pos.y += activateSelectionPrompt->getSize().h / 2; + pos.y -= pos.h / 2; + } + activateSelectionPrompt->setSize(activateSelectionPromptPos); + if ( pos.x % 2 == 1 ) + { + ++pos.x; + } + if ( pos.y % 2 == 1 ) + { + --pos.y; + } + activateSelectionGlyph->pos = pos; + activateSelectionGlyph->disabled = false; + activateSelectionPrompt->setDisabled(false); + } + } + else + { + if ( (!usingGamepad && (ticks - animTooltipTicks > TICKS_PER_SECOND / 3)) + || (usingGamepad) + || animTooltip < 0.9999 ) + { + const real_t fpsScale = getFPSScale(50.0); // ported from 50Hz + real_t setpointDiffX = fpsScale * std::max(.01, (animTooltip)) / 2.0; + animTooltip -= setpointDiffX; + animTooltip = std::max(0.0, animTooltip); + } + } + + { + SDL_Color color; + getColor(displayItemTextImg->color, &color.r, &color.g, &color.b, &color.a); + color.a = (Uint8)(192 * animTooltip); + displayItemTextImg->color = (makeColor(color.r, color.g, color.b, color.a)); + } + + { + SDL_Color color; + getColor(displayItemName->getColor(), &color.r, &color.g, &color.b, &color.a); + color.a = (Uint8)(255 * animTooltip); + displayItemName->setColor(makeColor(color.r, color.g, color.b, color.a)); + } + + { + SDL_Color color; + getColor(activateSelectionPrompt->getColor(), &color.r, &color.g, &color.b, &color.a); + color.a = (Uint8)(255 * animTooltip); + activateSelectionPrompt->setColor(makeColor(color.r, color.g, color.b, color.a)); + } + + { + SDL_Color color; + getColor(activateSelectionGlyph->color, &color.r, &color.g, &color.b, &color.a); + color.a = (Uint8)(255 * animTooltip); + activateSelectionGlyph->color = (makeColor(color.r, color.g, color.b, color.a)); + } + + bool tryBrew = false; + bool activateSelection = false; + if ( isInteractable ) + { + if ( !inputs.getUIInteraction(playernum)->selectedItem + && !player->GUI.isDropdownActive() + && (player->GUI.bModuleAccessibleWithMouse(Player::GUI_t::MODULE_MAILBOX) + || player->GUI.bModuleAccessibleWithMouse(Player::GUI_t::MODULE_INVENTORY)) + && player->bControlEnabled && !gamePaused + && !player->usingCommand() ) + { + + if ( Input::inputs[playernum].binaryToggle("MenuCancel") ) + { + Input::inputs[playernum].consumeBinaryToggle("MenuCancel"); + if ( sendItem1Uid == 0 ) + { + parentGUI.closeGUI(); + Player::soundCancel(); + return; + } + else if ( animRecvItem < 0.001 ) + { + if ( sendItem1Uid != 0 ) + { + sendItem1Uid = 0; + animSendItem1 = 0.0; + //animPotion1Frame->setDisabled(true); + } + Player::soundCancel(); + } + } + //else if ( Input::inputs[playernum].binaryToggle("MenuPageRightAlt") || Input::inputs[playernum].binaryToggle("MenuPageLeftAlt") ) + //{ + // bool left = Input::inputs[playernum].consumeBinaryToggle("MenuPageLeftAlt"); + // bool right = Input::inputs[playernum].consumeBinaryToggle("MenuPageRightAlt"); + // /*if ( left && !stationCookBtn->isInvisible() ) + // { + // stationCookBtn->activate(); + // }*/ + // if ( (left || right) && !recipeBtn->isInvisible() ) + // { + // recipeBtn->activate(); + // return; + // } + //} + //else if ( Input::inputs[playernum].binaryToggle("MenuPageRight") || Input::inputs[playernum].binaryToggle("MenuPageLeft") ) + //{ + // bool left = Input::inputs[playernum].consumeBinaryToggle("MenuPageLeft"); + // bool right = Input::inputs[playernum].consumeBinaryToggle("MenuPageRight"); + // if ( (left || right) && !stationCookBtn->isInvisible() ) + // { + // if ( left && (currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES) ) + // { + // stationCookBtn->activate(); + // return; + // } + // if ( right && (currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK) ) + // { + // stationCookBtn->activate(); + // return; + // } + // } + //} + //else if ( Input::inputs[playernum].binaryToggle("MenuAlt2") ) + //{ + // if ( !brewGlyph->disabled ) + // { + // activateSelection = true; + // tryBrew = true; + // Input::inputs[playernum].consumeBinaryToggle("MenuAlt2"); + // } + //} + else + { + if ( usingGamepad && Input::inputs[playernum].binaryToggle("MenuConfirm") ) + { + activateSelection = true; + Input::inputs[playernum].consumeBinaryToggle("MenuConfirm"); + } + else if ( !usingGamepad && Input::inputs[playernum].binaryToggle("MenuRightClick") ) + { + activateSelection = true; + Input::inputs[playernum].consumeBinaryToggle("MenuRightClick"); + } + } + } + } + + if ( activateSelection && players[playernum] && players[playernum]->entity + && animRecvItem < 0.001 ) + { + parentGUI.basePotion = nullptr; + parentGUI.secondaryPotion = nullptr; + if ( itemActionType != MAIL_ACTION_OK && itemActionType != MAIL_ACTION_NONE ) + { + playSound(90, 64); + } + if ( (player->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX || (tryBrew && player->GUI.activeModule == Player::GUI_t::MODULE_INVENTORY)) + && (itemActionType == MAIL_ACTION_OK || tryBrew) + ) + { + ItemType oldPotion1Type = WOODEN_SHIELD; + ItemType oldPotion2Type = WOODEN_SHIELD; + if ( (getSelectedMailSlotX() >= MAIL_SLOT_RECV && getSelectedMailSlotX() < 0 + && getSelectedMailSlotY() == 0) || tryBrew ) + { + if ( !tryBrew && getSelectedMailSlotX() == MAIL_SLOT_SEND ) + { + sendItem1Uid = 0; + animSendItem1 = 0.0; + animSendItem1Frame->setDisabled(true); + Player::soundCancel(); + } + else if ( tryBrew || getSelectedMailSlotX() == MAIL_SLOT_RECV ) + { + // recv it + if ( !recvItemFrame->isDisabled() + && mailReceiveItem.type != POTION_EMPTY + && recvItemUid == 0 ) + { + recvItemUid = 0; + + parentGUI.mailboxClaimItem(); + + if ( recvItemUid != 0 ) + { + if ( auto item = uidToItem(recvItemUid) ) + { + auto slotType = player->paperDoll.getSlotForItem(*item); + if ( recvItemUid == sendItem1Uid ) + { + animRecvItemDestX = animSendItem1DestX; + animRecvItemDestY = animSendItem1DestY; + animRecvItem = 1.0; + } + else if ( slotType != Player::PaperDoll_t::SLOT_MAX ) // on paper doll + { + animRecvItemDestX = animRecvItemStartX; + animRecvItemDestY = animRecvItemStartY - player->inventoryUI.getSlotSize(); + animRecvItem = 1.0; + } + else if ( auto slotFrame = player->inventoryUI.getInventorySlotFrame(item->x, item->y) ) + { + getInventoryItemAlchemyAnimSlotPos(slotFrame, player, item->x, item->y, animRecvItemDestX, animRecvItemDestY, mailItemAnimOffsetY); + animRecvItemDestY += 2; + animRecvItemDestY += (player->inventoryUI.bCompactView ? -2 : 0); + animRecvItem = 1.0; + } + } + } + if ( animRecvItem < .999 ) + { + recvItemUid = 0; + } + } + } + } + } + else if ( player->GUI.activeModule == Player::GUI_t::MODULE_INVENTORY + && itemActionType == MAIL_ACTION_OK ) + { + if ( auto slotFrame = player->inventoryUI.getInventorySlotFrame(player->inventoryUI.getSelectedSlotX(), + player->inventoryUI.getSelectedSlotY()) ) + { + for ( node_t* node = stats[playernum]->inventory.first; node != NULL; node = node->next ) + { + Item* item = (Item*)node->element; + if ( !item ) + { + continue; + } + if ( itemCategory(item) == SPELL_CAT ) + { + continue; + } + + if ( item->x == player->inventoryUI.getSelectedSlotX() + && item->y == player->inventoryUI.getSelectedSlotY() + && item->x >= 0 && item->x < player->inventoryUI.getPlayerItemInventoryX() + && item->y >= 0 && item->y < player->inventoryUI.getPlayerItemInventoryY() ) + { + if ( sendItem1Uid == item->uid ) + { + sendItem1Uid = 0; + animSendItem1 = 0.0; + animSendItem1Frame->setDisabled(true); + Player::soundCancel(); + } + else + { + if ( sendItem1Uid == 0 || true ) + { + if ( !parentGUI.isItemMailable(item) ) + { + continue; + } + animSendItem1 = 1.0; + getInventoryItemMailboxAnimSlotPos(slotFrame, player, item->x, item->y, animSendItem1StartX, animSendItem1StartY, mailItemAnimOffsetY); + sendItem1Uid = item->uid; + //alchemyResultPotion.type = POTION_EMPTY; + playSound(139, 64); // click sound + } + } + break; + } + } + } + } + } +} + +void GenericGUIMenu::mailboxClaimItem() +{ + auto& item = mailboxGUI.mailReceiveItem; + + Item* claimedItem = newItem(item.type, item.status, item.beatitude, item.count, item.appearance, item.identified, nullptr); + Item* pickedUp = itemPickup(gui_player, claimedItem); + if ( pickedUp ) + { + mailboxGUI.recvItemUid = pickedUp->uid; + messagePlayer(gui_player, MESSAGE_MISC, Language::get(504), claimedItem->description()); + //mailboxGUI.animPotionResultCount = alchemyGUI.alchemyResultPotion.count; + playSoundEntity(players[gui_player]->entity, 35 + local_rng.rand() % 3, 64); + } + + free(claimedItem); + claimedItem = nullptr; + + mailboxGUI.mailReceiveItem.type = POTION_EMPTY; +} + +void GenericGUIMenu::MailboxGui_t::createMailMenu() +{ + const int player = parentGUI.getPlayer(); + if ( !gui || !mailFrame || !players[player]->inventoryUI.frame ) + { + return; + } + if ( mailGUIHasBeenCreated() ) + { + return; + } + + SDL_Rect basePos{ 0, 0, mailBaseWidth, 308 }; + mailSlotFrames.clear(); + + const int inventorySlotSize = players[player]->inventoryUI.getSlotSize(); + + /*{ + auto notificationFrame = alchFrame->addFrame("notification"); + notificationFrame->setHollow(false); + notificationFrame->setBorder(0); + notificationFrame->setInheritParentFrameOpacity(false); + notificationFrame->setDisabled(true); + notificationFrame->setSize(SDL_Rect{ 0, 0, 180, 56 }); + + auto notifBg = notificationFrame->addImage(SDL_Rect{ 0, 0, 180, 56 }, 0xFFFFFFFF, + "*#images/ui/Alchemy/Alchemy_Notification_00.png", "notif bg"); + + auto notifIcon = notificationFrame->addImage(SDL_Rect{ 8, 56 / 2 - players[player]->inventoryUI.getItemSpriteSize() / 2, + players[player]->inventoryUI.getItemSpriteSize(), + players[player]->inventoryUI.getItemSpriteSize() }, 0xFFFFFFFF, + "", "notif icon"); + + auto title = notificationFrame->addField("notif title", 128); + title->setFont("fonts/pixel_maz_multiline.ttf#16#2"); + title->setText("New Title Unlocked!"); + title->setHJustify(Field::justify_t::LEFT); + title->setVJustify(Field::justify_t::TOP); + title->setSize(SDL_Rect{ notifIcon->pos.x + notifIcon->pos.w, 8, notificationFrame->getSize().w, 24 }); + title->setColor(makeColor(255, 255, 0, 255)); + + auto body = notificationFrame->addField("notif body", 128); + body->setFont("fonts/pixel_maz_multiline.ttf#16#2"); + body->setText("Blah Blah Blah!"); + body->setHJustify(Field::justify_t::LEFT); + body->setVJustify(Field::justify_t::TOP); + body->setSize(SDL_Rect{ notifIcon->pos.x + notifIcon->pos.w, 8 + 18, notificationFrame->getSize().w, 24 }); + body->setColor(makeColor(255, 255, 255, 255)); + }*/ + + { + auto bgFrame = mailFrame->addFrame("mail base"); + bgFrame->setSize(basePos); + bgFrame->setHollow(false); + bgFrame->setDisabled(true); + auto bg = bgFrame->addImage(SDL_Rect{ 0, 0, basePos.w, basePos.h }, + makeColor(255, 255, 255, 255), + "*#images/ui/Shrines/pillar_box/PillarBox_Base_00.png", "mail base img"); + + /*auto alembicItemIcon = bgFrame->addImage(SDL_Rect{ 11, 23, 36, 36 }, 0xFFFFFFFF, + "", "alchemy item icon"); + alembicItemIcon->disabled = true; + + auto alembicAlchemyBadge = bgFrame->addImage(SDL_Rect{ 8, 6, 190, 60 }, 0xFFFFFFFF, + "*#images/ui/Alchemy/Alchemy_Badge.png", "alchemy badge");*/ + + auto headerFont = "fonts/pixel_maz_multiline.ttf#16#2"; + auto mailTitle = bgFrame->addField("mail title", 128); + mailTitle->setFont(headerFont); + mailTitle->setText(""); + mailTitle->setHJustify(Field::justify_t::CENTER); + mailTitle->setVJustify(Field::justify_t::TOP); + mailTitle->setSize(SDL_Rect{ 0, 0, 0, 0 }); + mailTitle->setTextColor(hudColors.characterSheetLightNeutral); + mailTitle->setOutlineColor(makeColor(29, 16, 11, 255)); + /*auto alembicStatus = bgFrame->addField("alchemy alembic status", 128); + alembicStatus->setFont(headerFont); + alembicStatus->setText(""); + alembicStatus->setHJustify(Field::justify_t::CENTER); + alembicStatus->setVJustify(Field::justify_t::TOP); + alembicStatus->setSize(SDL_Rect{ 0, 0, 0, 0 }); + alembicStatus->setTextColor(hudColors.characterSheetLightNeutral); + alembicStatus->setOutlineColor(makeColor(29, 16, 11, 255));*/ + + auto itemFont = "fonts/pixel_maz_multiline.ttf#16#2"; + { + auto itemDisplayTooltip = bgFrame->addFrame("mail display tooltip"); + itemDisplayTooltip->setSize(SDL_Rect{ 0, 0, 186, 108 }); + itemDisplayTooltip->setHollow(true); + itemDisplayTooltip->setInheritParentFrameOpacity(false); + { + auto itemNameText = itemDisplayTooltip->addField("item display name", 1024); + itemNameText->setFont(itemFont); + itemNameText->setText(""); + itemNameText->setHJustify(Field::justify_t::LEFT); + itemNameText->setVJustify(Field::justify_t::TOP); + itemNameText->setSize(SDL_Rect{ 0, 0, 0, 0 }); + itemNameText->setTextColor(hudColors.characterSheetLightNeutral); + + auto itemDisplayTextBg = itemDisplayTooltip->addImage(SDL_Rect{ 0, 0, 186, 42 }, + 0xFFFFFFFF, "*#images/ui/Alchemy/Alchemy_LabelName_2Row_00.png", "item text img"); + } + } + + { + auto closeBtn = bgFrame->addButton("close mail button"); + SDL_Rect closeBtnPos{ basePos.w - 0 - 26, 0, 26, 26 }; + closeBtn->setSize(closeBtnPos); + closeBtn->setColor(makeColor(255, 255, 255, 255)); + closeBtn->setHighlightColor(makeColor(255, 255, 255, 255)); + closeBtn->setText("X"); + closeBtn->setFont(itemFont); + closeBtn->setHideGlyphs(true); + closeBtn->setHideKeyboardGlyphs(true); + closeBtn->setHideSelectors(true); + closeBtn->setMenuConfirmControlType(0); + closeBtn->setBackground("*#images/ui/Alchemy/Button_X_00.png"); + closeBtn->setBackgroundHighlighted("*#images/ui/Alchemy/Button_XHigh_00.png"); + closeBtn->setBackgroundActivated("*#images/ui/Alchemy/Button_XPress_00.png"); + closeBtn->setTextHighlightColor(makeColor(201, 162, 100, 255)); + closeBtn->setCallback([](Button& button) { + GenericGUI[button.getOwner()].closeGUI(); + Player::soundCancel(); + }); + closeBtn->setTickCallback(genericgui_deselect_fn); + + auto closeGlyph = bgFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "", "close mail glyph"); + closeGlyph->disabled = true; + closeGlyph->ontop = true; + + /*auto brewBtn = bgFrame->addButton("brew button"); + SDL_Rect brewPos{ basePos.w - 18 - 64, basePos.h - 20 - 46, 64, 46 }; + brewBtn->setSize(brewPos); + brewBtn->setColor(makeColor(255, 255, 255, 255)); + brewBtn->setHighlightColor(makeColor(255, 255, 255, 255)); + brewBtn->setText("Brew"); + brewBtn->setFont(itemFont); + brewBtn->setHideGlyphs(true); + brewBtn->setHideKeyboardGlyphs(true); + brewBtn->setHideSelectors(true); + brewBtn->setMenuConfirmControlType(0); + brewBtn->setBackground("*#images/ui/Alchemy/Alchemy_ButtonBrew_Base_00.png"); + brewBtn->setBackgroundHighlighted("*#images/ui/Alchemy/Alchemy_ButtonBrew_High_00.png"); + brewBtn->setBackgroundActivated("*#images/ui/Alchemy/Alchemy_ButtonBrew_Press_00.png"); + brewBtn->setTextHighlightColor(makeColor(201, 162, 100, 255)); + brewBtn->setCallback([](Button& button) { + int player = button.getOwner(); + auto& alchemyGUI = GenericGUI[player].alchemyGUI; + alchemyGUI.recipes.activateRecipeIndex = -1; + alchemyGUI.potion1Uid = 0; + alchemyGUI.animPotion1 = 0.0; + alchemyGUI.potion2Uid = 0; + alchemyGUI.animPotion2 = 0.0; + alchemyGUI.alchemyResultPotion.type = POTION_EMPTY; + alchemyGUI.potionResultUid = 0; + alchemyGUI.animPotionResult = 0.0; + alchemyGUI.animRecipeAutoAddToSlot1Uid = 0; + alchemyGUI.animRecipeAutoAddToSlot2Uid = 0; + Player::soundCancel(); + }); + brewBtn->setTickCallback(genericgui_deselect_fn); + + auto brewGlyph = bgFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "", "brew glyph"); + brewGlyph->disabled = true; + brewGlyph->ontop = true;*/ + } + + { + Frame* slotFrame = mailFrame->addFrame("mail send frame"); + SDL_Rect slotPos{ 0, 0, players[player]->inventoryUI.getSlotSize(), players[player]->inventoryUI.getSlotSize() }; + slotFrame->setSize(slotPos); + slotFrame->setDisabled(true); + slotFrame->setInheritParentFrameOpacity(false); + createPlayerInventorySlotFrameElements(slotFrame); + mailSlotFrames[MAIL_SLOT_SEND + 0 * 100] = slotFrame; + + slotFrame = mailFrame->addFrame("mail recv frame"); + slotFrame->setSize(slotPos); + slotFrame->setDisabled(true); + slotFrame->setInheritParentFrameOpacity(false); + createPlayerInventorySlotFrameElements(slotFrame); + mailSlotFrames[MAIL_SLOT_RECV + 0 * 100] = slotFrame; + } + } + + auto activateSelectionGlyph = mailFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "", "activate glyph"); + activateSelectionGlyph->disabled = true; + activateSelectionGlyph->ontop = true; + auto activateSelectionPrompt = mailFrame->addField("activate prompt", 64); + activateSelectionPrompt->setFont("fonts/pixel_maz_multiline.ttf#16#2"); + activateSelectionPrompt->setText(""); + activateSelectionPrompt->setHJustify(Field::justify_t::LEFT); + activateSelectionPrompt->setVJustify(Field::justify_t::TOP); + activateSelectionPrompt->setSize(SDL_Rect{ 0, 0, 0, 0 }); + activateSelectionPrompt->setColor(makeColor(255, 255, 255, 255)); + activateSelectionPrompt->setDisabled(true); + activateSelectionPrompt->setOntop(true); +} + +void GenericGUIMenu::MailboxGui_t::selectMailSlot(const int x, const int y) +{ + selectedMailSlotX = x; + selectedMailSlotY = y; +} + +Frame* GenericGUIMenu::MailboxGui_t::getMailSlotFrame(int x, int y) const +{ + if ( mailFrame ) + { + int key = x + y * 100; + if ( mailSlotFrames.find(key) != mailSlotFrames.end() ) + { + return mailSlotFrames.at(key); + } + } + return nullptr; +} + +bool GenericGUIMenu::MailboxGui_t::inventoryItemAllowedInGUI(Item* item) +{ + if ( !item ) { return false; } + if ( item->status == BROKEN ) + { + return false; + } + + if ( item->type == READABLE_BOOK || itemCategory(item) == SCROLL ) + { + return true; + } + /*if ( currentView == ALCHEMY_VIEW_BREW || currentView == ALCHEMY_VIEW_RECIPES ) + { + if ( itemCategory(item) == POTION && item->type != POTION_EMPTY ) + { + return true; + } + } + else if ( currentView == ALCHEMY_VIEW_COOK || currentView == ALCHEMY_VIEW_RECIPES_COOK ) + { + Item* item1 = potion1Uid != 0 ? uidToItem(potion1Uid) : nullptr; + Item* item2 = potion2Uid != 0 ? uidToItem(potion2Uid) : nullptr; + + if ( GenericGUIMenu::isItemRationSeasoning(item->type) || item->type == TOOL_TOWEL || item->type == POTION_WATER ) + { + if ( !item1 && !item2 ) + { + return true; + } + if ( (item1 && item1->type == FOOD_RATION) || (item2 && item2->type == FOOD_RATION) ) + { + return true; + } + if ( (item->type == TOOL_TOWEL || item->type == POTION_WATER || GenericGUIMenu::isItemRationSeasoning(item->type)) + && + ((item1 && item1->type == TOOL_TOWEL) || (item2 && item2->type == TOOL_TOWEL) + || (item1 && item1->type == POTION_WATER) || (item2 && item2->type == POTION_WATER) + || (item1 && GenericGUIMenu::isItemRationSeasoning(item1->type)) + || (item2 && GenericGUIMenu::isItemRationSeasoning(item2->type))) ) + { + return true; + } + } + else if ( itemCategory(item) == FOOD ) + { + if ( item->type == FOOD_RATION ) + { + if ( !item1 && !item2 ) + { + return true; + } + if ( (item1 && itemCategory(item1) == FOOD && item1->type != FOOD_RATION) + || (item2 && itemCategory(item2) == FOOD && item2->type != FOOD_RATION) ) + { + return false; + } + return true; + } + else if ( GenericGUIMenu::isItemRation(item->type) ) + { + return false; + } + else + { + if ( !item1 && !item2 ) + { + return true; + } + if ( (item1 && itemCategory(item1) == FOOD && item1->type != FOOD_RATION) + || (item2 && itemCategory(item2) == FOOD && item2->type != FOOD_RATION) ) + { + return true; + } + } + } + }*/ + + return false; +} + +void GenericGUIMenu::MailboxGui_t::setItemDisplayNameAndPrice(Item* item, const bool isTooltipForRecvItem) +{ + itemActionType = MAIL_ACTION_NONE; + if ( !item || item->type == SPELL_ITEM ) + { + clearItemDisplayed(); + } + if ( !item ) + { + return; + } + + char buf[1024]; + if ( !item->identified ) + { + if ( isTooltipForRecvItem ) + { + snprintf(buf, sizeof(buf), "%s (?)", Language::get(4161)); + } + else + { + snprintf(buf, sizeof(buf), "%s %s (?)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName()); + } + } + else + { + snprintf(buf, sizeof(buf), "%s %s (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->beatitude); + } + + auto activateSelectionPrompt = mailFrame->findField("activate prompt"); + activateSelectionPrompt->setText(""); + + int player = parentGUI.getPlayer(); + if ( isTooltipForRecvItem ) + { + if ( item->type != POTION_EMPTY ) + { + itemActionType = MAIL_ACTION_OK; + } + } + else if ( itemCategory(item) == SCROLL || item->type == READABLE_BOOK ) + { + bool isEquipped = itemIsEquipped(item, player); + if ( (!item->identified || isEquipped) && !isTooltipForRecvItem ) + { + itemActionType = MAIL_ACTION_UNIDENTIFIED; + } + else + { + itemActionType = MAIL_ACTION_OK; + } + Item* basePotion = nullptr; + Item* secondaryPotion = nullptr; + if ( isEquipped ) + { + strcat(buf, "\n"); + strcat(buf, Language::get(4165)); + } + else if ( item->identified ) + { + /*if ( parentGUI.isItemBaseIngredient(item->type) ) + { + strcat(buf, "\n"); + strcat(buf, Language::get(4156)); + } + else if ( parentGUI.isItemSecondaryIngredient(item->type) ) + { + strcat(buf, "\n"); + strcat(buf, Language::get(4157)); + } + else + { + strcat(buf, "\n"); + strcat(buf, Language::get(4158)); + }*/ + } + else + { + /*if ( !item->identified ) + { + if ( isTooltipForResultPotion ) + { + strcat(buf, "\n"); + strcat(buf, Language::get(4162)); + } + else + { + strcat(buf, "\n"); + strcat(buf, Language::get(4160)); + } + } + else + { + strcat(buf, "\n"); + strcat(buf, Language::get(4155)); + }*/ + } + } + else + { + itemActionType = MAIL_ACTION_INVALID_ITEM; + } + if ( itemDesc != buf ) + { + itemRequiresTitleReflow = true; + } + itemDesc = buf; + itemType = item->type; + if ( itemActionType == MAIL_ACTION_OK ) + { + if ( !isTooltipForRecvItem ) + { + if ( item->uid == sendItem1Uid ) + { + activateSelectionPrompt->setText(Language::get(4173)); + } + else + { + activateSelectionPrompt->setText(Language::get(4172)); + } + } + else if ( isTooltipForRecvItem ) + { + bool usingGamepad = inputs.hasController(player) && !inputs.getVirtualMouse(player)->draw_cursor; + if ( !usingGamepad ) + { + activateSelectionPrompt->setText(Language::get(6988)); + } + } + } +} + +bool GenericGUIMenu::MailboxGui_t::warpMouseToSelectedMailItem(Item* snapToItem, Uint32 flags) +{ + if ( mailGUIHasBeenCreated() ) + { + int x = getSelectedMailSlotX(); + int y = getSelectedMailSlotY(); + if ( snapToItem ) + { + x = snapToItem->x; + y = snapToItem->y; + } + + if ( auto slot = getMailSlotFrame(x, y) ) + { + int playernum = parentGUI.getPlayer(); + auto player = players[playernum]; + if ( !isInteractable ) + { + //messagePlayer(0, "[Debug]: select item queued"); + player->inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_MAILBOX; + player->inventoryUI.cursor.queuedFrameToWarpTo = slot; + return false; + } + else + { + //messagePlayer(0, "[Debug]: select item warped"); + player->inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_NONE; + player->inventoryUI.cursor.queuedFrameToWarpTo = nullptr; + slot->warpMouseToFrame(playernum, flags); + } + return true; + } + } + return false; +} + +void GenericGUIMenu::MailboxGui_t::clearItemDisplayed() +{ + itemType = -1; + itemActionType = MAIL_ACTION_NONE; +} + +bool GenericGUIMenu::isItemMailable(const Item* item) +{ + if ( !item ) + { + return false; + } + + if ( !mailboxGUI.inventoryItemAllowedInGUI(const_cast(item)) ) + { + return false; + } + + if ( itemIsEquipped(item, gui_player) ) + { + return false; // don't want to deal with client/server desync problems here. + } + + return true; } \ No newline at end of file diff --git a/src/interface/interface.hpp b/src/interface/interface.hpp index db648bc5b..9fa53837e 100644 --- a/src/interface/interface.hpp +++ b/src/interface/interface.hpp @@ -72,12 +72,16 @@ enum DamageGib { DMG_POISON, DMG_HEAL, DMG_MISS, - DMG_TODO + DMG_GUARD, + DMG_TODO, + DMG_DETECT_MONSTER }; enum DamageGibDisplayType { DMG_GIB_NUMBER, DMG_GIB_MISS, - DMG_GIB_SPRITE + DMG_GIB_SPRITE, + DMG_GIB_GUARD, + DMG_GIB_DPS_CHECK }; class EnemyHPDamageBarHandler { @@ -131,13 +135,20 @@ class EnemyHPDamageBarHandler Uint32 enemy_uid = 0; Uint32 enemy_statusEffects1 = 0; Uint32 enemy_statusEffects2 = 0; + Uint32 enemy_statusEffects3 = 0; + Uint32 enemy_statusEffects4 = 0; + Uint32 enemy_statusEffects5 = 0; Uint32 enemy_statusEffectsLowDuration1 = 0; Uint32 enemy_statusEffectsLowDuration2 = 0; + Uint32 enemy_statusEffectsLowDuration3 = 0; + Uint32 enemy_statusEffectsLowDuration4 = 0; + Uint32 enemy_statusEffectsLowDuration5 = 0; bool lowPriorityTick = false; bool shouldDisplay = true; bool hasDistanceCheck = false; bool displayOnHUD = false; bool expired = false; + bool detectMonsterCheckStatus = false; real_t depletionAnimationPercent = 100.0; EnemyHPDetails() {}; EnemyHPDetails(Uint32 uid, Sint32 HP, Sint32 maxHP, Sint32 oldHP, const char* name, bool isLowPriority) @@ -215,6 +226,7 @@ void select_tinkering_slot(int player, int currentx, int currenty, int diffx, in void select_alchemy_slot(int player, int currentx, int currenty, int diffx, int diffy); void select_feather_slot(int player, int currentx, int currenty, int diffx, int diffy); void select_assistshrine_slot(int player, int currentx, int currenty, int diffx, int diffy); +void select_mail_slot(int player, int currentx, int currenty, int diffx, int diffy); extern Entity* openedChest[MAXPLAYERS]; //One for each client. //TODO: Clientside, [0] will always point to something other than NULL when a chest is open and it will be NULL when a chest is closed. extern list_t chestInv[MAXPLAYERS]; //This is just for the client, so that it can populate the chest inventory on its end. @@ -313,7 +325,8 @@ enum GUICurrentType GUI_TYPE_TINKERING, GUI_TYPE_SCRIBING, GUI_TYPE_ITEMFX, - GUI_TYPE_ASSIST + GUI_TYPE_ASSIST, + GUI_TYPE_MAILBOX }; // Generic GUI Stuff (repair/alchemy) @@ -329,8 +342,14 @@ class GenericGUIMenu Item* basePotion; Item* secondaryPotion; Item* alembicItem; + Uint32 alembicEntityUid = 0; bool experimentingAlchemy; + bool isItemMailable(const Item* item); + Uint32 mailboxEntityUid = 0; + void mailboxClaimItem(); + bool mailboxSendItem(); + // Misc item/spell effects Item* itemEffectScrollItem; bool itemEffectUsingSpell; @@ -358,6 +377,7 @@ class GenericGUIMenu bool tinkeringBulkSalvage = false; Sint32 tinkeringBulkSalvageMetalScrap = 0; Sint32 tinkeringBulkSalvageMagicScrap = 0; + Uint32 workstationEntityUid = 0; // Scribing Item* scribingToolItem; @@ -371,6 +391,9 @@ class GenericGUIMenu }; ScribingFilter scribingFilter; + Item* transmuteItemTarget = nullptr; + int transmuteItemScroll = 0; + GenericGUIMenu() : guiActive(false), basePotion(nullptr), @@ -397,7 +420,8 @@ class GenericGUIMenu alchemyGUI(*this), featherGUI(*this), itemfxGUI(*this), - assistShrineGUI(*this) + assistShrineGUI(*this), + mailboxGUI(*this) { tinkeringTotalItems.first = nullptr; tinkeringTotalItems.last = nullptr; @@ -434,12 +458,42 @@ class GenericGUIMenu bool isItemEnchantArmorable(const Item* item); void enchantItem(Item* item); + // transmute + bool isItemAlterable(const Item* item); + void alterItem(Item* item); + int getAlterItemResultAtCycle(Item* item); + + // void + bool isItemVoidable(const Item* item); + void sendItemToVoid(Item* item); + + // adorcise + bool isItemAdorcisable(const Item* item); + void adorciseItem(Item* item); + + // scepter charge + bool isItemScepterChargeable(const Item* item); + void rechargeScepterUsingItem(Item* item); + + // misc + bool isItemDesecratable(const Item* item); + void desecrateItem(Item* item); + bool isItemBlessWaterable(const Item* item); + void blessWater(Item* item); + bool isItemSanctifiable(const Item* item); + void sanctifyItem(Item* item); + bool isItemCleaseFoodable(const Item* item); + void cleanseFood(Item* item); + //alchemy menu funcs bool isItemMixable(const Item* item); void alchemyCombinePotions(); + void alchemyCookCombination(); bool alchemyLearnRecipe(int type, bool increaseskill, bool notify = true); bool isItemBaseIngredient(int type); bool isItemSecondaryIngredient(int type); + static int isItemRationSeasoning(int type); + static bool isItemRation(int type); void alchemyLearnRecipeOnLevelUp(int skill); // tinkering menu foncs @@ -501,6 +555,7 @@ class GenericGUIMenu { if ( &item == scribingToolItem || &item == tinkeringKitItem || &item == alembicItem || &item == scribingBlankScrollTarget + || &item == transmuteItemTarget || &item == basePotion || &item == secondaryPotion || &item == itemEffectScrollItem ) { return true; @@ -513,6 +568,10 @@ class GenericGUIMenu { scribingToolItem = nullptr; } + if ( &item == transmuteItemTarget ) + { + transmuteItemTarget = nullptr; + } if ( &item == scribingBlankScrollTarget ) { scribingBlankScrollTarget = nullptr; @@ -644,6 +703,18 @@ class GenericGUIMenu bool isInteractable = true; bool bOpen = false; bool bFirstTimeSnapCursor = false; + enum CostEffectTypes + { + COST_EFFECT_NONE, + COST_EFFECT_GOLD, + COST_EFFECT_MANA, + COST_EFFECT_MANA_RETURN_GOLD, + COST_EFFECT_MANA_AND_GOLD + }; + CostEffectTypes modeHasCostEffect = COST_EFFECT_NONE; + bool modeHasTransmuteMenu(); + int costEffectGoldAmount = 0; + int costEffectMPAmount = 0; enum ItemEffectModes : int { ITEMFX_MODE_NONE, @@ -654,7 +725,25 @@ class GenericGUIMenu ITEMFX_MODE_SPELL_IDENTIFY, ITEMFX_MODE_SPELL_REMOVECURSE, ITEMFX_MODE_SCROLL_ENCHANT_WEAPON, - ITEMFX_MODE_SCROLL_ENCHANT_ARMOR + ITEMFX_MODE_SCROLL_ENCHANT_ARMOR, + ITEMFX_MODE_ALTER_INSTRUMENT, + ITEMFX_MODE_METALLURGY, + ITEMFX_MODE_GEOMANCY, + ITEMFX_MODE_FORGE_KEY, + ITEMFX_MODE_FORGE_JEWEL, + ITEMFX_MODE_ENHANCE_WEAPON, + ITEMFX_MODE_RESHAPE_WEAPON, + ITEMFX_MODE_ALTER_ARROW, + ITEMFX_MODE_PUNCTURE_VOID, + ITEMFX_MODE_ADORCISE_WEAPON, + ITEMFX_MODE_RESTORE, + ITEMFX_MODE_VANDALISE, + ITEMFX_MODE_DESECRATE, + ITEMFX_MODE_SANCTIFY, + ITEMFX_MODE_SANCTIFY_WATER, + ITEMFX_MODE_CLEANSE_FOOD, + ITEMFX_MODE_ADORCISE_INSTRUMENT, + ITEMFX_MODE_SCEPTER_CHARGE }; void openItemEffectMenu(ItemEffectModes mode); ItemEffectModes currentMode = ITEMFX_MODE_NONE; @@ -667,6 +756,7 @@ class GenericGUIMenu std::string itemDesc = ""; int itemType = -1; int itemRequirement = -1; + int confirmActionSteps = 0; enum ItemEffectActions_t : int { ITEMFX_ACTION_NONE, @@ -677,8 +767,15 @@ class GenericGUIMenu ITEMFX_ACTION_ITEM_IDENTIFIED, ITEMFX_ACTION_MUST_BE_UNEQUIPPED, ITEMFX_ACTION_NOT_IDENTIFIED_YET, - ITEMFX_ACTION_NOT_CURSED + ITEMFX_ACTION_CANT_AFFORD_GOLD, + ITEMFX_ACTION_CANT_AFFORD_MANA, + ITEMFX_ACTION_NOT_CURSED, + ITEMFX_ACTION_UNVOIDABLE, + ITEMFX_ACTION_AT_MAX_BLESSING, + ITEMFX_ACTION_NEED_SKILL_LVLS, + ITEMFX_ACTION_CANT_AFFORD_MANA_AND_GOLD }; + std::pair confirmActionOnItemSteps; ItemEffectActions_t itemActionType = ITEMFX_ACTION_NONE; bool itemRequiresTitleReflow = true; real_t animTooltip = 0.0; @@ -699,7 +796,8 @@ class GenericGUIMenu ItemEffectActions_t setItemDisplayNameAndPrice(Item* item, bool checkResultOnly = false); void clearItemDisplayed(); - + bool consumeResourcesForTransmute(); + void getItemEffectCost(Item* itemUsedWith, int& goldCost, int& manaCost); static int heightOffsetWhenNotCompact; }; ItemEffectGUI_t itemfxGUI; @@ -874,6 +972,93 @@ class GenericGUIMenu }; AssistShrineGUI_t assistShrineGUI; + struct MailboxGui_t + { + GenericGUIMenu& parentGUI; + + static const int MAIL_SLOT_SEND = -1; + static const int MAIL_SLOT_RECV = -2; + + Item mailReceiveItem; + MailboxGui_t(GenericGUIMenu& g) : + parentGUI(g) + { + mailReceiveItem.appearance = 0; + mailReceiveItem.type = POTION_EMPTY; + mailReceiveItem.node = nullptr; + mailReceiveItem.status = SERVICABLE; + mailReceiveItem.beatitude = 0; + mailReceiveItem.count = 1; + mailReceiveItem.appearance = 0; + mailReceiveItem.identified = false; + mailReceiveItem.uid = 0; + mailReceiveItem.isDroppable = false; + mailReceiveItem.x = MAIL_SLOT_RECV; + mailReceiveItem.y = 0; + } + + enum MailActions_t : int + { + MAIL_ACTION_NONE, + MAIL_ACTION_OK, + MAIL_ACTION_INVALID_ITEM, + MAIL_ACTION_UNIDENTIFIED + }; + MailActions_t itemActionType = MAIL_ACTION_NONE; + /*enum AlchemyView_t : int + { + ALCHEMY_VIEW_BREW, + ALCHEMY_VIEW_RECIPES, + ALCHEMY_VIEW_COOK, + ALCHEMY_VIEW_RECIPES_COOK + };*/ + //AlchemyView_t currentView = ALCHEMY_VIEW_BREW; + Frame* mailFrame = nullptr; + real_t animx = 0.0; + real_t animTooltip = 0.0; + Uint32 animTooltipTicks = 0; + real_t animSendItem1 = 0.0; + int animSendItem1StartX = 0; + int animSendItem1StartY = 0; + int animSendItem1DestX = 0; + int animSendItem1DestY = 0; + Uint32 sendItem1Uid = 0; + real_t animRecvItem = 0.0; + int animRecvItemStartX = 0; + int animRecvItemStartY = 0; + int animRecvItemDestX = 0; + int animRecvItemDestY = 0; + Uint32 recvItemUid = 0; + //int animRecvItemCount = 1; + bool isInteractable = true; + bool bOpen = false; + bool bFirstTimeSnapCursor = false; + void openMailMenu(/*AlchemyView_t view*/); + //void changeCurrentView(AlchemyView_t view); + void closeMailMenu(); + void updateMailMenu(); + void createMailMenu(); + bool mailGUIHasBeenCreated() const; + std::string itemDesc = ""; + int itemType = -1; + bool itemRequiresTitleReflow = true; + int selectedMailSlotX = -1; + int selectedMailSlotY = -1; + std::unordered_map mailSlotFrames; + void selectMailSlot(const int x, const int y); + const int getSelectedMailSlotX() const { return selectedMailSlotX; } + const int getSelectedMailSlotY() const { return selectedMailSlotY; } + Frame* getMailSlotFrame(int x, int y) const; + void setItemDisplayNameAndPrice(Item* item, const bool isTooltipForRecvItem); + //void setItemDisplayNameAndPriceBrew(Item* item, const bool isTooltipForResultPotion, const bool isTooltipForRecipe); + //void setItemDisplayNameAndPriceCook(Item* item, const bool isTooltipForResultPotion, const bool isTooltipForRecipe); + bool inventoryItemAllowedInGUI(Item* item); + bool warpMouseToSelectedMailItem(Item* snapToItem, Uint32 flags); + void clearItemDisplayed(); + static int heightOffsetWhenNotCompact; + }; + MailboxGui_t mailboxGUI; + struct FeatherGUI_t { GenericGUIMenu& parentGUI; @@ -1096,6 +1281,7 @@ class GenericGUIMenu Item alchemyResultPotion; Item emptyBottleCount; + Item torchCount; AlchemyGUI_t(GenericGUIMenu& g) : parentGUI(g), recipes(*this) @@ -1125,6 +1311,19 @@ class GenericGUIMenu emptyBottleCount.isDroppable = false; emptyBottleCount.x = 0; emptyBottleCount.y = 0; + + torchCount.appearance = 0; + torchCount.type = TOOL_TORCH; + torchCount.node = nullptr; + torchCount.status = SERVICABLE; + torchCount.beatitude = 0; + torchCount.count = 0; + torchCount.appearance = 0; + torchCount.identified = true; + torchCount.uid = 0; + torchCount.isDroppable = false; + torchCount.x = 0; + torchCount.y = 0; } enum AlchemyActions_t : int { @@ -1137,7 +1336,9 @@ class GenericGUIMenu enum AlchemyView_t : int { ALCHEMY_VIEW_BREW, - ALCHEMY_VIEW_RECIPES + ALCHEMY_VIEW_RECIPES, + ALCHEMY_VIEW_COOK, + ALCHEMY_VIEW_RECIPES_COOK }; struct AlchNotification_t { @@ -1155,6 +1356,7 @@ class GenericGUIMenu }; std::vector> notifications; AlchemyView_t currentView = ALCHEMY_VIEW_BREW; + bool hasTinOpener = false; Frame* alchFrame = nullptr; real_t animx = 0.0; real_t animTooltip = 0.0; @@ -1186,7 +1388,8 @@ class GenericGUIMenu bool isInteractable = true; bool bOpen = false; bool bFirstTimeSnapCursor = false; - void openAlchemyMenu(); + void openAlchemyMenu(AlchemyView_t view); + void changeCurrentView(AlchemyView_t view); void closeAlchemyMenu(); void updateAlchemyMenu(); void createAlchemyMenu(); @@ -1208,9 +1411,13 @@ class GenericGUIMenu const int getSelectedAlchemySlotX() const { return selectedAlchemySlotX; } const int getSelectedAlchemySlotY() const { return selectedAlchemySlotY; } Frame* getAlchemySlotFrame(int x, int y) const; - void setItemDisplayNameAndPrice(Item* item, bool isTooltipForResultPotion, bool isTooltipForRecipe); + void setItemDisplayNameAndPrice(Item* item, const bool isTooltipForResultPotion, const bool isTooltipForRecipe); + void setItemDisplayNameAndPriceBrew(Item* item, const bool isTooltipForResultPotion, const bool isTooltipForRecipe); + void setItemDisplayNameAndPriceCook(Item* item, const bool isTooltipForResultPotion, const bool isTooltipForRecipe); + bool inventoryItemAllowedInGUI(Item* item); bool warpMouseToSelectedAlchemyItem(Item* snapToItem, Uint32 flags); void clearItemDisplayed(); + bool alchemyMissingIngredientQty(Item* item); static int heightOffsetWhenNotCompact; }; AlchemyGUI_t alchemyGUI; @@ -1336,7 +1543,9 @@ struct AttackHoverText_t ATK_HOVER_TYPE_MAGICSTAFF, ATK_HOVER_TYPE_TOOL, ATK_HOVER_TYPE_PICKAXE, - ATK_HOVER_TYPE_TOOL_TRAP + ATK_HOVER_TYPE_TOOL_TRAP, + ATK_HOVER_TYPE_THROWN_MISC, + ATK_HOVER_TYPE_RAPIER }; HoverTypes hoverType = ATK_HOVER_TYPE_DEFAULT; Sint32 totalAttack = 0; @@ -1676,7 +1885,12 @@ struct CalloutRadialMenu CALLOUT_TYPE_COLLIDER_BREAKABLE, CALLOUT_TYPE_BELL, CALLOUT_TYPE_DAEDALUS, - CALLOUT_TYPE_ASSIST_SHRINE + CALLOUT_TYPE_ASSIST_SHRINE, + CALLOUT_TYPE_WALL_LOCK, + CALLOUT_TYPE_WALL_LOCK_ON, + CALLOUT_TYPE_WALL_LOCK_OFF, + CALLOUT_TYPE_WALL_BUTTON_ON, + CALLOUT_TYPE_WALL_BUTTON_OFF /*,CALLOUT_TYPE_PEDESTAL*/ }; enum CalloutHelpFlags : int @@ -1793,6 +2007,7 @@ enum ItemContextMenuPrompts { PROMPT_UNEQUIP, PROMPT_SPELL_EQUIP, PROMPT_SPELL_QUICKCAST, + PROMPT_SPELL_CHANGE_FOCUS, PROMPT_APPRAISE, PROMPT_DROPDOWN, PROMPT_INTERACT, @@ -1812,7 +2027,9 @@ enum ItemContextMenuPrompts { PROMPT_TINKER, PROMPT_GRAB, PROMPT_UNEQUIP_FOR_DROP, - PROMPT_CLEAR_HOTBAR_SLOT + PROMPT_CLEAR_HOTBAR_SLOT, + PROMPT_COOK, + PROMPT_SCEPTER_CHARGE }; std::vector getContextMenuOptionsForItem(const int player, Item* item); diff --git a/src/interface/playerinventory.cpp b/src/interface/playerinventory.cpp index 6efbf0e31..4350039e3 100644 --- a/src/interface/playerinventory.cpp +++ b/src/interface/playerinventory.cpp @@ -180,6 +180,10 @@ const char* itemEquipString(int player, const Item& item) case STEEL_SHIELD_RESISTANCE: case CRYSTAL_SHIELD: case MIRROR_SHIELD: + case SCUTUM: + case BONE_SHIELD: + case BLACKIRON_SHIELD: + case SILVER_SHIELD: if ( itemIsEquipped(&item, player) ) { return Language::get(325); @@ -370,7 +374,7 @@ const char* itemUseString(int player, const Item& item) { return Language::get(330); } - else if ( itemCategory(&item) == SPELLBOOK ) + else if ( itemCategory(&item) == SPELLBOOK || itemCategory(&item) == TOME_SPELL ) { return Language::get(330); } @@ -887,6 +891,7 @@ bool moveInPaperDoll(int player, Player::PaperDoll_t::PaperDollSlotType paperDol { if ( GenericGUI[player].tinkerGUI.bOpen || GenericGUI[player].alchemyGUI.bOpen || GenericGUI[player].featherGUI.bOpen + || GenericGUI[player].mailboxGUI.bOpen || GenericGUI[player].assistShrineGUI.bOpen ) { return false; @@ -1462,6 +1467,62 @@ void select_assistshrine_slot(int player, int currentx, int currenty, int diffx, } } +void select_mail_slot(int player, int currentx, int currenty, int diffx, int diffy) +{ + int x = currentx + diffx; + int y = currenty + diffy; + + auto& mailboxGUI = GenericGUI[player].mailboxGUI; + int lowestItemY = 0; + + if ( currentx < 0 ) + { + if ( diffy != 0 ) + { + // main mail frame + if ( currentx == mailboxGUI.MAIL_SLOT_SEND ) + { + x = mailboxGUI.MAIL_SLOT_RECV; + } + else if ( currentx == mailboxGUI.MAIL_SLOT_RECV ) + { + x = mailboxGUI.MAIL_SLOT_SEND; + } + y = 0; + } + else if ( diffx != 0 ) + { + if ( diffx > 0 ) + { + if ( currentx == mailboxGUI.MAIL_SLOT_SEND ) + { + players[player]->inventoryUI.selectSlot(0, 0); + } + else + { + players[player]->inventoryUI.selectSlot(0, 5); + } + players[player]->GUI.activateModule(Player::GUI_t::MODULE_INVENTORY); + return; + } + else + { + if ( currentx == mailboxGUI.MAIL_SLOT_SEND ) + { + players[player]->inventoryUI.selectSlot(players[player]->inventoryUI.getSizeX() - 1, 0); + } + else + { + players[player]->inventoryUI.selectSlot(players[player]->inventoryUI.getSizeX() - 1, 5); + } + players[player]->GUI.activateModule(Player::GUI_t::MODULE_INVENTORY); + return; + } + } + } + mailboxGUI.selectMailSlot(x, y); +} + // only called by handleInventoryMovement in player.cpp void select_alchemy_slot(int player, int currentx, int currenty, int diffx, int diffy) { @@ -1862,6 +1923,7 @@ void select_inventory_slot(int player, int currentx, int currenty, int diffx, in bool skipPaperDollSelection = false; if ( GenericGUI[player].tinkerGUI.bOpen || GenericGUI[player].alchemyGUI.bOpen + || GenericGUI[player].mailboxGUI.bOpen || GenericGUI[player].assistShrineGUI.bOpen || GenericGUI[player].featherGUI.bOpen ) { @@ -2024,6 +2086,19 @@ void select_inventory_slot(int player, int currentx, int currenty, int diffx, in return; } } + else if ( !selectedItem && GenericGUI[player].mailboxGUI.bOpen ) + { + if ( y >= 4 ) + { + select_mail_slot(player, GenericGUI[player].mailboxGUI.MAIL_SLOT_RECV, 0, 0, 0); + } + else + { + select_mail_slot(player, GenericGUI[player].mailboxGUI.MAIL_SLOT_SEND, 0, 0, 0); + } + players[player]->GUI.activateModule(Player::GUI_t::MODULE_MAILBOX); + return; + } else if ( !selectedItem && players[player]->gui_mode == GUI_MODE_SHOP && players[player]->shopGUI.bOpen ) { if ( y >= players[player]->shopGUI.MAX_SHOP_Y ) @@ -2090,6 +2165,19 @@ void select_inventory_slot(int player, int currentx, int currenty, int diffx, in return; } } + else if ( !selectedItem && GenericGUI[player].mailboxGUI.bOpen ) + { + if ( y >= 4 ) + { + select_mail_slot(player, GenericGUI[player].mailboxGUI.MAIL_SLOT_RECV, 0, 0, 0); + } + else + { + select_mail_slot(player, GenericGUI[player].mailboxGUI.MAIL_SLOT_SEND, 0, 0, 0); + } + players[player]->GUI.activateModule(Player::GUI_t::MODULE_MAILBOX); + return; + } else if ( !selectedItem && GenericGUI[player].assistShrineGUI.bOpen ) { if ( GenericGUI[player].assistShrineGUI.currentView == GenericGUI[player].assistShrineGUI.ASSIST_SHRINE_VIEW_RACE ) @@ -2234,6 +2322,29 @@ std::string getItemSpritePath(const int player, Item& item) imagePathsNode = list_Node(&items[item.type].images, item.getLootBagPlayer()); } } + else if ( item.type == MAGICSTAFF_SCEPTER ) + { + if ( item.appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX == 0 ) + { + imagePathsNode = list_Node(&items[item.type].images, 1); + } + else + { + imagePathsNode = list_Node(&items[item.type].images, 0); + } + } + else if ( itemCategory(&item) == SPELLBOOK || itemCategory(&item) == TOME_SPELL ) + { + int variation = getItemVariationFromSpellbookOrTome(item); + if ( variation >= 0 && variation < items[item.type].variations ) + { + imagePathsNode = list_Node(&items[item.type].images, variation); + } + else + { + imagePathsNode = list_Node(&items[item.type].images, item.appearance % items[item.type].variations); + } + } else { imagePathsNode = list_Node(&items[item.type].images, item.appearance % items[item.type].variations); @@ -2258,9 +2369,9 @@ Item* takeItemFromChest(int player, Item* item, int amount, Item* addToSpecificI { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( !chest_inventory || !item->node || item->node->list != chest_inventory ) @@ -2304,9 +2415,9 @@ bool dragDropStackChestItems(const int player, Item*& selectedItem, Item* tempIt { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( !chest_inventory || !selectedItem->node || selectedItem->node->list != chest_inventory @@ -2468,7 +2579,7 @@ bool dragDropStackInventoryItems(const int player, Item*& selectedItem, Item* te bool releaseInventoryItemIntoAlchemy(const int player) { return false; // disable for now - Item*& selectedItem = inputs.getUIInteraction(player)->selectedItem; + /*Item*& selectedItem = inputs.getUIInteraction(player)->selectedItem; if ( !selectedItem ) { return false; @@ -2519,7 +2630,7 @@ bool releaseInventoryItemIntoAlchemy(const int player) return true; } } - return false; + return false;*/ } void releaseChestItem(const int player) @@ -2820,9 +2931,9 @@ void releaseChestItem(const int player) { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( chest_inventory ) @@ -3285,8 +3396,6 @@ void releaseItem(const int player) bool mouseOverSlot = getSlotFrameXYFromMousePos(player, slotFrameX, slotFrameY, itemCategory(selectedItem) == SPELL_CAT); bool mouseInInventory = mouseInsidePlayerInventory(player); bool mouseInChest = false; - bool mouseInAlchemyBasePotion = false; - bool mouseInAlchemySecondaryPotion = false; bool allowDropItems = players[player]->inventoryUI.guiAllowDropItems(selectedItem); if ( !mouseInInventory ) { @@ -3327,6 +3436,20 @@ void releaseItem(const int player) } } } + else if ( GenericGUI[player].mailboxGUI.bOpen ) + { + if ( GenericGUI[player].mailboxGUI.mailFrame + && !GenericGUI[player].mailboxGUI.mailFrame->isDisabled() ) + { + if ( auto baseFrame = GenericGUI[player].mailboxGUI.mailFrame->findFrame("mail base") ) + { + if ( !baseFrame->isDisabled() && baseFrame->capturesMouse() ) + { + allowDropItems = false; + } + } + } + } } if ( mouseOverSlot && mouseInInventory && slotFrameY > Player::Inventory_t::DOLL_ROW_5 ) @@ -3606,9 +3729,9 @@ void releaseItem(const int player) { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( chest_inventory ) @@ -4175,6 +4298,7 @@ void Player::Inventory_t::cycleInventoryTab() { player.inventoryUI.selectSpell(selectedItem->x, selectedItem->y); } + player.inventoryUI.spellPanel.currentScrollRow = player.inventoryUI.spellPanel.scrollSetpoint / player.inventoryUI.getSlotSize(); player.inventoryUI.spellPanel.scrollToSlot(player.inventoryUI.getSelectedSpellX(), player.inventoryUI.getSelectedSpellY(), true); } @@ -4504,34 +4628,118 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int if ( keystatus[SDLK_KP_5] ) { keystatus[SDLK_KP_5] = 0; - if ( item->type == WOODEN_SHIELD ) - { - item->type = static_cast(NUMITEMS - 1); - } - else - { - item->type = static_cast(item->type - 1); - if ( item->type == SPELL_ITEM ) - { - item->type = static_cast(item->type - 1); - } - } + if ( itemIsEquipped(item, player) ) + { + ItemType prevType = item->type; + if ( item->type == WOODEN_SHIELD ) + { + item->type = static_cast(NUMITEMS - 1); + } + else + { + item->type = static_cast(item->type - 1); + if ( item->type == SPELL_ITEM ) + { + item->type = static_cast(item->type - 1); + } + } + bool found = false; + if ( items[prevType].item_slot == items[item->type].item_slot ) + { + found = true; + } + while ( !found && item->type != prevType ) + { + if ( item->type == WOODEN_SHIELD ) + { + item->type = static_cast(NUMITEMS); + } + item->type = static_cast(item->type - 1); + if ( item->type == WOODEN_SHIELD ) + { + item->type = static_cast(NUMITEMS - 1); + } + if ( item->type == SPELL_ITEM ) + { + item->type = static_cast(item->type - 1); + } + if ( items[prevType].item_slot == items[item->type].item_slot ) + { + break; + } + } + } + else + { + if ( item->type == WOODEN_SHIELD ) + { + item->type = static_cast(NUMITEMS - 1); + } + else + { + item->type = static_cast(item->type - 1); + if ( item->type == SPELL_ITEM ) + { + item->type = static_cast(item->type - 1); + } + } + } } if ( keystatus[SDLK_KP_8] ) { keystatus[SDLK_KP_8] = 0; - if ( item->type == NUMITEMS - 1 ) - { - item->type = WOODEN_SHIELD; - } - else - { - item->type = static_cast(item->type + 1); - if ( item->type == SPELL_ITEM ) - { - item->type = static_cast(item->type + 1); - } - } + if ( itemIsEquipped(item, player) ) + { + ItemType prevType = item->type; + if ( item->type == NUMITEMS - 1 ) + { + item->type = WOODEN_SHIELD; + } + else + { + item->type = static_cast(item->type + 1); + if ( item->type == SPELL_ITEM ) + { + item->type = static_cast(item->type + 1); + } + } + bool found = false; + if ( items[prevType].item_slot == items[item->type].item_slot ) + { + found = true; + } + while ( !found && item->type != prevType ) + { + item->type = static_cast(item->type + 1); + if ( item->type == NUMITEMS - 1 ) + { + item->type = WOODEN_SHIELD; + } + else if ( item->type == SPELL_ITEM ) + { + item->type = static_cast(item->type + 1); + } + if ( items[prevType].item_slot == items[item->type].item_slot ) + { + break; + } + } + } + else + { + if ( item->type == NUMITEMS - 1 ) + { + item->type = WOODEN_SHIELD; + } + else + { + item->type = static_cast(item->type + 1); + if ( item->type == SPELL_ITEM ) + { + item->type = static_cast(item->type + 1); + } + } + } } } @@ -4690,6 +4898,11 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY, item->beatitude); } + else if ( item->type == MAGICSTAFF_SCEPTER ) + { + snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), + item->getName(), item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX, item->beatitude); + } else if ( itemCategory(item) == BOOK ) { snprintf(buf, sizeof(buf), "%s %s\n%s (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), @@ -5060,8 +5273,10 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int if ( itemTooltip.icons.size() > 0 ) { int index = 0; + int iconSpellIndex = -1; for ( auto& icon : itemTooltip.icons ) { + ItemTooltips_t::ItemTooltipIcons_t* overrideIconType = nullptr; if ( icon.conditionalAttribute.compare("") != 0 ) { if ( itemCategory(item) == MAGICSTAFF ) @@ -5122,7 +5337,47 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int spell_t* spell = getSpellFromItem(player, item, false); if ( spell && ItemTooltips.spellItems[spell->ID].internalName == icon.conditionalAttribute ) { + ++iconSpellIndex; // current spell uses this attribute + + if ( index == 2 ) + { + if ( spell->ID == SPELL_RAT_FORM || spell->ID == SPELL_SPIDER_FORM + || spell->ID == SPELL_TROLL_FORM || spell->ID == SPELL_IMP_FORM ) + { + if ( client_classes[player] != CLASS_SHAMAN ) + { + continue; // ignore mentions of spells having access to + } + } + } + if ( spell->ID == SPELL_TURN_UNDEAD ) + { + if ( index >= 1 ) + { + int effectStrength = std::min(3, std::max(1, getSpellDamageSecondaryFromID(SPELL_TURN_UNDEAD, players[player]->entity, stats[player], players[player]->entity))); + if ( compendiumTooltip && intro ) + { + effectStrength = 1; + } + if ( (effectStrength - 1) != iconSpellIndex ) + { + continue; + } + } + } + else if ( spell->ID == SPELL_DASH ) + { + int effectStrength = std::min(2, std::max(1, getSpellDamageFromID(SPELL_DASH, players[player]->entity, stats[player], players[player]->entity))); + if ( compendiumTooltip && intro ) + { + effectStrength = 1; + } + if ( (effectStrength - 1) != iconSpellIndex ) + { + continue; + } + } } else { @@ -5149,11 +5404,23 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int } } } - else if ( itemCategory(item) == SPELLBOOK + else if ( (itemCategory(item) == SPELLBOOK || itemCategory(item) == TOME_SPELL) && icon.conditionalAttribute.find("SPELLBOOK_") != std::string::npos ) { - spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item->type)); - int skillLVL = std::min(100, stats[player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[player], players[player]->entity)); + spell_t* spell = nullptr; + if ( itemCategory(item) == SPELLBOOK ) + { + spell = getSpellFromID(getSpellIDFromSpellbook(item->type)); + } + else if ( itemCategory(item) == TOME_SPELL ) + { + spell = getSpellFromID(item->getTomeSpellID()); + } + if ( !spell ) + { + continue; + } + int skillLVL = std::min(100, stats[player]->getModifiedProficiency(spell->skillID) + statGetINT(stats[player], players[player]->entity)); bool isGoblin = (stats[player] && (stats[player]->type == GOBLIN || (stats[player]->playerRace == RACE_GOBLIN && stats[player]->stat_appearance == 0))); @@ -5163,7 +5430,7 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int { continue; } - if ( spell->ID == SPELL_CUREAILMENT && getSpellbookBonusPercent(players[player]->entity, stats[player], item) < 25 ) + if ( spell->ID == SPELL_CUREAILMENT && getSpellbookBonusPercent(players[player]->entity, stats[player], item) < 10 ) { continue; } @@ -5194,8 +5461,17 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int continue; } } + else if ( icon.conditionalAttribute == "SPELLBOOK_ALREADY_LEARNED" ) + { + if ( compendiumTooltip && intro ) { continue; } + if ( !playerLearnedSpellbook(player, item) ) + { + continue; + } + } else if ( icon.conditionalAttribute == "SPELLBOOK_SPELLINFO_UNLEARNED" - || icon.conditionalAttribute == "SPELLBOOK_SPELLINFO_LEARNED" ) + || icon.conditionalAttribute == "SPELLBOOK_SPELLINFO_LEARNED" + || icon.conditionalAttribute == "SPELLBOOK_SPELLINFO_TOME" ) { if ( icon.conditionalAttribute == "SPELLBOOK_SPELLINFO_LEARNED" ) { @@ -5203,6 +5479,25 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int { continue; } + + if ( index == 1 ) + { + bool found = false; + auto& spellTooltip = ItemTooltips.tooltips["tooltip_spell_item"]; + for ( auto& icon : spellTooltip.icons ) + { + if ( icon.conditionalAttribute == spell->spell_internal_name ) + { + found = true; + overrideIconType = &icon; + break; + } + } + if ( !found ) + { + continue; + } + } } else if ( icon.conditionalAttribute == "SPELLBOOK_SPELLINFO_UNLEARNED" ) { @@ -5276,9 +5571,9 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int else if ( index == 1 ) { imgSecondaryIcon->disabled = false; - imgSecondaryIcon->path = icon.iconPath; + imgSecondaryIcon->path = overrideIconType ? overrideIconType->iconPath : icon.iconPath; - std::string iconText = icon.text; + std::string iconText = overrideIconType ? overrideIconType->text : icon.text; ItemTooltips.formatItemIcon(player, tooltipType, *item, iconText, index, icon.conditionalAttribute, parentFrame); std::string bracketText = ""; @@ -5457,7 +5752,7 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int } else if ( itemCategory(item) == WEAPON || itemCategory(item) == ARMOR || itemCategory(item) == TOOL || itemCategory(item) == AMULET || itemCategory(item) == RING || itemTypeIsQuiver(item->type) - || itemCategory(item) == GEM ) + || itemCategory(item) == GEM || itemCategory(item) == THROWN ) { if ( tag.compare("weapon_durability") == 0 ) { @@ -5602,6 +5897,11 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int continue; } } + else if ( (tag == "thrown_atk_from_player_stat" || tag == "thrown_skill_modifier") + && (itemTypeIsThrownBall(item->type) || item->type == BOLAS) ) + { + continue; + } } else if ( itemCategory(item) == SPELLBOOK ) { @@ -5643,25 +5943,44 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int else if ( itemCategory(item) == SPELL_CAT ) { spell_t* spell = getSpellFromItem(player, item, false); + if ( !spell ) { continue; } if ( tag.compare("spell_damage_bonus") == 0 ) { if ( !ItemTooltips.bIsSpellDamageOrHealingType(spell) ) { - continue; + if ( ItemTooltips.spellItems[spell->ID].spellTags.find(ItemTooltips_t::SPELL_TAG_BONUS_AS_EFFECT_POWER) + == ItemTooltips.spellItems[spell->ID].spellTags.end() ) + { + continue; + } } } else if ( tag.compare("spell_cast_success") == 0 + || tag.compare("spell_cast_success1") == 0 + || tag.compare("spell_cast_success2") == 0 || tag.compare("spell_extramana_chance") == 0 ) { if ( compendiumTooltip && intro ) { continue; } - bool newbie = isSpellcasterBeginner(player, players[player]->entity); - if ( !newbie ) - { - continue; - } + if ( tag.compare("spell_cast_success2") == 0 ) + { + bool newbie = std::min(std::max(0, + stats[player]->getModifiedProficiency(spell->skillID) + statGetINT(stats[player], players[player]->entity)), 100) < SKILL_LEVEL_BASIC; + if ( !newbie ) + { + continue; + } + } + else + { + bool newbie = isSpellcasterBeginner(player, players[player]->entity, spell->skillID); + if ( !newbie ) + { + continue; + } + } } else if ( tag.compare("spell_newbie_newline") == 0 ) { @@ -5669,7 +5988,7 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int { continue; } - bool newbie = isSpellcasterBeginner(player, players[player]->entity); + bool newbie = isSpellcasterBeginner(player, players[player]->entity, spell->skillID); if ( !newbie ) { continue; @@ -5679,29 +5998,82 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int continue; // don't insert this newline } } - else if ( tag.find("attribute_spell_") != std::string::npos ) - { - spell_t* spell = getSpellFromItem(player, item, false); - if ( !spell ) { continue; } - std::string subs = tag.substr(10, std::string::npos); - if ( !spell || ItemTooltips.spellItems[spell->ID].internalName != subs ) - { - continue; - } - } - } - else if ( itemCategory(item) == MAGICSTAFF ) - { - if ( tag.compare("magicstaff_degrade_chance") == 0 ) - { - if ( item->type == MAGICSTAFF_CHARM ) - { - continue; - } - } - else if ( tag.compare("magicstaff_charm_degrade_chance") == 0 ) - { - if ( item->type != MAGICSTAFF_CHARM ) + else if ( tag == "spell_cast_time_newline" ) + { + if ( detailsTextString.compare("") == 0 ) + { + continue; // don't insert this newline + } + } + else if ( tag == "spell_distance_newline" ) + { + if ( spell->rangefinder == SpellRangefinderType::RANGEFINDER_NONE ) + { + continue; + } + if ( detailsTextString.compare("") == 0 ) + { + continue; // don't insert this newline + } + } + else if ( tag == "spell_distance" ) + { + if ( spell->rangefinder == SpellRangefinderType::RANGEFINDER_NONE ) + { + continue; + } + } + else if ( tag.compare("spell_nogain_newline") == 0 ) + { + if ( compendiumTooltip && intro ) + { + continue; + } + + if ( item->spellNotifyIcon ) + { + continue; + } + else if ( detailsTextString.compare("") == 0 ) + { + continue; // don't insert this newline + } + } + else if ( tag.compare("spell_nogain_levels") == 0 ) + { + if ( compendiumTooltip && intro ) + { + continue; + } + + if ( item->spellNotifyIcon ) + { + continue; + } + } + else if ( tag.find("attribute_spell_") != std::string::npos ) + { + spell_t* spell = getSpellFromItem(player, item, false); + if ( !spell ) { continue; } + std::string subs = tag.substr(10, std::string::npos); + if ( !spell || ItemTooltips.spellItems[spell->ID].internalName != subs ) + { + continue; + } + } + } + else if ( itemCategory(item) == MAGICSTAFF ) + { + if ( tag.compare("magicstaff_degrade_chance") == 0 ) + { + if ( item->type == MAGICSTAFF_CHARM ) + { + continue; + } + } + else if ( tag.compare("magicstaff_charm_degrade_chance") == 0 ) + { + if ( item->type != MAGICSTAFF_CHARM ) { continue; } @@ -6072,7 +6444,11 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int auto txtGoldValue = frameValues->findField("inventory mouse tooltip gold value"); auto txtWeightValue = frameValues->findField("inventory mouse tooltip weight value"); auto txtIdentifiedValue = frameValues->findField("inventory mouse tooltip identified value"); - + auto txtSpellSkillValue = frameValues->findField("inventory mouse tooltip spell skill value"); + auto spellSkillImg = frameValues->findImage("inventory mouse tooltip spell skill image"); + spellSkillImg->disabled = true; + txtSpellSkillValue->setDisabled(true); + SDL_Rect frameValuesPos = frameDescPos; if ( !frameValues->isDisabled() ) { @@ -6093,6 +6469,38 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int txtGoldValue->setText(spellCost.substr(0, spellCost.find('\n')).c_str()); txtWeightValue->setDisabled(false); } + + if ( auto spell = getSpellFromItem(player, item, false) ) + { + for ( auto& skill : Player::SkillSheet_t::skillSheetData.skillEntries ) + { + if ( skill.skillId == spell->skillID ) + { + std::string spellSkillValue = skill.getSkillName(); + char spellTierBuf[128]; + snprintf(spellTierBuf, sizeof(spellTierBuf), ItemTooltips.adjectives["spell_prefixes"]["tier"].c_str(), spell->getSpellTierName()); + spellSkillValue += spellTierBuf; + + txtSpellSkillValue->setText(spellSkillValue.c_str()); + txtSpellSkillValue->setDisabled(false); + spellSkillImg->path = skill.skillIconPath; + /*auto find = spellSkillImg->path.find('#'); + if ( find != std::string::npos ) + { + spellSkillImg->path.erase(find, 1); + } + find = spellSkillImg->path.find('*'); + if ( find != std::string::npos ) + { + spellSkillImg->path.erase(find, 1); + }*/ + spellSkillImg->pos.h = 24; + spellSkillImg->pos.w = 24; + spellSkillImg->disabled = false; + break; + } + } + } } else { @@ -6102,21 +6510,37 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int } else { - if ( !item->identified && itemCategory(item) == GEM ) + if ( !item->identified && stats[player]->getModifiedProficiency(PRO_APPRAISAL) < SKILL_LEVEL_LEGENDARY ) { - snprintf(valueBuf, sizeof(valueBuf), "%d", items[GEM_GLASS].value); + if ( itemCategory(item) == GEM && item->type != GEM_ROCK ) + { + snprintf(valueBuf, sizeof(valueBuf), "%s", "???"); + } + else + { + if ( item->getGoldValue() < 100 ) + { + snprintf(valueBuf, sizeof(valueBuf), "%s", "?"); + } + else if ( item->getGoldValue() < 1000 ) + { + snprintf(valueBuf, sizeof(valueBuf), "%s", "??"); + } + else + { + snprintf(valueBuf, sizeof(valueBuf), "%s", "???"); + } + } } else { - snprintf(valueBuf, sizeof(valueBuf), "%d", items[item->type].value); + snprintf(valueBuf, sizeof(valueBuf), "%d", item->getGoldValue()); } } txtGoldValue->setText(valueBuf); } txtGoldValue->setDisabled(false); - int charWidth = 8; - int charHeight = 13; const int lowerIconImgToTextOffset = 0; const std::string goldImagePath = "images/ui/Inventory/tooltips/HUD_Tooltip_Icon_Money_00.png"; @@ -6124,8 +6548,6 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int const std::string spellMPCostImagePath = "images/ui/Inventory/tooltips/HUD_Tooltip_Icon_ManaRegen_00.png"; const std::string assistImagePath = "images/ui/Inventory/tooltips/HUD_Tooltip_Icon_Assistance_00.png"; - Font::get(txtGoldValue->getFont())->sizeText("_", &charWidth, &charHeight); - Font::get(txtWeightValue->getFont())->sizeText("_", &charWidth, &charHeight); if ( tooltipType.find("tooltip_spell_") != std::string::npos ) { auto imgMPCost = imgGoldIcon; @@ -6141,7 +6563,8 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int txtMPCost->setSize(SDL_Rect{ imgMPCost->pos.x + imgMPCost->pos.w + padx, imgMPCost->pos.y + lowerIconImgToTextOffset, txtHeader->getSize().w, imgMPCost->pos.h }); imgMPCost->path = spellMPCostImagePath; - + imgMPCost->pos.w = 16; + imgMPCost->pos.h = 16; frameValuesPos.h += imgMPCost->disabled ? 0 : (imgMPCost->pos.y + imgMPCost->pos.h); frameValuesPos.h += pady; @@ -6167,6 +6590,15 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int imgMPCost->pos.y += imgMPCost->pos.h / 2; imgMPCost->pos.x -= padx; } + else + { + imgMPCost->pos.y += imgMPCost->pos.h / 2; + imgMPCost->pos.x -= padx; + SDL_Rect pos = txtMPCost->getSize(); + pos.y += imgMPCost->pos.h / 2; + txtMPCost->setSize(pos); + frameValuesPos.h += imgSustainedMPCost->pos.h; + } } else { @@ -6175,6 +6607,8 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int imgGoldIcon->pos.x = frameValuesPos.w - (textGetGoldValue->getWidth()) - (imgGoldIcon->pos.w + padx); imgGoldIcon->pos.y = pady; + imgGoldIcon->pos.w = 16; + imgGoldIcon->pos.h = 16; txtGoldValue->setSize(SDL_Rect{ imgGoldIcon->pos.x + imgGoldIcon->pos.w + padx, imgGoldIcon->pos.y + lowerIconImgToTextOffset, txtHeader->getSize().w, imgGoldIcon->pos.h }); if ( GenericGUI[player].assistShrineGUI.bOpen && GenericGUI[player].assistShrineGUI.itemIsFromGUI(item) ) @@ -6201,6 +6635,51 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int imgWeightIcon->path = weightImagePath; frameValuesPos.h += imgGoldIcon->disabled ? 0 : (imgGoldIcon->pos.y + imgGoldIcon->pos.h); frameValuesPos.h += pady; + + if ( tooltipType.find("tooltip_spellbook") != std::string::npos + || tooltipType.find("tooltip_tome") != std::string::npos ) + { + spell_t* spell = itemCategory(item) == SPELLBOOK ? getSpellFromID(getSpellIDFromSpellbook(item->type)) + : itemCategory(item) == TOME_SPELL ? getSpellFromID(item->getTomeSpellID()) : nullptr; + if ( spell ) + { + for ( auto& skill : Player::SkillSheet_t::skillSheetData.skillEntries ) + { + if ( skill.skillId == spell->skillID ) + { + std::string spellSkillValue = skill.getSkillName(); + char spellTierBuf[128]; + snprintf(spellTierBuf, sizeof(spellTierBuf), ItemTooltips.adjectives["spell_prefixes"]["tier"].c_str(), spell->getSpellTierName()); + spellSkillValue += spellTierBuf; + + txtSpellSkillValue->setText(spellSkillValue.c_str()); + txtSpellSkillValue->setDisabled(false); + spellSkillImg->path = skill.skillIconPath; + spellSkillImg->pos.h = 24; + spellSkillImg->pos.w = 24; + spellSkillImg->disabled = false; + + SDL_Rect pos = txtSpellSkillValue->getSize(); + spellSkillImg->pos.x = pos.x; + spellSkillImg->pos.y = pos.y + pos.h / 2 - spellSkillImg->pos.h / 2; + pos.x += spellSkillImg->pos.w; + txtSpellSkillValue->setSize(pos); + + imgGoldIcon->pos.y += imgGoldIcon->pos.h / 2; + pos = txtGoldValue->getSize(); + pos.y += imgGoldIcon->pos.h / 2; + txtGoldValue->setSize(pos); + frameValuesPos.h += imgGoldIcon->pos.h; + + imgWeightIcon->pos.y += imgWeightIcon->pos.h / 2; + pos = txtWeightValue->getSize(); + pos.y += imgGoldIcon->pos.h / 2; + txtWeightValue->setSize(pos); + break; + } + } + } + } } frameValues->setSize(frameValuesPos); @@ -6209,6 +6688,19 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int txtIdentifiedValue->setSize(SDL_Rect{ padx * 2, imgGoldIcon->pos.y + lowerIconImgToTextOffset, frameValuesPos.w - padx, txtGoldValue->getSize().h }); + if ( !txtSpellSkillValue->isDisabled() ) + { + SDL_Rect pos = txtIdentifiedValue->getSize(); + spellSkillImg->pos.x = pos.x; + spellSkillImg->pos.y = imgGoldIcon->pos.y + imgGoldIcon->pos.h / 2 - spellSkillImg->pos.h / 2; + pos.x += spellSkillImg->pos.w + padx; + pos.y -= 16 / 2 + 4; + pos.h = 24; + txtSpellSkillValue->setSize(pos); + pos.y += 16; + txtIdentifiedValue->setSize(pos); + } + if ( tooltipType.find("tooltip_spell_") != std::string::npos ) { txtIdentifiedValue->setText(ItemTooltips.getSpellTypeString(player, *item).c_str()); @@ -6264,7 +6756,7 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int framePromptPos.h = imgBottomBackground->pos.h; if ( (!item->identified - && (!isItemFromInventory || (isItemFromInventory && inputs.getVirtualMouse(player)->draw_cursor && appraisal.current_item != item->uid))) + && (!isItemFromInventory || (isItemFromInventory && false/*&& inputs.getVirtualMouse(player)->draw_cursor*/ && appraisal.current_item != item->uid))) || doShortTooltip || (frameDesc->isDisabled() && item->identified) ) { @@ -6470,10 +6962,42 @@ void Player::HUD_t::updateFrameTooltip(Item* item, const int x, const int y, int bool doAppraisalProgressPrompt = !item->identified && isItemFromInventory && appraisal.current_item == item->uid && !compendiumTooltip; if ( doAppraisalProgressPrompt ) { - real_t percent = (((double)(appraisal.timermax - appraisal.timer)) / ((double)appraisal.timermax)) * 100; - char buf[32]; - snprintf(buf, sizeof(buf), Language::get(4198), percent); - txtPrompt->setText(buf); + if ( expandBindingPressed ) + { + if ( players[player]->inventoryUI.appraisal.manual_appraised_item == item->uid ) + { + players[player]->inventoryUI.appraisal.manual_appraised_item = 0; + } + else + { + players[player]->inventoryUI.appraisal.manual_appraised_item = item->uid; + } + } + static ConsoleVariable cvar_appraisal_display("/appraisal_display", 0); + if ( *cvar_appraisal_display == 0 ) + { + char buf[64]; + Uint32 sec = (appraisal.timer / TICKS_PER_SECOND) % 60; + Uint32 min = ((appraisal.timer / TICKS_PER_SECOND) / 60) % 60; + snprintf(buf, sizeof(buf), Language::get(6956), min, sec); + txtPrompt->setText(buf); + } + else if ( *cvar_appraisal_display == 1 ) + { + real_t percent = (((double)(appraisal.timermax - appraisal.timer)) / ((double)appraisal.timermax)) * 100; + char buf[64]; + snprintf(buf, sizeof(buf), Language::get(4198), percent); + txtPrompt->setText(buf); + } + else if ( *cvar_appraisal_display == 2 ) + { + Uint32 sec = (appraisal.timer / TICKS_PER_SECOND) % 60; + Uint32 min = ((appraisal.timer / TICKS_PER_SECOND) / 60) % 60; + real_t percent = (((double)(appraisal.timermax - appraisal.timer)) / ((double)appraisal.timermax)) * 100; + char buf[64]; + snprintf(buf, sizeof(buf), Language::get(6957), min, sec, percent); + txtPrompt->setText(buf); + } } else if ( doAppraisalPrompt ) { @@ -7455,6 +7979,101 @@ void Player::Inventory_t::closeInventory() itemTooltipDisplay.scrolledToMax = 0; } +int Player::Inventory_t::getKeyAmountForWallLock(Entity& entity) const +{ + int num = 0; + ItemType key = WOODEN_SHIELD; + switch ( entity.wallLockMaterial ) + { + case 0: + key = KEY_STONE; + break; + case 1: + key = KEY_BONE; + break; + case 2: + key = KEY_BRONZE; + break; + case 3: + key = KEY_IRON; + break; + case 4: + key = KEY_SILVER; + break; + case 5: + key = KEY_GOLD; + break; + case 6: + key = KEY_CRYSTAL; + break; + case 7: + key = KEY_MACHINE; + break; + default: + break; + } + + if ( key == WOODEN_SHIELD ) { return num; } + + for ( node_t* node = stats[player.playernum]->inventory.first; node != nullptr; node = node->next ) + { + Item* tempItem = (Item*)(node->element); + if ( !tempItem ) { continue; } + if ( tempItem->type == key ) + { + num += tempItem->count; + } + } + return num; +} + +Item* Player::Inventory_t::hasKeyForWallLock(Entity& entity) const +{ + ItemType key = WOODEN_SHIELD; + switch ( entity.wallLockMaterial ) + { + case 0: + key = KEY_STONE; + break; + case 1: + key = KEY_BONE; + break; + case 2: + key = KEY_BRONZE; + break; + case 3: + key = KEY_IRON; + break; + case 4: + key = KEY_SILVER; + break; + case 5: + key = KEY_GOLD; + break; + case 6: + key = KEY_CRYSTAL; + break; + case 7: + key = KEY_MACHINE; + break; + default: + break; + } + + if ( key == WOODEN_SHIELD ) { return nullptr; } + + for ( node_t* node = stats[player.playernum]->inventory.first; node != nullptr; node = node->next ) + { + Item* tempItem = (Item*)(node->element); + if ( !tempItem ) { continue; } + if ( tempItem->type == key ) + { + return tempItem; + } + } + return nullptr; +} + bool Player::Inventory_t::guiAllowDefaultRightClick() const { if ( player.GUI.bModuleAccessibleWithMouse(player.GUI.activeModule) ) @@ -7523,6 +8142,7 @@ void Player::Inventory_t::updateInventory() auto& shopGUI = this->player.shopGUI; auto& tinkerGUI = GenericGUI[player].tinkerGUI; auto& alchemyGUI = GenericGUI[player].alchemyGUI; + auto& mailboxGUI = GenericGUI[player].mailboxGUI; auto& featherGUI = GenericGUI[player].featherGUI; auto& itemfxGUI = GenericGUI[player].itemfxGUI; auto& assistShrineGUI = GenericGUI[player].assistShrineGUI; @@ -7715,6 +8335,10 @@ void Player::Inventory_t::updateInventory() { alchemyGUI.warpMouseToSelectedAlchemyItem(nullptr, (Inputs::SET_CONTROLLER)); } + else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX ) + { + mailboxGUI.warpMouseToSelectedMailItem(nullptr, (Inputs::SET_CONTROLLER)); + } else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_FEATHER ) { featherGUI.warpMouseToSelectedFeatherItem(nullptr, (Inputs::SET_CONTROLLER)); @@ -7914,6 +8538,7 @@ void Player::Inventory_t::updateInventory() bool tinkeringSalvageOrRepairMenuActive = tinkerGUI.isSalvageOrRepairMenuActive(); bool featherDrawerOpen = featherGUI.isInscriptionDrawerOpen(); bool featherInscribeOrRepairActive = featherGUI.isInscribeOrRepairActive(); + bool transmuteItemOpen = itemfxGUI.isItemEffectMenuActive() && GenericGUI[player].transmuteItemTarget && itemfxGUI.modeHasTransmuteMenu(); featherGUI.highlightedSlot = -1; if ( true ) @@ -8107,6 +8732,55 @@ void Player::Inventory_t::updateInventory() } } + if ( mailboxGUI.bOpen && players[player]->inventory_mode == INVENTORY_MODE_ITEM + && players[player]->GUI.bModuleAccessibleWithMouse(Player::GUI_t::MODULE_MAILBOX) ) + { + for ( int x = mailboxGUI.MAIL_SLOT_RECV; x <= mailboxGUI.MAIL_SLOT_SEND; ++x ) + { + int y = 0; + bool slotVisible = false; + if ( x < 0 ) + { + if ( mailboxGUI.animSendItem1 < 0.001 ) + { + slotVisible = true; + } + } + + if ( auto slotFrame = mailboxGUI.getMailSlotFrame(x, y) ) + { + if ( !players[player]->GUI.isDropdownActive() ) // don't update selected slot while item menu open + { + if ( slotVisible && mailboxGUI.isInteractable && slotFrame->capturesMouseInRealtimeCoords() ) + { + mailboxGUI.selectMailSlot(x, y); + if ( inputs.getVirtualMouse(player)->draw_cursor ) + { + // mouse movement captures the inventory + players[player]->GUI.activateModule(Player::GUI_t::MODULE_MAILBOX); + } + } + } + + bool hideCursor = false; + if ( x == mailboxGUI.getSelectedMailSlotX() + && y == mailboxGUI.getSelectedMailSlotY() + && !hideCursor + && players[player]->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX + && mailboxGUI.isInteractable + && !(x == mailboxGUI.MAIL_SLOT_RECV && mailboxGUI.animRecvItem > 0.001) + && slotVisible ) + { + slotFrameToHighlight = slotFrame; + startx = slotFrame->getAbsoluteSize().x; + starty = slotFrame->getAbsoluteSize().y; + startx -= players[player]->camera_virtualx1(); // offset any splitscreen camera positioning. + starty -= players[player]->camera_virtualy1(); + } + } + } + } + if ( assistShrineGUI.bOpen && players[player]->inventory_mode == INVENTORY_MODE_ITEM && players[player]->GUI.bModuleAccessibleWithMouse(Player::GUI_t::MODULE_ASSISTSHRINE) ) { @@ -8225,6 +8899,7 @@ void Player::Inventory_t::updateInventory() && !tinkerCraftableListOpen && !featherDrawerOpen && !(alchemyGUI.bOpen && !alchemyGUI.isInteractable) + && !(mailboxGUI.bOpen && !mailboxGUI.isInteractable) && players[player]->GUI.bModuleAccessibleWithMouse(Player::GUI_t::MODULE_INVENTORY) ) { for ( int x = 0; x < getSizeX(); ++x ) @@ -8589,9 +9264,9 @@ void Player::Inventory_t::updateInventory() { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( chest_inventory ) @@ -8743,7 +9418,23 @@ void Player::Inventory_t::updateInventory() { if ( auto slotFrame = getItemSlotFrame(item, itemx, itemy) ) { - updateSlotFrameFromItem(slotFrame, item); + slotFrame->setUserData(&GAMEUI_FRAMEDATA_SPELL_LEARNABLE); + if ( spellPanel.spellFilterBySkill > 0 ) + { + bool greyBackground = false; + if ( auto spell = getSpellFromItem(player, item, true) ) + { + if ( spell->skillID != spellPanel.spellFilterBySkill ) + { + greyBackground = true; + } + } + updateSlotFrameFromItem(slotFrame, item, greyBackground); + } + else + { + updateSlotFrameFromItem(slotFrame, item); + } } } else if ( itemx >= 0 && itemx < getSizeX() @@ -8771,7 +9462,8 @@ void Player::Inventory_t::updateInventory() else if ( itemfxGUI.isItemEffectMenuActive() ) { auto res = itemfxGUI.setItemDisplayNameAndPrice(item, true); - if ( res == GenericGUIMenu::ItemEffectGUI_t::ITEMFX_ACTION_OK ) + if ( (res == GenericGUIMenu::ItemEffectGUI_t::ITEMFX_ACTION_OK + && GenericGUI[player].transmuteItemTarget == nullptr) || GenericGUI[player].transmuteItemTarget == item ) { updateSlotFrameFromItem(slotFrame, item); } @@ -8817,8 +9509,8 @@ void Player::Inventory_t::updateInventory() } else if ( alchemyGUI.bOpen ) { - if ( !(itemCategory(item) == POTION && item->type != POTION_EMPTY /*&& item != GenericGUI[player].alembicItem*/ - && item->identified && !itemIsEquipped(item, player)) ) + if ( !(alchemyGUI.inventoryItemAllowedInGUI(item) /*&& item != GenericGUI[player].alembicItem*/ + && item->identified && !itemIsEquipped(item, player) && !(item->type == FOOD_TIN && !alchemyGUI.hasTinOpener)) ) { updateSlotFrameFromItem(slotFrame, item, true); } @@ -8846,6 +9538,23 @@ void Player::Inventory_t::updateInventory() updateSlotFrameFromItem(slotFrame, item, !item->identified); } } + else if ( mailboxGUI.bOpen ) + { + if ( !(mailboxGUI.inventoryItemAllowedInGUI(item) + && item->identified && !itemIsEquipped(item, player)) ) + { + updateSlotFrameFromItem(slotFrame, item, true); + } + else if ( (mailboxGUI.sendItem1Uid == item->uid) ) + { + slotFrame->setUserData(&GAMEUI_FRAMEDATA_ALCHEMY_ITEM); + updateSlotFrameFromItem(slotFrame, item); + } + else + { + updateSlotFrameFromItem(slotFrame, item, !item->identified); + } + } else { updateSlotFrameFromItem(slotFrame, item); @@ -8903,7 +9612,11 @@ void Player::Inventory_t::updateInventory() shopGUI.clearItemDisplayed(); tinkerGUI.clearItemDisplayed(); alchemyGUI.clearItemDisplayed(); - itemfxGUI.clearItemDisplayed(); + mailboxGUI.clearItemDisplayed(); + if ( !transmuteItemOpen ) + { + itemfxGUI.clearItemDisplayed(); + } if ( !featherDrawerOpen || (featherDrawerOpen && GenericGUI[player].scribingBlankScrollTarget == nullptr) ) { featherGUI.clearItemDisplayed(); @@ -8956,7 +9669,7 @@ void Player::Inventory_t::updateInventory() break; } - if ( stats[player]->HP <= 0 ) + if ( stats[player]->HP <= 0 || players[player]->ghost.isActive() ) { break; } @@ -9016,7 +9729,7 @@ void Player::Inventory_t::updateInventory() break; } - if ( stats[player]->HP <= 0 ) + if ( stats[player]->HP <= 0 || players[player]->ghost.isActive() ) { break; } @@ -9067,7 +9780,7 @@ void Player::Inventory_t::updateInventory() break; } - if ( stats[player]->HP <= 0 ) + if ( stats[player]->HP <= 0 || players[player]->ghost.isActive() ) { break; } @@ -9102,9 +9815,9 @@ void Player::Inventory_t::updateInventory() { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( chest_inventory ) @@ -9214,7 +9927,7 @@ void Player::Inventory_t::updateInventory() players[player]->hud.updateFrameTooltip(item, tooltipCoordX, tooltipCoordY, justify); } - if ( stats[player]->HP <= 0 ) + if ( stats[player]->HP <= 0 || players[player]->ghost.isActive() ) { break; } @@ -9551,6 +10264,17 @@ void Player::Inventory_t::updateInventory() tooltipCoordX -= alchemyGUI.alchFrame->getSize().w; } } + if ( mailboxGUI.mailFrame && mailboxGUI.bOpen && mailboxGUI.isInteractable ) + { + if ( justify == PANEL_JUSTIFY_LEFT ) + { + tooltipCoordX += mailboxGUI.mailFrame->getSize().w; + } + else + { + tooltipCoordX -= mailboxGUI.mailFrame->getSize().w; + } + } } else { @@ -9606,6 +10330,7 @@ void Player::Inventory_t::updateInventory() bool sellingItemToShop = false; bool tinkerOpen = false; bool alchemyOpen = false; + bool mailboxOpen = false; bool featherOpen = false; bool itemfxOpen = false; if ( featherInscribeOrRepairActive ) @@ -9621,7 +10346,10 @@ void Player::Inventory_t::updateInventory() { tooltipOpen = false; itemfxOpen = true; - itemfxGUI.setItemDisplayNameAndPrice(item); + if ( !transmuteItemOpen ) + { + itemfxGUI.setItemDisplayNameAndPrice(item); + } } else if ( tinkeringSalvageOrRepairMenuActive ) { @@ -9640,11 +10368,19 @@ void Player::Inventory_t::updateInventory() else if ( alchemyGUI.bOpen ) { alchemyOpen = true; - if ( itemCategory(item) == POTION && item->type != POTION_EMPTY ) + if ( alchemyGUI.inventoryItemAllowedInGUI(item) ) { alchemyGUI.setItemDisplayNameAndPrice(item, false, false); } } + else if ( mailboxGUI.bOpen ) + { + mailboxOpen = true; + if ( mailboxGUI.inventoryItemAllowedInGUI(item) ) + { + mailboxGUI.setItemDisplayNameAndPrice(item, false); + } + } else { if ( inventoryControlActive && !bIsTooltipDelayed() ) @@ -9654,7 +10390,7 @@ void Player::Inventory_t::updateInventory() } } - if ( stats[player]->HP <= 0 ) + if ( stats[player]->HP <= 0 || players[player]->ghost.isActive() ) { break; } @@ -9670,6 +10406,7 @@ void Player::Inventory_t::updateInventory() || tinkerOpen || featherOpen || itemfxOpen + || mailboxOpen || alchemyOpen) && inventoryControlActive && !selectedItem ) @@ -9868,22 +10605,43 @@ void Player::Inventory_t::updateInventory() if ( guiAllowDefaultRightClick() ) { // auto-appraise the item + int prevAppraisedManual = players[player]->inventoryUI.appraisal.manual_appraised_item; appraisal.appraiseItem(item); + if ( appraisal.current_item == item->uid ) + { + if ( prevAppraisedManual == item->uid ) + { + appraisal.manual_appraised_item = 0; + } + else + { + appraisal.manual_appraised_item = item->uid; + } + } Input::inputs[player].consumeBinaryToggle("MenuRightClick"); } } else if ( !disableItemUsage - && (itemCategory(item) == POTION || itemCategory(item) == SPELLBOOK || item->type == FOOD_CREAMPIE) && + && (itemCategory(item) == POTION + || itemCategory(item) == SPELL_CAT + || itemCategory(item) == SPELLBOOK || item->type == FOOD_CREAMPIE) && (keystatus[SDLK_LALT] || keystatus[SDLK_RALT]) ) { Input::inputs[player].consumeBinaryToggle("MenuRightClick"); if ( guiAllowDefaultRightClick() ) { - // force equip potion/spellbook - playerTryEquipItemAndUpdateServer(player, item, false); + if ( itemCategory(item) == SPELL_CAT ) + { + players[player]->inventoryUI.activateItemContextMenuOption(item, ItemContextMenuPrompts::PROMPT_SPELL_QUICKCAST); + } + else + { + // force equip potion/spellbook + playerTryEquipItemAndUpdateServer(player, item, true); + } } } - else if ( !tinkeringSalvageOrRepairMenuActive && !alchemyOpen && !featherInscribeOrRepairActive && !itemfxOpen ) + else if ( !tinkeringSalvageOrRepairMenuActive && !alchemyOpen && !mailboxOpen && !featherInscribeOrRepairActive && !itemfxOpen ) { // open a drop-down menu of options for "using" the item itemMenuOpen = true; @@ -10075,7 +10833,7 @@ void Player::Inventory_t::updateInventory() } } - if ( !noPreviousSelectedItem && stats[player]->HP > 0 ) + if ( !noPreviousSelectedItem && stats[player]->HP > 0 && !players[player]->ghost.isActive() ) { // releasing items Item* oldSelectedItem = selectedItem; @@ -10196,7 +10954,22 @@ void Player::Inventory_t::updateInventory() { if ( auto slotFrame = getItemSlotFrame(item, itemx, itemy) ) { - updateSlotFrameFromItem(slotFrame, item); + if ( spellPanel.spellFilterBySkill > 0 ) + { + bool greyBackground = false; + if ( auto spell = getSpellFromItem(player, item, true) ) + { + if ( spell->skillID != spellPanel.spellFilterBySkill ) + { + greyBackground = true; + } + } + updateSlotFrameFromItem(slotFrame, item, greyBackground); + } + else + { + updateSlotFrameFromItem(slotFrame, item); + } } } else if ( itemx >= 0 && itemx < getSizeX() @@ -10224,7 +10997,8 @@ void Player::Inventory_t::updateInventory() else if ( itemfxGUI.isItemEffectMenuActive() ) { auto res = itemfxGUI.setItemDisplayNameAndPrice(item, true); - if ( res == GenericGUIMenu::ItemEffectGUI_t::ITEMFX_ACTION_OK ) + if ( (res == GenericGUIMenu::ItemEffectGUI_t::ITEMFX_ACTION_OK + && GenericGUI[player].transmuteItemTarget == nullptr) || GenericGUI[player].transmuteItemTarget == item ) { updateSlotFrameFromItem(slotFrame, item); } @@ -10268,8 +11042,8 @@ void Player::Inventory_t::updateInventory() } else if ( alchemyGUI.bOpen ) { - if ( !(itemCategory(item) == POTION && item->type != POTION_EMPTY /*&& item != GenericGUI[player].alembicItem*/ - && item->identified && !itemIsEquipped(item, player)) ) + if ( !(alchemyGUI.inventoryItemAllowedInGUI(item) /*&& item != GenericGUI[player].alembicItem*/ + && item->identified && !itemIsEquipped(item, player) && !(item->type == FOOD_TIN && !alchemyGUI.hasTinOpener)) ) { updateSlotFrameFromItem(slotFrame, item, true); } @@ -10297,6 +11071,23 @@ void Player::Inventory_t::updateInventory() updateSlotFrameFromItem(slotFrame, item, !item->identified); } } + else if ( mailboxGUI.bOpen ) + { + if ( !(mailboxGUI.inventoryItemAllowedInGUI(item) + && item->identified && !itemIsEquipped(item, player)) ) + { + updateSlotFrameFromItem(slotFrame, item, true); + } + else if ( (mailboxGUI.sendItem1Uid == item->uid) ) + { + slotFrame->setUserData(&GAMEUI_FRAMEDATA_ALCHEMY_ITEM); + updateSlotFrameFromItem(slotFrame, item); + } + else + { + updateSlotFrameFromItem(slotFrame, item, !item->identified); + } + } else { updateSlotFrameFromItem(slotFrame, item); @@ -10312,9 +11103,9 @@ void Player::Inventory_t::updateInventory() { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( chest_inventory ) @@ -10504,6 +11295,7 @@ std::string getContextMenuOptionBindingName(const int player, const ItemContextM case PROMPT_STORE_CHEST_ALL: case PROMPT_CONSUME_ALTERNATE: case PROMPT_INSPECT_ALTERNATE: + case PROMPT_SCEPTER_CHARGE: if ( players[player]->inventoryUI.useItemDropdownOnGamepad == Player::Inventory_t::GAMEPAD_DROPDOWN_COMPACT ) { return "MenuConfirm"; @@ -10515,10 +11307,12 @@ std::string getContextMenuOptionBindingName(const int player, const ItemContextM case PROMPT_GRAB: return "MenuAlt1"; case PROMPT_TINKER: + case PROMPT_COOK: case PROMPT_INTERACT: case PROMPT_INTERACT_SPELLBOOK_HOTBAR: case PROMPT_EAT: case PROMPT_SPELL_QUICKCAST: + case PROMPT_SPELL_CHANGE_FOCUS: case PROMPT_CONSUME: case PROMPT_SELL: case PROMPT_BUY: @@ -10557,8 +11351,12 @@ const char* getContextMenuLangEntry(const int player, const ItemContextMenuPromp return itemUseString(player, item); case PROMPT_SPELL_QUICKCAST: return Language::get(4049); + case PROMPT_SPELL_CHANGE_FOCUS: + return Language::get(6556); case PROMPT_TINKER: return Language::get(3670); + case PROMPT_COOK: + return Language::get(6773); case PROMPT_CLEAR_HOTBAR_SLOT: return Language::get(3723); case PROMPT_APPRAISE: @@ -10588,6 +11386,8 @@ const char* getContextMenuLangEntry(const int player, const ItemContextMenuPromp return Language::get(4050); case PROMPT_DROPDOWN: return Language::get(4040); + case PROMPT_SCEPTER_CHARGE: + return Language::get(6834); default: return "Invalid"; } @@ -10596,7 +11396,7 @@ const char* getContextMenuLangEntry(const int player, const ItemContextMenuPromp std::vector getContextTooltipOptionsForItem(const int player, Item* item, int useDropdownMenu, bool hotbarItem) { - if ( stats[player] && stats[player]->HP <= 0 ) + if ( (stats[player] && stats[player]->HP <= 0) || players[player]->ghost.isActive() ) { // ded, cant do anything with items return std::vector(); @@ -10690,7 +11490,17 @@ std::vector getContextMenuOptionsForItem(const int playe { options.push_back(PROMPT_SPELL_EQUIP); options.push_back(PROMPT_SPELL_QUICKCAST); - return options; + /*if ( spell_t* spell = getSpellFromItem(player, item, true) ) + { + if ( spell->ID == SPELL_RESHAPE_WEAPON ) + { + options.push_back(PROMPT_SPELL_CHANGE_FOCUS); + } + }*/ + if ( !(GenericGUI[player].itemfxGUI.bOpen && GenericGUI[player].itemfxGUI.currentMode == GenericGUIMenu::ItemEffectGUI_t::ITEMFX_MODE_SCEPTER_CHARGE) ) + { + return options; + } } if ( itemCategory(item) == POTION ) @@ -10737,6 +11547,20 @@ std::vector getContextMenuOptionsForItem(const int playe options.push_back(PROMPT_APPRAISE); options.push_back(PROMPT_DROP); } + else if ( item->type == TOOL_FRYING_PAN ) + { + options.push_back(PROMPT_COOK); + options.push_back(PROMPT_EQUIP); + options.push_back(PROMPT_APPRAISE); + options.push_back(PROMPT_DROP); + } + else if ( item->type == MAGICSTAFF_SCEPTER ) + { + options.push_back(PROMPT_EQUIP); + options.push_back(PROMPT_SCEPTER_CHARGE); + options.push_back(PROMPT_APPRAISE); + options.push_back(PROMPT_DROP); + } else if ( item->type == FOOD_CREAMPIE ) { if ( !itemIsEquipped(item, player) ) @@ -10842,6 +11666,7 @@ std::vector getContextMenuOptionsForItem(const int playe bool alembicOpen = false; bool featherOpen = false; bool itemfxOpen = false; + bool mailboxOpen = false; if ( players[player]->gui_mode == GUI_MODE_SHOP && itemCategory(item) != SPELL_CAT ) { if ( playerOwnedItem ) @@ -10857,6 +11682,10 @@ std::vector getContextMenuOptionsForItem(const int playe { alembicOpen = true; } + else if ( GenericGUI[player].mailboxGUI.bOpen ) + { + mailboxOpen = true; + } else if ( GenericGUI[player].featherGUI.bOpen ) { featherOpen = true; @@ -10868,7 +11697,7 @@ std::vector getContextMenuOptionsForItem(const int playe for ( auto it = options.begin(); it != options.end(); ) { - if ( sellingToShop || tinkerOpen || alembicOpen || featherOpen || itemfxOpen ) + if ( sellingToShop || tinkerOpen || alembicOpen || featherOpen || itemfxOpen || mailboxOpen ) { if ( getContextMenuOptionBindingName(player, *it) == "MenuConfirm" || getContextMenuOptionBindingName(player, *it) == "MenuCancel" ) @@ -10891,6 +11720,11 @@ std::vector getContextMenuOptionsForItem(const int playe it = options.erase(it); continue; } + if ( itemfxOpen && GenericGUI[player].itemfxGUI.modeHasTransmuteMenu() && getContextMenuOptionBindingName(player, *it) == "MenuAlt2" ) + { + it = options.erase(it); + continue; + } if ( *it == PROMPT_CONSUME_ALTERNATE || *it == PROMPT_INSPECT_ALTERNATE ) { it = options.erase(it); @@ -10938,80 +11772,6 @@ std::vector getContextMenuOptionsForItem(const int playe return options; } -inline bool itemMenuSkipRow1ForShopsAndChests(const int player, const Item& item) -{ - if ( (openedChest[player] || players[player]->gui_mode == GUI_MODE_SHOP) - && (itemCategory(&item) == POTION || item.type == TOOL_ALEMBIC || item.type == TOOL_TINKERING_KIT || itemCategory(&item) == SPELLBOOK) ) - { - return true; - } - return false; -} - -inline void drawItemMenuOptionAutomaton(const int player, const Item& item, int x, int y, int height, bool is_potion_bad) -{ - return; - int width = 0; - - //Option 0. - if ( openedChest[player] ) - { - //drawOptionStoreInChest(x, y); - } - else if ( players[player]->gui_mode == GUI_MODE_SHOP ) - { - //drawOptionSell(x, y); - } - else - { - if ( !is_potion_bad ) - { - if ( !itemIsConsumableByAutomaton(item) || (itemCategory(&item) != FOOD && item.type != TOOL_METAL_SCRAP && item.type != TOOL_MAGIC_SCRAP) ) - { - //drawOptionUse(player, item, x, y); - } - else - { - getSizeOfText(ttf12, Language::get(3487), &width, nullptr); - ttfPrintText(ttf12, x + 50 - width / 2, y + 4, Language::get(3487)); - } - } - else - { - if ( itemIsEquipped(&item, player) ) - { - //drawOptionUnwield(x, y); - } - else - { - //drawOptionWield(x, y); - } - } - } - y += height; - - //Option 1. - if ( item.type == TOOL_METAL_SCRAP || item.type == TOOL_MAGIC_SCRAP ) - { - getSizeOfText(ttf12, Language::get(1881), &width, nullptr); - ttfPrintText(ttf12, x + 50 - width / 2, y + 4, Language::get(1881)); - y += height; - } - else if ( itemCategory(&item) != FOOD ) - { - getSizeOfText(ttf12, Language::get(3487), &width, nullptr); - ttfPrintText(ttf12, x + 50 - width / 2, y + 4, Language::get(3487)); - y += height; - } - - //Option 1. - //drawOptionAppraise(x, y); - y += height; - - //Option 2. - //drawOptionDrop(x, y); -} - // filters out items excluded by auto_hotbar_categories bool autoAddHotbarFilter(const Item& item) { @@ -11063,6 +11823,26 @@ bool autoAddHotbarFilter(const Item& item) case TOOL_TORCH: case TOOL_LANTERN: case TOOL_CRYSTALSHARD: + case INSTRUMENT_FLUTE: + case INSTRUMENT_LYRE: + case INSTRUMENT_DRUM: + case INSTRUMENT_LUTE: + case INSTRUMENT_HORN: + case TOOL_FOCI_FIRE: + case TOOL_FOCI_SNOW: + case TOOL_FOCI_NEEDLES: + case TOOL_FOCI_ARCS: + case TOOL_FOCI_SAND: + case TOOL_FOCI_DARK_LIFE: + case TOOL_FOCI_DARK_RIFT: + case TOOL_FOCI_DARK_SILENCE: + case TOOL_FOCI_DARK_VENGEANCE: + case TOOL_FOCI_DARK_SUPPRESS: + case TOOL_FOCI_LIGHT_PEACE: + case TOOL_FOCI_LIGHT_JUSTICE: + case TOOL_FOCI_LIGHT_PROVIDENCE: + case TOOL_FOCI_LIGHT_PURITY: + case TOOL_FOCI_LIGHT_SANCTUARY: return true; break; default: @@ -11072,7 +11852,7 @@ bool autoAddHotbarFilter(const Item& item) break; case 5: // thrown if ( cat == THROWN || item.type == GEM_ROCK || itemTypeIsQuiver(item.type) - || itemIsThrowableTinkerTool(&item) || item.type == TOOL_BEARTRAP ) + || itemIsThrowableTinkerTool(&item) || item.type == TOOL_BEARTRAP || item.type == TOOL_DUCK ) { return true; } @@ -11207,6 +11987,7 @@ void autosortInventory(int player, bool sortPaperDoll) break; case 3: // books/spellbooks sortInventoryItemsOfType(player, SPELLBOOK, invertSortDirection); + sortInventoryItemsOfType(player, TOME_SPELL, invertSortDirection); sortInventoryItemsOfType(player, BOOK, invertSortDirection); break; case 4: // tools @@ -11261,6 +12042,7 @@ void autosortInventory(int player, bool sortPaperDoll) break; case 3: // books/spellbooks sortInventoryItemsOfType(player, SPELLBOOK, invertSortDirection); + sortInventoryItemsOfType(player, TOME_SPELL, invertSortDirection); sortInventoryItemsOfType(player, BOOK, invertSortDirection); break; case 4: // tools @@ -11503,13 +12285,13 @@ bool Player::Inventory_t::moveItemToFreeInventorySlot(Item* item) void sortInventoryItemsOfType(int player, int categoryInt, bool sortRightToLeft) { - node_t* node = nullptr; - Item* itemBeingSorted = nullptr; Category cat = static_cast(categoryInt); - for ( node = stats[player]->inventory.first; node != NULL; node = node->next ) + std::map> itemsToSort; + std::vector itemTypeSortOrder; + for ( node_t* node = stats[player]->inventory.first; node != NULL; node = node->next ) { - itemBeingSorted = (Item*)node->element; + Item* itemBeingSorted = (Item*)node->element; if ( itemBeingSorted && (itemBeingSorted->x == -1 || itemBeingSorted->x == Player::PaperDoll_t::ITEM_RETURN_TO_INVENTORY_COORDINATE) ) { if ( itemCategory(itemBeingSorted) == SPELL_CAT ) @@ -11522,6 +12304,10 @@ void sortInventoryItemsOfType(int player, int categoryInt, bool sortRightToLeft) { // exception for rocks as they are part of the thrown sort category... } + else if ( categoryInt == MAGICSTAFF && itemTypeIsFoci(itemBeingSorted->type) ) + { + // foci part of magicstaffs + } else { // if item is not in the category specified, continue on. @@ -11540,78 +12326,32 @@ void sortInventoryItemsOfType(int player, int categoryInt, bool sortRightToLeft) continue; } } - - // find a place... - int x, y; - bool notfree = false, foundaspot = false; - - bool is_spell = false; - int inventory_y = std::min(players[player]->inventoryUI.getSizeY(), players[player]->inventoryUI.DEFAULT_INVENTORY_SIZEY); // only sort y values of 2-3, if extra row don't auto sort into it. - - if ( sortRightToLeft ) - { - x = players[player]->inventoryUI.getSizeX() - 1; // fill rightmost first. - } - else + if ( categoryInt == TOOL && itemTypeIsFoci(itemBeingSorted->type) ) { - x = 0; // fill leftmost first. + continue; } - while ( 1 ) + + if ( itemsToSort.find(itemBeingSorted->type) == itemsToSort.end() ) { - for ( y = 0; y < inventory_y; y++ ) - { - node_t* node2 = nullptr; - for ( node2 = stats[player]->inventory.first; node2 != nullptr; node2 = node2->next ) - { - Item* tempItem = (Item*)node2->element; - if ( tempItem == itemBeingSorted ) - { - continue; - } - if ( tempItem ) - { - if ( tempItem->x == x && tempItem->y == y ) - { - if ( is_spell && itemCategory(tempItem) == SPELL_CAT ) - { - notfree = true; //Both spells. Can't fit in the same slot. - } - else if ( !is_spell && itemCategory(tempItem) != SPELL_CAT ) - { - notfree = true; //Both not spells. Can't fit in the same slot. - } - } - } - } - if ( notfree ) - { - notfree = false; - continue; - } - itemBeingSorted->x = x; - itemBeingSorted->y = y; - foundaspot = true; - break; - } - if ( foundaspot ) - { - break; - } - if ( sortRightToLeft ) - { - --x; // fill rightmost first. - } - else - { - ++x; // fill leftmost first. - } + itemTypeSortOrder.push_back(itemBeingSorted->type); } + itemsToSort[itemBeingSorted->type].push_back(itemBeingSorted); + } + } - // backpack sorting, sort into here as last priority. - if ( (x < 0 || x > players[player]->inventoryUI.getSizeX() - 1) && players[player]->inventoryUI.getSizeY() > players[player]->inventoryUI.DEFAULT_INVENTORY_SIZEY ) + for ( auto type : itemTypeSortOrder ) + { + for ( Item* itemBeingSorted : itemsToSort[type] ) + { + if ( itemBeingSorted && (itemBeingSorted->x == -1 || itemBeingSorted->x == Player::PaperDoll_t::ITEM_RETURN_TO_INVENTORY_COORDINATE) ) { - foundaspot = false; - notfree = false; + // find a place... + int x, y; + bool notfree = false, foundaspot = false; + + bool is_spell = false; + int inventory_y = std::min(players[player]->inventoryUI.getSizeY(), players[player]->inventoryUI.DEFAULT_INVENTORY_SIZEY); // only sort y values of 2-3, if extra row don't auto sort into it. + if ( sortRightToLeft ) { x = players[player]->inventoryUI.getSizeX() - 1; // fill rightmost first. @@ -11622,7 +12362,7 @@ void sortInventoryItemsOfType(int player, int categoryInt, bool sortRightToLeft) } while ( 1 ) { - for ( y = players[player]->inventoryUI.DEFAULT_INVENTORY_SIZEY; y < players[player]->inventoryUI.getSizeY(); y++ ) + for ( y = 0; y < inventory_y; y++ ) { node_t* node2 = nullptr; for ( node2 = stats[player]->inventory.first; node2 != nullptr; node2 = node2->next ) @@ -11670,6 +12410,71 @@ void sortInventoryItemsOfType(int player, int categoryInt, bool sortRightToLeft) ++x; // fill leftmost first. } } + + // backpack sorting, sort into here as last priority. + if ( (x < 0 || x > players[player]->inventoryUI.getSizeX() - 1) && players[player]->inventoryUI.getSizeY() > players[player]->inventoryUI.DEFAULT_INVENTORY_SIZEY ) + { + foundaspot = false; + notfree = false; + if ( sortRightToLeft ) + { + x = players[player]->inventoryUI.getSizeX() - 1; // fill rightmost first. + } + else + { + x = 0; // fill leftmost first. + } + while ( 1 ) + { + for ( y = players[player]->inventoryUI.DEFAULT_INVENTORY_SIZEY; y < players[player]->inventoryUI.getSizeY(); y++ ) + { + node_t* node2 = nullptr; + for ( node2 = stats[player]->inventory.first; node2 != nullptr; node2 = node2->next ) + { + Item* tempItem = (Item*)node2->element; + if ( tempItem == itemBeingSorted ) + { + continue; + } + if ( tempItem ) + { + if ( tempItem->x == x && tempItem->y == y ) + { + if ( is_spell && itemCategory(tempItem) == SPELL_CAT ) + { + notfree = true; //Both spells. Can't fit in the same slot. + } + else if ( !is_spell && itemCategory(tempItem) != SPELL_CAT ) + { + notfree = true; //Both not spells. Can't fit in the same slot. + } + } + } + } + if ( notfree ) + { + notfree = false; + continue; + } + itemBeingSorted->x = x; + itemBeingSorted->y = y; + foundaspot = true; + break; + } + if ( foundaspot ) + { + break; + } + if ( sortRightToLeft ) + { + --x; // fill rightmost first. + } + else + { + ++x; // fill leftmost first. + } + } + } } } } @@ -11730,7 +12535,7 @@ bool playerLearnedSpellbook(int player, Item* current_item) { return false; } - if ( itemCategory(current_item) != SPELLBOOK ) + if ( !(itemCategory(current_item) == SPELLBOOK || itemCategory(current_item) == TOME_SPELL) ) { return false; } @@ -11756,10 +12561,21 @@ bool playerLearnedSpellbook(int player, Item* current_item) { continue; } - if ( current_item->type == getSpellbookFromSpellID(spell->ID) ) + + if ( itemCategory(current_item) == SPELLBOOK ) { - // learned spell, default option is now equip spellbook. - return true; + if ( current_item->type == getSpellbookFromSpellID(spell->ID) ) + { + // learned spell, default option is now equip spellbook. + return true; + } + } + else if ( itemCategory(current_item) == TOME_SPELL ) + { + if ( spell->ID == current_item->getTomeSpellID() ) + { + return true; + } } } return false; diff --git a/src/interface/shopgui.cpp b/src/interface/shopgui.cpp index c19fd2a18..150eece88 100644 --- a/src/interface/shopgui.cpp +++ b/src/interface/shopgui.cpp @@ -928,6 +928,11 @@ void Player::ShopGUI_t::setItemDisplayNameAndPrice(Item* item) snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY, item->beatitude); } + else if ( item->type == MAGICSTAFF_SCEPTER && item->identified ) + { + snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), + item->getName(), item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX, item->beatitude); + } else { snprintf(buf, sizeof(buf), "%s %s (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->beatitude); @@ -990,6 +995,11 @@ void Player::ShopGUI_t::setItemDisplayNameAndPrice(Item* item) snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY, item->beatitude); } + else if ( item->type == MAGICSTAFF_SCEPTER && item->identified ) + { + snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), + item->getName(), item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX, item->beatitude); + } else { snprintf(buf, sizeof(buf), "%s %s (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->beatitude); diff --git a/src/interface/updatecharactersheet.cpp b/src/interface/updatecharactersheet.cpp index 88e3279d6..dbb9502aa 100644 --- a/src/interface/updatecharactersheet.cpp +++ b/src/interface/updatecharactersheet.cpp @@ -48,12 +48,17 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) if ( !stats[player]->weapon || shapeshiftUseMeleeAttack ) { // fists - attack += Entity::getAttack(players[player]->entity, stats[player], true); + int weaponAttack = 0; + attack += Entity::getAttack(players[player]->entity, stats[player], true, -1, &weaponAttack); output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_UNARMED; // melee output.totalAttack = attack; output.proficiencyBonus = (stats[player]->getModifiedProficiency(PRO_UNARMED) / 20); // bonus from proficiency - output.mainAttributeBonus = statGetSTR(stats[player], entity); // bonus from main attribute - output.equipmentAndEffectBonus = attack - statGetSTR(stats[player], entity) - BASE_PLAYER_UNARMED_DAMAGE - output.proficiencyBonus; // bonus from equipment + Sint32 attr = statGetSTR(stats[player], entity); // bonus from main attribute + Sint32 prevStat = stats[player]->STR; + stats[player]->STR += -attr; + output.mainAttributeBonus = output.totalAttack - Entity::getAttack(players[player]->entity, stats[player], true); + stats[player]->STR = prevStat; + output.equipmentAndEffectBonus = weaponAttack - BASE_PLAYER_UNARMED_DAMAGE - output.proficiencyBonus; // bonus from equipment { real_t variance = 20; @@ -79,23 +84,30 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) { if ( isRangedWeapon(*stats[player]->weapon) ) { + Sint32 quiverATK = 0; + if ( stats[player]->shield && rangedWeaponUseQuiverOnAttack(stats[player]) ) + { + quiverATK = stats[player]->shield->weaponGetAttack(stats[player]); + } if ( entity ) { - attack += entity->getRangedAttack(); + attack += entity->getRangedAttack(quiverATK); } output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_RANGED; // ranged output.totalAttack = attack; output.weaponBonus = stats[player]->weapon->weaponGetAttack(stats[player]); // bonus from weapon output.equipmentAndEffectBonus = 0; - Sint32 quiverATK = 0; - if ( stats[player]->shield && rangedWeaponUseQuiverOnAttack(stats[player]) ) + + Sint32 attr = statGetDEX(stats[player], entity); // bonus from main attribute + Sint32 prevStat = stats[player]->DEX; + stats[player]->DEX += -attr; + if ( entity ) { - quiverATK = stats[player]->shield->weaponGetAttack(stats[player]); - attack += quiverATK; + output.mainAttributeBonus = output.totalAttack - entity->getRangedAttack(quiverATK); } - output.mainAttributeBonus = statGetDEX(stats[player], entity); // bonus from main attribute - output.equipmentAndEffectBonus += attack - output.mainAttributeBonus - - BASE_RANGED_DAMAGE - output.weaponBonus; // bonus from equipment + stats[player]->DEX = prevStat; + + output.equipmentAndEffectBonus += quiverATK; // bonus from equipment { real_t variance = 20; @@ -122,13 +134,22 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) output.weaponBonus = stats[player]->weapon->weaponGetAttack(stats[player]); // bonus from weapon Sint32 STR = statGetSTR(stats[player], entity); Sint32 DEX = statGetDEX(stats[player], entity); - Sint32 totalAttributeBonus = (STR + DEX); - totalAttributeBonus = std::min(totalAttributeBonus / 2, totalAttributeBonus); + + Sint32 prevStatSTR = stats[player]->STR; + Sint32 prevStatDEX = stats[player]->DEX; + stats[player]->STR += -STR; + stats[player]->DEX += -DEX; + output.mainAttributeBonus = output.totalAttack - Entity::getAttack(players[player]->entity, stats[player], true); + stats[player]->STR = prevStatSTR; + stats[player]->DEX = prevStatDEX; + + //Sint32 totalAttributeBonus = (STR + DEX); + //totalAttributeBonus = std::min(totalAttributeBonus / 2, totalAttributeBonus); //output.mainAttributeBonus = totalAttributeBonus - STRComponent; // bonus from main attribute (DEX) //output.secondaryAttributeBonus = totalAttributeBonus - DEXComponent; // secondary (STR) - output.mainAttributeBonus = totalAttributeBonus; - output.equipmentAndEffectBonus += attack - totalAttributeBonus - - BASE_MELEE_DAMAGE - output.weaponBonus; // bonus from equipment + //output.mainAttributeBonus = totalAttributeBonus; + //output.equipmentAndEffectBonus += attack - totalAttributeBonus + // - BASE_MELEE_DAMAGE - output.weaponBonus; // bonus from equipment { real_t variance = 20; @@ -155,6 +176,10 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) attack += entity->getThrownAttack(); } output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_THROWN; // thrown + if ( itemTypeIsThrownBall(stats[player]->weapon->type) || stats[player]->weapon->type == BOLAS ) + { + output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_THROWN_MISC; + } output.totalAttack = attack; // bonus from weapon output.weaponBonus = stats[player]->weapon->weaponGetAttack(stats[player]); @@ -162,13 +187,21 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) if ( itemCategory(stats[player]->weapon) != POTION ) { Sint32 oldDEX = statGetDEX(stats[player], entity); - Sint32 DEXComponent = oldDEX / 4; + //Sint32 DEXComponent = oldDEX / 4; if ( itemCategory(stats[player]->weapon) == THROWN ) { - output.mainAttributeBonus = DEXComponent; //(output.totalAttack - entity->getThrownAttack()); + //output.mainAttributeBonus = DEXComponent; //(output.totalAttack - entity->getThrownAttack()); Sint32 oldSkill = stats[player]->getProficiency(PRO_RANGED); stats[player]->setProficiencyUnsafe(PRO_RANGED, -999); + + Sint32 noSkillATK = (entity ? entity->getThrownAttack() : 0); + + Sint32 prevStat = stats[player]->DEX; + stats[player]->DEX += -oldDEX; + output.mainAttributeBonus = noSkillATK - (entity ? entity->getThrownAttack() : 0); + stats[player]->DEX = prevStat; + output.proficiencyBonus = output.totalAttack - (entity ? entity->getThrownAttack() : 0); stats[player]->setProficiency(PRO_RANGED, oldSkill); } @@ -179,14 +212,27 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_THROWN_GEM; // thrown } // gems etc. - Sint32 tmpDEX = stats[player]->DEX; + /*Sint32 tmpDEX = stats[player]->DEX; stats[player]->DEX -= oldDEX; if ( entity ) { output.mainAttributeBonus = (output.totalAttack - (entity ? entity->getThrownAttack() : 0)); } output.proficiencyBonus = skillLVL / 10; - stats[player]->DEX = tmpDEX; + stats[player]->DEX = tmpDEX;*/ + + Sint32 oldSkill = stats[player]->getProficiency(PRO_RANGED); + stats[player]->setProficiencyUnsafe(PRO_RANGED, -999); + + Sint32 noSkillATK = (entity ? entity->getThrownAttack() : 0); + + Sint32 prevStat = stats[player]->DEX; + stats[player]->DEX += -oldDEX; + output.mainAttributeBonus = noSkillATK - (entity ? entity->getThrownAttack() : 0); + stats[player]->DEX = prevStat; + + output.proficiencyBonus = output.totalAttack - (entity ? entity->getThrownAttack() : 0); + stats[player]->setProficiency(PRO_RANGED, oldSkill); } output.proficiency = weaponskill; } @@ -225,7 +271,23 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_MELEE_WEAPON; // melee output.totalAttack = attack; output.weaponBonus = stats[player]->weapon->weaponGetAttack(stats[player]); // bonus from weapon - output.mainAttributeBonus = statGetSTR(stats[player], entity); // bonus from main attribute + if ( stats[player]->weapon && stats[player]->weapon->type == RAPIER ) + { + output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_RAPIER; + Sint32 attr = statGetDEX(stats[player], entity); // bonus from main attribute + Sint32 prevStat = stats[player]->DEX; + stats[player]->DEX += -attr; + output.mainAttributeBonus = output.totalAttack - Entity::getAttack(players[player]->entity, stats[player], true); + stats[player]->DEX = prevStat; + } + else + { + Sint32 attr = statGetSTR(stats[player], entity); // bonus from main attribute + Sint32 prevStat = stats[player]->STR; + stats[player]->STR += -attr; + output.mainAttributeBonus = output.totalAttack - Entity::getAttack(players[player]->entity, stats[player], true); + stats[player]->STR = prevStat; + } if ( weaponskill == PRO_AXE ) { output.weaponBonus += 1; // bonus from equipment @@ -250,20 +312,21 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) output.totalAttack = output.attackMaxRange - ((output.attackMaxRange - output.attackMinRange) / 2.0); } } - else if ( itemCategory(stats[player]->weapon) == MAGICSTAFF ) // staffs. + else if ( itemCategory(stats[player]->weapon) == MAGICSTAFF + && !(stats[player]->weapon->type == MAGICSTAFF_SCEPTER) ) // staffs. { attack = 0; output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_MAGICSTAFF; // staffs output.totalAttack = attack; output.attackMaxRange = output.totalAttack; output.attackMinRange = output.totalAttack; - output.proficiency = PRO_SPELLCASTING; + output.proficiency = PRO_LEGACY_MAGIC; } else // tools etc. { attack += Entity::getAttack(players[player]->entity, stats[player], true); output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_TOOL; // tools - if ( stats[player]->weapon->type == TOOL_PICKAXE ) + if ( stats[player]->weapon->type == TOOL_PICKAXE || stats[player]->weapon->type == MAGICSTAFF_SCEPTER ) { output.hoverType = AttackHoverText_t::ATK_HOVER_TYPE_PICKAXE; } @@ -273,12 +336,32 @@ Sint32 displayAttackPower(const int player, AttackHoverText_t& output) } output.totalAttack = attack; output.weaponBonus = 0; // bonus from weapon - output.mainAttributeBonus = statGetSTR(stats[player], entity); // bonus from main attribute + + if ( stats[player]->weapon->type == TOOL_PICKAXE || stats[player]->weapon->type == MAGICSTAFF_SCEPTER ) + { + output.weaponBonus = stats[player]->weapon->weaponGetAttack(stats[player]); // bonus from weapon + } + + //output.mainAttributeBonus = statGetSTR(stats[player], entity); // bonus from main attribute + //if ( stats[player]->weapon->type == MAGICSTAFF_SCEPTER ) + //{ + // output.mainAttributeBonus /= 2; + //} + + Sint32 attr = statGetSTR(stats[player], entity); // bonus from main attribute + Sint32 prevStat = stats[player]->STR; + stats[player]->STR += -attr; + output.mainAttributeBonus = output.totalAttack - Entity::getAttack(players[player]->entity, stats[player], true); + stats[player]->STR = prevStat; + output.proficiencyBonus = 0; // bonus from proficiency - output.equipmentAndEffectBonus = attack - output.mainAttributeBonus - BASE_MELEE_DAMAGE; // bonus from equipment + //output.equipmentAndEffectBonus = attack - output.mainAttributeBonus - BASE_MELEE_DAMAGE; // bonus from equipment output.attackMaxRange = output.totalAttack; output.attackMinRange = output.totalAttack; - output.proficiency = PRO_LOCKPICKING; + if ( stats[player]->weapon->type == MAGICSTAFF_SCEPTER ) + { + output.proficiency = PRO_LEGACY_MAGIC; + } output.totalAttack = output.attackMaxRange - ((output.attackMaxRange - output.attackMinRange) / 2.0); } } diff --git a/src/interface/updatechestinventory.cpp b/src/interface/updatechestinventory.cpp index 5db655a0a..22370db92 100644 --- a/src/interface/updatechestinventory.cpp +++ b/src/interface/updatechestinventory.cpp @@ -30,9 +30,9 @@ int numItemsInChest(const int player) { chestInventory = &chestInv[player]; } - else if (openedChest[player]->children.first && openedChest[player]->children.first->element) + else if ( openedChest[player] ) { - chestInventory = (list_t*)openedChest[player]->children.first->element; + chestInventory = openedChest[player]->getChestInventoryList(); } int i = 0; @@ -68,9 +68,9 @@ void updateChestInventory(const int player) { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( chest_inventory ) diff --git a/src/item_tool.cpp b/src/item_tool.cpp index a356691f7..0ba40c665 100644 --- a/src/item_tool.cpp +++ b/src/item_tool.cpp @@ -26,6 +26,10 @@ void Item::applySkeletonKey(int player, Entity& entity) { + static std::pair> entitiesUnlockedFirstTime; + bool interacted = false; + bool rollDegrade = false; + if ( entity.behavior == &actChest ) { playSoundEntity(&entity, 91, 64); @@ -35,6 +39,7 @@ void Item::applySkeletonKey(int player, Entity& entity) entity.unlockChest(); Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_CHESTS_UNLOCKED, "chest", 1); Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_LOCKPICK_CHESTS_UNLOCK, TOOL_SKELETONKEY, 1); + rollDegrade = true; } else { @@ -42,30 +47,113 @@ void Item::applySkeletonKey(int player, Entity& entity) entity.lockChest(); Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_LOCKPICK_CHESTS_LOCK, TOOL_SKELETONKEY, 1); } + interacted = true; } - else if ( entity.behavior == &actDoor ) + else if ( entity.behavior == &actWallLock ) + { + if ( entity.wallLockState == Entity::WallLockStates::LOCK_NO_KEY ) + { + if ( entity.wallLockPickableSkeletonKey == 1 ) + { + if ( entity.wallLockPower == 1 ) + { + if ( entity.wallLockTurnable == 0 ) + { + // untoggleable + playSoundEntity(&entity, 92, 64); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6429), Language::get(6383 + entity.wallLockMaterial)); + } + else + { + playSoundEntity(&entity, 57, 64); + entity.wallLockPower = 2; // turn off later in actWallLock + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6427), items[TOOL_SKELETONKEY].getIdentifiedName(), + Language::get(6383 + entity.wallLockMaterial)); + interacted = true; + } + } + else if ( entity.wallLockPower == 0 ) + { + playSoundEntity(&entity, 91, 64); + entity.wallLockPower = 3; // turn on later in actWallLock + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6426), items[TOOL_SKELETONKEY].getIdentifiedName(), + Language::get(6383 + entity.wallLockMaterial)); + interacted = true; + rollDegrade = true; + + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_KEYLOCK_SKELETON_KEY, "wall locks", 1); + } + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6425)); + playSoundEntity(&entity, 92, 64); + } + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6423), Language::get(6383 + entity.wallLockMaterial)); + playSoundEntity(&entity, 92, 64); + } + } + else if ( entity.behavior == &actDoor || entity.behavior == &actIronDoor ) { - playSoundEntity(&entity, 91, 64); if ( entity.doorLocked ) { if ( entity.doorDisableLockpicks == 1 ) { - Uint32 color = makeColorRGB(255, 0, 255); - messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(3101)); // disabled. + if ( entity.behavior == &actIronDoor ) + { + Uint32 color = makeColorRGB(255, 255, 255); + playSoundEntity(&entity, 92, 64); + messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(6403)); // disabled. + } + else + { + Uint32 color = makeColorRGB(255, 0, 255); + playSoundEntity(&entity, 92, 64); + messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(3101)); // disabled. + } } else { - messagePlayer(player, MESSAGE_INTERACTION, Language::get(1099)); + playSoundEntity(&entity, 91, 64); + if ( entity.behavior == &actIronDoor ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6415)); + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_DOOR_UNLOCKED, "iron door", 1); + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(1099)); + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_DOOR_UNLOCKED, "door", 1); + } entity.doorLocked = 0; - Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_DOOR_UNLOCKED, "door", 1); Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_LOCKPICK_DOOR_UNLOCK, TOOL_SKELETONKEY, 1); } } else { - messagePlayer(player, MESSAGE_INTERACTION, Language::get(1100)); - entity.doorLocked = 1; - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_LOCKPICK_DOOR_LOCK, TOOL_SKELETONKEY, 1); + if ( entity.doorDisableLockpicks == 1 && entity.behavior == &actIronDoor ) + { + Uint32 color = makeColorRGB(255, 255, 255); + playSoundEntity(&entity, 92, 64); + messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(6403)); // disabled. + } + else + { + playSoundEntity(&entity, 91, 64); + if ( entity.behavior == &actIronDoor ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6416)); + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(1100)); + } + entity.doorLocked = 1; + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_LOCKPICK_DOOR_LOCK, TOOL_SKELETONKEY, 1); + } } } else if ( entity.behavior == &actMonster && entity.getMonsterTypeFromSprite() == MIMIC ) @@ -75,7 +163,7 @@ void Item::applySkeletonKey(int player, Entity& entity) if ( entity.isInertMimic() ) { playSoundEntity(&entity, 91, 64); - if ( myStats->EFFECTS[EFF_MIMIC_LOCKED] ) + if ( myStats->getEffectActive(EFF_MIMIC_LOCKED) ) { messagePlayer(player, MESSAGE_INTERACTION, Language::get(1097)); entity.setEffect(EFF_MIMIC_LOCKED, false, 0, false); @@ -85,14 +173,16 @@ void Item::applySkeletonKey(int player, Entity& entity) messagePlayer(player, MESSAGE_INTERACTION, Language::get(1098)); entity.setEffect(EFF_MIMIC_LOCKED, true, -1, false); } + interacted = true; } else { - if ( myStats->EFFECTS[EFF_MIMIC_LOCKED] ) + if ( myStats->getEffectActive(EFF_MIMIC_LOCKED) ) { playSoundEntity(&entity, 91, 64); messagePlayer(player, MESSAGE_INTERACTION, Language::get(1097)); entity.setEffect(EFF_MIMIC_LOCKED, false, 0, false); + interacted = true; } else { @@ -113,6 +203,7 @@ void Item::applySkeletonKey(int player, Entity& entity) { myStats->monsterMimicLockedBy = players[player]->entity ? players[player]->entity->getUID() : 0; } + interacted = true; } } } @@ -122,6 +213,57 @@ void Item::applySkeletonKey(int player, Entity& entity) { messagePlayer(player, MESSAGE_INTERACTION, Language::get(1101), getName()); } + + if ( entitiesUnlockedFirstTime.first != currentlevel ) + { + entitiesUnlockedFirstTime.second.clear(); + } + entitiesUnlockedFirstTime.first = currentlevel; + if ( interacted ) + { + auto find = entitiesUnlockedFirstTime.second.find(entity.getUID()); + if ( find == entitiesUnlockedFirstTime.second.end() ) + { + if ( rollDegrade ) + { + if ( stats[player]->weapon && stats[player]->weapon->type == TOOL_SKELETONKEY ) + { + if ( local_rng.rand() % 1 == 0 && !(players[player]->entity && players[player]->entity->spellEffectPreserveItem(stats[player]->weapon)) ) + { + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + if ( count > 1 ) + { + newItem(type, status, beatitude, count - 1, appearance, identified, &stats[player]->inventory); + } + } + stats[player]->weapon->count = 1; + stats[player]->weapon->status = static_cast(stats[player]->weapon->status - 1); + if ( status != BROKEN ) + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(6960)); + } + else + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(6961)); + } + if ( player > 0 && multiplayer == SERVER ) + { + strcpy((char*)(net_packet->data), "ARMR"); + net_packet->data[4] = 5; + net_packet->data[5] = stats[player]->weapon->status; + SDLNet_Write16((int)stats[player]->weapon->type, &net_packet->data[6]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + } + } + } + entitiesUnlockedFirstTime.second.insert(entity.getUID()); + } + } } @@ -240,7 +382,10 @@ void Item::applyLockpick(int player, Entity& entity) { if ( stats[player]->getProficiency(PRO_LOCKPICKING) < SKILL_LEVEL_EXPERT ) { - players[player]->entity->increaseSkill(PRO_LOCKPICKING); + if ( players[player]->entity ) + { + players[player]->entity->increaseSkill(PRO_LOCKPICKING); + } } else { @@ -255,9 +400,8 @@ void Item::applyLockpick(int player, Entity& entity) { int metalscrap = 5 + local_rng.rand() % 6; int magicscrap = 5 + local_rng.rand() % 11; - if ( entity.children.first ) { - list_t* inventory = static_cast(entity.children.first->element); + list_t* inventory = entity.getChestInventoryList(); if ( inventory ) { newItem(TOOL_METAL_SCRAP, DECREPIT, 0, metalscrap, 0, true, inventory); @@ -282,7 +426,10 @@ void Item::applyLockpick(int player, Entity& entity) { if ( local_rng.rand() % 10 == 0 ) { - players[player]->entity->increaseSkill(PRO_LOCKPICKING); + if ( players[player]->entity ) + { + players[player]->entity->increaseSkill(PRO_LOCKPICKING); + } tryDegradeLockpick = false; } } @@ -296,9 +443,9 @@ void Item::applyLockpick(int player, Entity& entity) } } - if ( tryDegradeLockpick ) + if ( tryDegradeLockpick && stats[player]->weapon && stats[player]->weapon->type == TOOL_LOCKPICK ) { - if ( local_rng.rand() % 5 == 0 ) + if ( local_rng.rand() % 5 == 0 && !(players[player]->entity && players[player]->entity->spellEffectPreserveItem(stats[player]->weapon)) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -322,9 +469,10 @@ void Item::applyLockpick(int player, Entity& entity) strcpy((char*) (net_packet->data), "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = stats[player]->weapon->status; + SDLNet_Write16((int)stats[player]->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -336,7 +484,7 @@ void Item::applyLockpick(int player, Entity& entity) messagePlayer(player, MESSAGE_INTERACTION, Language::get(1105)); } } - else if ( entity.behavior == &actDoor ) + else if ( entity.behavior == &actDoor || entity.behavior == &actIronDoor ) { if ( entity.doorLocked ) { @@ -352,8 +500,18 @@ void Item::applyLockpick(int player, Entity& entity) if ( entity.doorDisableLockpicks == 1 ) { - Uint32 color = makeColorRGB(255, 0, 255); - messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(3101)); // disabled. + if ( entity.behavior == &actIronDoor ) + { + Uint32 color = makeColorRGB(255, 255, 255); + playSoundEntity(&entity, 92, 64); + messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(6403)); // disabled. + } + else + { + Uint32 color = makeColorRGB(255, 0, 255); + playSoundEntity(&entity, 92, 64); + messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(3101)); // disabled. + } } else if ( capstoneUnlocked || stats[player]->getModifiedProficiency(PRO_LOCKPICKING) > local_rng.rand() % 200 @@ -361,21 +519,33 @@ void Item::applyLockpick(int player, Entity& entity) { //Unlock door. playSoundEntity(&entity, 91, 64); - messagePlayer(player, MESSAGE_INTERACTION, Language::get(1099)); - Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_DOOR_UNLOCKED, "door", 1); + if ( entity.behavior == &actIronDoor ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6415)); + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_DOOR_UNLOCKED, "iron door", 1); + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(1099)); + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_DOOR_UNLOCKED, "door", 1); + } Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_LOCKPICK_DOOR_UNLOCK, TOOL_LOCKPICK, 1); entity.doorLocked = 0; if ( !entity.doorPreventLockpickExploit ) { if ( stats[player]->getProficiency(PRO_LOCKPICKING) < SKILL_LEVEL_SKILLED ) { - players[player]->entity->increaseSkill(PRO_LOCKPICKING); + if ( players[player]->entity ) + { + players[player]->entity->increaseSkill(PRO_LOCKPICKING); + } } else { if ( local_rng.rand() % 20 == 0 ) { - messagePlayer(player, MESSAGE_INTERACTION, Language::get(3689), Language::get(674)); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(3689), + entity.behavior == &actIronDoor ? Language::get(6414) : Language::get(674)); } } } @@ -385,7 +555,14 @@ void Item::applyLockpick(int player, Entity& entity) { //Failed to unlock door. playSoundEntity(&entity, 92, 64); - messagePlayer(player, MESSAGE_INTERACTION, Language::get(1106)); + if ( entity.behavior == &actIronDoor ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6417)); + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(1106)); + } bool tryDegradeLockpick = true; if ( !entity.doorPreventLockpickExploit ) { @@ -393,7 +570,10 @@ void Item::applyLockpick(int player, Entity& entity) { if ( local_rng.rand() % 10 == 0 ) { - players[player]->entity->increaseSkill(PRO_LOCKPICKING); + if ( players[player]->entity ) + { + players[player]->entity->increaseSkill(PRO_LOCKPICKING); + } tryDegradeLockpick = false; } } @@ -401,15 +581,16 @@ void Item::applyLockpick(int player, Entity& entity) { if ( local_rng.rand() % 20 == 0 ) { - messagePlayer(player, MESSAGE_INTERACTION, Language::get(3689), Language::get(674)); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(3689), + entity.behavior == &actIronDoor ? Language::get(6414) : Language::get(674)); tryDegradeLockpick = false; } } } - if ( tryDegradeLockpick ) + if ( tryDegradeLockpick && stats[player]->weapon && stats[player]->weapon->type == TOOL_LOCKPICK ) { - if ( local_rng.rand() % 5 == 0 ) + if ( local_rng.rand() % 5 == 0 && !(players[player]->entity && players[player]->entity->spellEffectPreserveItem(stats[player]->weapon)) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -433,9 +614,10 @@ void Item::applyLockpick(int player, Entity& entity) strcpy((char*) (net_packet->data), "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = stats[player]->weapon->status; + SDLNet_Write16((int)stats[player]->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -444,7 +626,202 @@ void Item::applyLockpick(int player, Entity& entity) } else { - messagePlayer(player, MESSAGE_INTERACTION, Language::get(1107)); + if ( entity.behavior == &actIronDoor ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6418)); + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(1107)); + } + } + } + else if ( entity.behavior == &actWallLock ) + { + if ( entity.wallLockState != Entity::WallLockStates::LOCK_NO_KEY ) + { + playSoundEntity(&entity, 92, 64); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6423), Language::get(6383 + entity.wallLockMaterial)); + } + else if ( entity.wallLockPickable == -1 ) + { + // can't be picked + playSoundEntity(&entity, 92, 64); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6424), Language::get(6383 + entity.wallLockMaterial)); + } + else if ( entity.wallLockPower == 0 ) + { + // 3-17 damage on lockpick depending on skill + // 0 skill is 3 damage + // 20 skill is 4-5 damage + // 60 skill is 6-11 damage + // 100 skill is 8-17 damage + bool wasLocked = entity.wallLockPickHealth > 0; + int lockpickDamageToLock = 3 + stats[player]->getModifiedProficiency(PRO_LOCKPICKING) / 20 + + local_rng.rand() % std::max(1, stats[player]->getModifiedProficiency(PRO_LOCKPICKING) / 10); + + int skillLVL = stats[player]->getModifiedProficiency(PRO_LOCKPICKING) + statGetPER(stats[player], players[player]->entity); + if ( wasLocked && (skillLVL < entity.wallLockPickable) ) + { + // unable to lockpick + playSoundEntity(&entity, 92, 64); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6422), Language::get(6383 + entity.wallLockMaterial)); + } + else + { + entity.wallLockPickHealth = std::max(0, entity.wallLockPickHealth - lockpickDamageToLock); + bool unlockedFromLockpickHealth = (entity.wallLockPickHealth == 0); + + if ( capstoneUnlocked + || stats[player]->getModifiedProficiency(PRO_LOCKPICKING) > local_rng.rand() % 200 + || unlockedFromLockpickHealth ) + { + entity.wallLockPickHealth = 0; + + //Unlock lock. + playSoundEntity(&entity, 91, 64); + + if ( !entity.wallLockPreventLockpickExploit ) + { + if ( players[player]->entity ) + { + players[player]->entity->increaseSkill(PRO_LOCKPICKING); + } + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_KEYLOCK_PICKED, "wall locks", 1); + steamStatisticUpdateClient(player, STEAM_STAT_CALL_LOCKSMITH, STEAM_STAT_INT, 1); + } + entity.wallLockPreventLockpickExploit = 1; + + // consume the lockpick + /*if ( player >= 0 && players[player]->isLocalPlayer() ) + { + if ( count > 1 ) + { + newItem(type, status, beatitude, count - 1, appearance, identified, &stats[player]->inventory); + } + } + stats[player]->weapon->count = 1; + stats[player]->weapon->status = BROKEN; + if ( status != BROKEN ) + { + messagePlayer(player, MESSAGE_INTERACTION | MESSAGE_EQUIPMENT, Language::get(1103)); + } + else + { + messagePlayer(player, MESSAGE_INTERACTION | MESSAGE_EQUIPMENT, Language::get(1104)); + } + if ( player > 0 && multiplayer == SERVER ) + { + strcpy((char*)(net_packet->data), "ARMR"); + net_packet->data[4] = 5; + net_packet->data[5] = stats[player]->weapon->status; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + }*/ + + entity.wallLockPower = 3; // turn on later in actWallLock + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6426), items[TOOL_LOCKPICK].getIdentifiedName(), + Language::get(6383 + entity.wallLockMaterial)); + } + else + { + //Failed to unlock lock. + playSoundEntity(&entity, 92, 64); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6428), Language::get(6383 + entity.wallLockMaterial)); + + bool tryDegradeLockpick = true; + if ( !entity.wallLockPreventLockpickExploit ) + { + int skillIncreaseMinimum = std::min(100, std::max(0, entity.wallLockPickable + 20)); + if ( stats[player]->getProficiency(PRO_LOCKPICKING) < skillIncreaseMinimum ) + { + if ( local_rng.rand() % 10 == 0 ) + { + if ( players[player]->entity ) + { + players[player]->entity->increaseSkill(PRO_LOCKPICKING); + } + tryDegradeLockpick = false; + } + } + else + { + if ( local_rng.rand() % 20 == 0 ) + { + /*messagePlayer(player, MESSAGE_INTERACTION, Language::get(3689), + entity.behavior == &actIronDoor ? Language::get(6414) : Language::get(674));*/ + tryDegradeLockpick = false; + } + } + } + + if ( tryDegradeLockpick && stats[player]->weapon && stats[player]->weapon->type == TOOL_LOCKPICK ) + { + if ( local_rng.rand() % 5 == 0 && !(players[player]->entity && players[player]->entity->spellEffectPreserveItem(stats[player]->weapon)) ) + { + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + if ( count > 1 ) + { + newItem(type, status, beatitude, count - 1, appearance, identified, &stats[player]->inventory); + } + } + stats[player]->weapon->count = 1; + stats[player]->weapon->status = static_cast(stats[player]->weapon->status - 1); + if ( status != BROKEN ) + { + messagePlayer(player, MESSAGE_INTERACTION | MESSAGE_EQUIPMENT, Language::get(1103)); + } + else + { + messagePlayer(player, MESSAGE_INTERACTION | MESSAGE_EQUIPMENT, Language::get(1104)); + } + if ( player > 0 && multiplayer == SERVER ) + { + strcpy((char*)(net_packet->data), "ARMR"); + net_packet->data[4] = 5; + net_packet->data[5] = stats[player]->weapon->status; + SDLNet_Write16((int)stats[player]->weapon->type, &net_packet->data[6]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + } + } + } + } + } + else + { + if ( entity.wallLockPower == 1 ) + { + int skillLVL = stats[player]->getModifiedProficiency(PRO_LOCKPICKING) + statGetPER(stats[player], players[player]->entity); + if ( skillLVL < entity.wallLockPickable ) + { + // unable to lockpick + playSoundEntity(&entity, 92, 64); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6422), Language::get(6383 + entity.wallLockMaterial)); + } + else + { + if ( entity.wallLockTurnable == 0 ) + { + // untoggleable + playSoundEntity(&entity, 92, 64); + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6429), Language::get(6383 + entity.wallLockMaterial)); + } + else + { + playSoundEntity(&entity, 57, 64); + entity.wallLockPower = 2; // turn off later in actWallLock + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6427), items[TOOL_LOCKPICK].getIdentifiedName(), + Language::get(6383 + entity.wallLockMaterial)); + } + } + } } } else if ( entity.behavior == &actMonster ) @@ -452,13 +829,13 @@ void Item::applyLockpick(int player, Entity& entity) Stat* myStats = entity.getStats(); if ( myStats && entity.isInertMimic() ) { - if ( myStats->EFFECTS[EFF_MIMIC_LOCKED] && local_rng.rand() % 4 > 0 ) + if ( myStats->getEffectActive(EFF_MIMIC_LOCKED) && local_rng.rand() % 4 > 0 ) { //Failed to unlock mimic playSoundEntity(&entity, 92, 64); messagePlayer(player, MESSAGE_INTERACTION, Language::get(1102)); } - else if ( players[player] && players[player]->entity && entity.disturbMimic(players[player]->entity, false, false) ) + else if ( players[player] && players[player]->entity && entity.disturbMimic(players[player]->entity, false, true) ) { playSoundEntity(&entity, 91, 64); messagePlayer(player, MESSAGE_INTERACTION, Language::get(6081)); @@ -466,7 +843,7 @@ void Item::applyLockpick(int player, Entity& entity) } else if ( myStats && myStats->type == AUTOMATON && entity.monsterSpecialState == 0 - && !myStats->EFFECTS[EFF_CONFUSED] ) + && !myStats->getEffectActive(EFF_CONFUSED) ) { if ( players[player] && players[player]->entity ) { @@ -485,7 +862,7 @@ void Item::applyLockpick(int player, Entity& entity) entity.monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_AUTOMATON_MALFUNCTION; serverUpdateEntitySkill(&entity, 33); - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = -1; playSoundEntity(&entity, 76, 128); messagePlayer(player, MESSAGE_COMBAT, Language::get(2527), getMonsterLocalizedName(myStats->type).c_str()); @@ -523,9 +900,8 @@ void Item::applyLockpick(int player, Entity& entity) else { messagePlayer(player, MESSAGE_COMBAT, Language::get(2526), getMonsterLocalizedName(myStats->type).c_str()); - myStats->EFFECTS[EFF_CONFUSED] = true; - myStats->EFFECTS_TIMERS[EFF_CONFUSED] = -1; - myStats->EFFECTS[EFF_PARALYZED] = true; + entity.setEffect(EFF_CONFUSED, Uint8(MAXPLAYERS + 1), -1, true, true, true, true); + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 25; playSoundEntity(&entity, 263, 128); spawnMagicEffectParticles(entity.x, entity.y, entity.z, 170); @@ -536,7 +912,8 @@ void Item::applyLockpick(int player, Entity& entity) players[player]->entity->increaseSkill(PRO_LOCKPICKING); } } - if ( local_rng.rand() % 2 == 0 ) + if ( local_rng.rand() % 2 == 0 && stats[player]->weapon && stats[player]->weapon->type == TOOL_LOCKPICK + && !(players[player]->entity && players[player]->entity->spellEffectPreserveItem(stats[player]->weapon)) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -560,9 +937,10 @@ void Item::applyLockpick(int player, Entity& entity) strcpy((char*)(net_packet->data), "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = stats[player]->weapon->status; + SDLNet_Write16((int)stats[player]->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -890,6 +1268,7 @@ void Item::applyEmptyPotion(int player, Entity& entity) { if ( player > 0 && !splitscreen ) { + Entity* oldSelected = client_selected[player]; client_selected[player] = &entity; bool oldInRange = inrange[player]; inrange[player] = true; @@ -897,9 +1276,11 @@ void Item::applyEmptyPotion(int player, Entity& entity) actSink(&entity); entity.skill[8] = 0; inrange[player] = oldInRange; + client_selected[player] = oldSelected; } else if ( player == 0 || (player > 0 && splitscreen) ) { + Entity* oldSelected = selectedEntity[player]; selectedEntity[player] = &entity; bool oldInRange = inrange[player]; inrange[player] = true; @@ -907,6 +1288,7 @@ void Item::applyEmptyPotion(int player, Entity& entity) actSink(&entity); entity.skill[8] = 0; inrange[player] = oldInRange; + selectedEntity[player] = oldSelected; } } else if ( entity.skill[0] > 1 ) @@ -1504,6 +1886,138 @@ void Item::applyBomb(Entity* parent, ItemType type, ItemBombPlacement placement, } } +void Item::applyDuck(Uint32 parentUid, real_t x, real_t y, Entity* hitentity, bool onLevelRespawn) +{ + bool tryExactLocation = true; + + if ( !onLevelRespawn ) + { + Entity* testEntity = newEntity(2225, 1, map.entities, nullptr); + if ( testEntity ) + { + testEntity->flags[PASSABLE] = true; + testEntity->x = x; + testEntity->y = y; + testEntity->behavior = &actMonster; + testEntity->sizex = 4; + testEntity->sizey = 4; + if ( !entityInsideSomething(testEntity) ) + { + for ( int i = -1; i <= 1; ++i ) + { + for ( int j = -1; j <= 1; ++j ) + { + int ix = static_cast(x / 16); + int iy = static_cast(y / 16); + if ( entityInsideTile(testEntity, ix + i, iy + j, OBSTACLELAYER) ) // check not clipping in surrounding walls + { + tryExactLocation = false; + break; + } + } + if ( !tryExactLocation ) + { + break; + } + } + } + + list_RemoveNode(testEntity->mynode); + } + } + + Entity* summon = nullptr; + if ( tryExactLocation ) + { + summon = summonMonster(DUCK_SMALL, x, y, true); + } + if ( !summon ) + { + summon = summonMonster(DUCK_SMALL, floor(x / 16) * 16 + 8, floor(y / 16) * 16 + 8, false); + } + if ( summon ) + { + Stat* summonedStats = summon->getStats(); + Entity* parent = uidToEntity(parentUid); + if ( parent && parent->behavior == &actPlayer ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_GADGET_DEPLOYED, this->type, 1); + } + } + + if ( summonedStats ) + { + //summon->yaw = thrown->yaw; + summon->monsterSpecialState = DUCK_INERT; + serverUpdateEntitySkill(summon, 33); + playSoundPos(summon->x, summon->y, 794 + local_rng.rand() % 2, 128); + //if ( forceFollower(*parent, *summon) ) + //{ + // if ( parent->behavior == &actPlayer ) + // { + // summon->monsterAllyIndex = parent->skill[2]; + // if ( multiplayer == SERVER ) + // { + // serverUpdateEntitySkill(summon, 42); // update monsterAllyIndex for clients. + // } + // } + // // change the color of the hit entity. + // summon->flags[USERFLAG2] = true; + // serverUpdateEntityFlag(summon, USERFLAG2); + //} + + if ( hitentity && (hitentity->behavior == &actMonster || hitentity->behavior == &actPlayer) ) + { + if ( parent ) + { + if ( parent->checkEnemy(hitentity) ) + { + summon->monsterTarget = hitentity->getUID(); + } + } + else + { + summon->monsterTarget = hitentity->getUID(); + } + playSoundPos(summon->x, summon->y, 786 + local_rng.rand() % 3, 128); + } + else + { + playSoundPos(summon->x, summon->y, 789 + local_rng.rand() % 5, 128); + } + int appearance = std::max(0, static_cast(this->appearance % items[TOOL_DUCK].variations)); + summonedStats->setAttribute("duck_type", std::to_string(appearance)); + if ( onLevelRespawn ) + { + summonedStats->setAttribute("duck_time", std::to_string(1 * TICKS_PER_SECOND)); + } + else + { + if ( status == BROKEN ) + { + summonedStats->setAttribute("duck_time", std::to_string(1 * TICKS_PER_SECOND)); + summonedStats->setAttribute("duck_run", "1"); + } + else + { + summonedStats->setAttribute("duck_time", std::to_string(15 * TICKS_PER_SECOND)); + } + } + summonedStats->setAttribute("duck_bless", std::to_string(beatitude)); + summonedStats->setAttribute("skip_obituary", "1"); + summonedStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; + int playerOwner = this->getDuckPlayer(); + if ( playerOwner >= 0 && playerOwner < MAXPLAYERS ) + { + summonedStats->leader_uid = achievementObserver.playerUids[playerOwner]; + summon->parent = achievementObserver.playerUids[playerOwner]; + } + } + } +} + void Item::applyTinkeringCreation(Entity* parent, Entity* thrown) { if ( !thrown ) @@ -1586,7 +2100,7 @@ void Item::applyTinkeringCreation(Entity* parent, Entity* thrown) summon->x = thrown->x; summon->y = thrown->y; } - summonedStats->EFFECTS[EFF_STUNNED] = true; + summonedStats->setEffectActive(EFF_STUNNED, 1); summonedStats->EFFECTS_TIMERS[EFF_STUNNED] = 30; playSoundEntity(summon, 453 + local_rng.rand() % 2, 192); } diff --git a/src/item_usage_funcs.cpp b/src/item_usage_funcs.cpp index bfc00261a..2669656cb 100644 --- a/src/item_usage_funcs.cpp +++ b/src/item_usage_funcs.cpp @@ -28,6 +28,126 @@ #include "mod_tools.hpp" #include "scrolls.hpp" +bool potionUseAbundanceEffect(Item* item, Entity* entity, Entity* usedBy) +{ + bool result = false; + if ( item && itemCategory(item) == POTION ) + { + if ( entity == usedBy && entity->behavior == &actPlayer ) + { + int player = entity->skill[2]; + if ( players[player]->isLocalPlayer() ) + { + if ( stats[player]->getEffectActive(EFF_GREATER_ABUNDANCE) ) + { + if ( !itemIsEquipped(item, player) ) + { + int chance = getSpellDamageFromID(SPELL_GREATER_ABUNDANCE, entity, nullptr, entity); + int maxchance = getSpellDamageSecondaryFromID(SPELL_GREATER_ABUNDANCE, entity, nullptr, entity); + chance = std::min(chance, maxchance); + if ( local_rng.rand() % 100 < chance ) + { + item->count++; + messagePlayerColor(player, MESSAGE_INTERACTION, makeColorRGB(0, 255, 0), Language::get(6652), item->getName()); + result = true; + } + } + } + } + } + } + return result; +} + +bool foodUseAbundanceEffect(Item* item, int player) +{ + bool result = false; + if ( player >= 0 ) + { + if ( players[player]->isLocalPlayer() ) + { + bool effect = false; + if ( stats[player]->getEffectActive(EFF_GREATER_ABUNDANCE) ) + { + if ( !itemIsEquipped(item, player) ) + { + int chance = getSpellDamageFromID(SPELL_GREATER_ABUNDANCE, players[player]->entity, stats[player], players[player]->entity); + int maxchance = getSpellDamageSecondaryFromID(SPELL_GREATER_ABUNDANCE, players[player]->entity, stats[player], players[player]->entity); + chance = std::min(chance, maxchance); + if ( local_rng.rand() % 100 < chance ) + { + bool hasCost = false; + if ( auto spell = getSpellFromID(SPELL_GREATER_ABUNDANCE) ) + { + real_t costPercent = getSpellEffectDurationSecondaryFromID(SPELL_GREATER_ABUNDANCE, players[player]->entity, stats[player], players[player]->entity) / 100.0; + if ( costPercent > 0.01 ) + { + int cost = std::max(1.0, spell->mana * costPercent); + if ( stats[player]->MP >= cost ) + { + hasCost = true; + } + } + else + { + hasCost = true; + } + } + if ( hasCost ) + { + item->count++; + effect = true; + messagePlayerColor(player, MESSAGE_INTERACTION, makeColorRGB(0, 255, 0), Language::get(6652), item->getName()); + result = true; + + magicOnSpellCastEvent(players[player]->entity, players[player]->entity, nullptr, SPELL_GREATER_ABUNDANCE, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + } + if ( !effect && stats[player]->getEffectActive(EFF_ABUNDANCE) ) + { + if ( !itemIsEquipped(item, player) ) + { + int chance = getSpellDamageFromID(SPELL_ABUNDANCE, players[player]->entity, stats[player], players[player]->entity); + int maxchance = getSpellDamageSecondaryFromID(SPELL_ABUNDANCE, players[player]->entity, stats[player], players[player]->entity); + chance = std::min(chance, maxchance); + if ( local_rng.rand() % 100 < chance ) + { + bool hasCost = false; + if ( auto spell = getSpellFromID(SPELL_ABUNDANCE) ) + { + real_t costPercent = getSpellEffectDurationSecondaryFromID(SPELL_ABUNDANCE, players[player]->entity, stats[player], players[player]->entity) / 100.0; + if ( costPercent > 0.01 ) + { + int cost = std::max(1.0, spell->mana * costPercent); + if ( stats[player]->MP >= cost ) + { + hasCost = true; + } + } + else + { + hasCost = true; + } + } + if ( hasCost ) + { + item->count++; + effect = true; + messagePlayerColor(player, MESSAGE_INTERACTION, makeColorRGB(0, 255, 0), Language::get(6653), item->getName()); + result = true; + + magicOnSpellCastEvent(players[player]->entity, players[player]->entity, nullptr, SPELL_ABUNDANCE, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + } + } + } + return result; +} + bool item_PotionWater(Item*& item, Entity* entity, Entity* usedBy) { if ( !entity ) @@ -72,7 +192,7 @@ bool item_PotionWater(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -137,16 +257,16 @@ bool item_PotionWater(Item*& item, Entity* entity, Entity* usedBy) } if ( player >= 0 && player < MAXPLAYERS ) { - if ( stats && stats->EFFECTS[EFF_POLYMORPH] ) + if ( stats && stats->getEffectActive(EFF_POLYMORPH) ) { - if ( stats->EFFECTS[EFF_POLYMORPH] ) + if ( stats->getEffectActive(EFF_POLYMORPH) ) { entity->setEffect(EFF_POLYMORPH, false, 0, true); entity->effectPolymorph = 0; serverUpdateEntitySkill(entity, 50); messagePlayer(player, MESSAGE_STATUS, Language::get(3192)); - if ( !stats->EFFECTS[EFF_SHAPESHIFT] ) + if ( !stats->getEffectActive(EFF_SHAPESHIFT) ) { messagePlayer(player, MESSAGE_STATUS, Language::get(3185)); } @@ -172,9 +292,41 @@ bool item_PotionWater(Item*& item, Entity* entity, Entity* usedBy) players[player]->entity->modMP(mpAmount); //Raise temperature because steam. serverUpdateHunger(player); } + + if ( stats->type == DRYAD ) + { + if ( auto effectStrength = stats->getEffectActive(EFF_GROWTH) ) + { + int chance = 10; + if ( (stats->type == DRYAD && stats->sex == FEMALE) ) + { + chance = 20; + } + if ( item->beatitude >= 0 ) + { + chance += 10 * item->beatitude; + if ( players[player]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_GROWTH, chance) ) + { + if ( stats->getEffectActive(EFF_GROWTH) < 4 ) + { + players[player]->entity->setEffect(EFF_GROWTH, (Uint8)(std::min(4, effectStrength + 1)), 15 * TICKS_PER_SECOND, false); + messagePlayerColor(player, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6924)); + } + } + } + } + } } if ( player >= 0 && !players[player]->isLocalPlayer() ) { + potionUseAbundanceEffect(item, entity, usedBy); + consumeItem(item, player); + return true; + } + + if ( entity->behavior == &actMonster ) + { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -352,6 +504,7 @@ bool item_PotionWater(Item*& item, Entity* entity, Entity* usedBy) sendPacketSafe(net_sock, -1, net_packet, 0); //messagePlayer(player, "sent server: %d, %d, %d", net_packet->data[4], net_packet->data[5], net_packet->data[6]); } + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -368,6 +521,7 @@ bool item_PotionWater(Item*& item, Entity* entity, Entity* usedBy) } if ( items == 0 ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -399,6 +553,7 @@ bool item_PotionWater(Item*& item, Entity* entity, Entity* usedBy) } } } + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -446,7 +601,7 @@ bool item_PotionBooze(Item*& item, Entity* entity, Entity* usedBy, bool shouldCo return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -455,15 +610,17 @@ bool item_PotionBooze(Item*& item, Entity* entity, Entity* usedBy, bool shouldCo } return false; } + if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } messagePlayer(player, MESSAGE_WORLD, Language::get(758)); messagePlayer(player, MESSAGE_STATUS, Language::get(759)); - stats->EFFECTS[EFF_DRUNK] = true; + stats->setEffectActive(EFF_DRUNK, 1); if ( player >= 0 ) { if ( stats->type == GOATMAN ) @@ -475,7 +632,7 @@ bool item_PotionBooze(Item*& item, Entity* entity, Entity* usedBy, bool shouldCo stats->EFFECTS_TIMERS[EFF_DRUNK] = item->potionGetEffectDurationRandom(entity, stats); stats->EFFECTS_TIMERS[EFF_DRUNK] = std::max(300, stats->EFFECTS_TIMERS[EFF_DRUNK] - (entity->getPER() + entity->getCON()) * 40); } - if ( stats->EFFECTS[EFF_WITHDRAWAL] ) + if ( stats->getEffectActive(EFF_WITHDRAWAL) ) { int hangoverReliefDuration = EFFECT_WITHDRAWAL_BASE_TIME; // 8 minutes switch ( local_rng.rand() % 3 ) @@ -548,6 +705,7 @@ bool item_PotionBooze(Item*& item, Entity* entity, Entity* usedBy, bool shouldCo spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); if ( shouldConsumeItem ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -597,7 +755,7 @@ bool item_PotionJuice(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -608,6 +766,7 @@ bool item_PotionJuice(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -618,7 +777,7 @@ bool item_PotionJuice(Item*& item, Entity* entity, Entity* usedBy) messagePlayer(player, MESSAGE_HINT, Language::get(2900)); messagePlayer(player, MESSAGE_WORLD, Language::get(758)); messagePlayer(player, MESSAGE_HINT, Language::get(759)); - stats->EFFECTS[EFF_DRUNK] = true; + stats->setEffectActive(EFF_DRUNK, 1); if ( player >= 0 ) { if ( stats->type == GOATMAN ) @@ -630,7 +789,7 @@ bool item_PotionJuice(Item*& item, Entity* entity, Entity* usedBy) stats->EFFECTS_TIMERS[EFF_DRUNK] = item->potionGetCursedEffectDurationRandom(entity, stats); stats->EFFECTS_TIMERS[EFF_DRUNK] = std::max(300, stats->EFFECTS_TIMERS[EFF_DRUNK] - (entity->getPER() + entity->getCON()) * 40); } - if ( stats->EFFECTS[EFF_WITHDRAWAL] ) + if ( stats->getEffectActive(EFF_WITHDRAWAL) ) { int hangoverReliefDuration = EFFECT_WITHDRAWAL_BASE_TIME; // 8 minutes switch ( local_rng.rand() % 3 ) @@ -735,6 +894,7 @@ bool item_PotionJuice(Item*& item, Entity* entity, Entity* usedBy) playSoundEntity(entity, 52, 64); playSoundEntity(entity, 168, 128); spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -789,7 +949,7 @@ bool item_PotionSickness(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -798,12 +958,13 @@ bool item_PotionSickness(Item*& item, Entity* entity, Entity* usedBy) } return false; } - if ( multiplayer == CLIENT || player == 0 ) + if ( multiplayer == CLIENT || (player >= 0 && players[player]->isLocalPlayer()) ) { camera_shakex += .1; camera_shakey += 10; if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -822,7 +983,7 @@ bool item_PotionSickness(Item*& item, Entity* entity, Entity* usedBy) messagePlayer(player, MESSAGE_HINT, Language::get(761)); int oldHP = stats->HP; entity->modHP(-damage); - stats->EFFECTS[EFF_POISONED] = true; + stats->setEffectActive(EFF_POISONED, 1); if ( usedBy && usedBy != entity ) { Stat* usedByStats = usedBy->getStats(); @@ -850,10 +1011,113 @@ bool item_PotionSickness(Item*& item, Entity* entity, Entity* usedBy) // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } +bool item_PotionGrease(Item*& item, Entity* entity, Entity* usedBy) +{ + if ( !entity ) + { + return false; + } + + /*int skillLVL = 0; + if ( multiplayer != CLIENT && usedBy && usedBy->behavior == &actPlayer ) + { + Stat* usedByStats = usedBy->getStats(); + if ( usedByStats ) + { + skillLVL = usedByStats->getModifiedProficiency(PRO_ALCHEMY) / 20; + } + }*/ + + int player = -1; + Stat* stats; + + if ( entity->behavior == &actPlayer ) + { + player = entity->skill[2]; + } + stats = entity->getStats(); + if ( !stats ) + { + return false; + } + + /*if ( stats->amulet != NULL ) + { + if ( stats->amulet->type == AMULET_STRANGULATION + && stats->type != SKELETON ) + { + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + messagePlayer(player, MESSAGE_HINT, Language::get(750)); + playSoundPlayer(player, 90, 64); + } + return false; + } + } + if ( stats->getEffectActive(EFF_VOMITING) ) + { + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + messagePlayer(player, MESSAGE_HINT, Language::get(751)); + playSoundPlayer(player, 90, 64); + } + return false; + }*/ + //if ( multiplayer == CLIENT ) + //{ + // //potionUseAbundanceEffect(item, entity, usedBy); + // consumeItem(item, player); + // return true; + //} + + //messagePlayer(player, MESSAGE_HINT, Language::get(6536)); + int duration = 0; + if ( player >= 0 ) + { + duration = std::max(300, item->potionGetEffectDurationRandom(entity, stats)); + } + else + { + duration = item->potionGetEffectDurationRandom(entity, stats); + } + if ( entity->setEffect(EFF_MAGIC_GREASE, true, duration, true) ) + { + entity->setEffect(EFF_GREASY, true, 5 * TICKS_PER_SECOND, false); + if ( usedBy && entity != usedBy ) + { + if ( usedBy->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(usedBy->skill[2], color, *stats, Language::get(6244), Language::get(6243), MSG_COMBAT); + } + if ( entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(entity->skill[2], MESSAGE_COMBAT, color, Language::get(6236)); + } + } + } + else + { + /*if ( usedBy && entity != usedBy && usedBy->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerMonsterEvent(usedBy->skill[2], color, *stats, Language::get(4320), Language::get(4321), MSG_COMBAT); + }*/ + } + + // play drink sound + //playSoundEntity(entity, 52, 64); + //potionUseAbundanceEffect(item, entity, usedBy); + /*consumeItem(item, player);*/ + return true; +} + bool item_PotionConfusion(Item*& item, Entity* entity, Entity* usedBy) { if (!entity) @@ -897,7 +1161,7 @@ bool item_PotionConfusion(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -908,6 +1172,7 @@ bool item_PotionConfusion(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -922,7 +1187,19 @@ bool item_PotionConfusion(Item*& item, Entity* entity, Entity* usedBy) { duration = item->potionGetEffectDurationRandom(entity, stats); } - if ( entity->setEffect(EFF_CONFUSED, true, duration, false) ) + Uint8 effectStrength = MAXPLAYERS + 1; + if ( entity != usedBy ) + { + if ( usedBy && usedBy->behavior == &actPlayer ) + { + effectStrength = usedBy->skill[2] + 1; + } + else if ( usedBy && usedBy->monsterAllyGetPlayerLeader() ) + { + effectStrength = usedBy->monsterAllyGetPlayerLeader()->skill[2] + 1; + } + } + if ( entity->setEffect(EFF_CONFUSED, effectStrength, duration, true, true, true) ) { if ( entity->behavior == &actMonster ) { @@ -945,6 +1222,7 @@ bool item_PotionConfusion(Item*& item, Entity* entity, Entity* usedBy) // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -993,7 +1271,7 @@ bool item_PotionCureAilment(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1013,6 +1291,7 @@ bool item_PotionCureAilment(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1026,9 +1305,9 @@ bool item_PotionCureAilment(Item*& item, Entity* entity, Entity* usedBy) { if ( stats->statusEffectRemovedByCureAilment(c, entity) ) { - if ( stats->EFFECTS[c] ) + if ( stats->getEffectActive(c) ) { - stats->EFFECTS[c] = false; + stats->clearEffect(c); if ( stats->EFFECTS_TIMERS[c] > 0 ) { stats->EFFECTS_TIMERS[c] = 1; @@ -1038,7 +1317,7 @@ bool item_PotionCureAilment(Item*& item, Entity* entity, Entity* usedBy) } } - if ( stats->EFFECTS[EFF_WITHDRAWAL] ) + if ( stats->getEffectActive(EFF_WITHDRAWAL) ) { ++numEffectsCured; entity->setEffect(EFF_WITHDRAWAL, false, EFFECT_WITHDRAWAL_BASE_TIME, true); @@ -1060,13 +1339,13 @@ bool item_PotionCureAilment(Item*& item, Entity* entity, Entity* usedBy) if ( item->beatitude < 0 ) { messagePlayer(player, MESSAGE_HINT, Language::get(2903)); - stats->EFFECTS[EFF_POISONED] = true; + stats->setEffectActive(EFF_POISONED, 1); stats->EFFECTS_TIMERS[EFF_POISONED] = item->potionGetCursedEffectDurationRandom(entity, stats); } else if ( item->beatitude > 0 ) { - stats->EFFECTS[EFF_HP_REGEN] = true; - stats->EFFECTS[EFF_MP_REGEN] = true; + stats->setEffectActive(EFF_HP_REGEN, 1); + stats->setEffectActive(EFF_MP_REGEN, 1); stats->EFFECTS_TIMERS[EFF_HP_REGEN] += item->potionGetEffectDurationRandom(entity, stats); stats->EFFECTS_TIMERS[EFF_MP_REGEN] += stats->EFFECTS_TIMERS[EFF_HP_REGEN]; } @@ -1075,6 +1354,7 @@ bool item_PotionCureAilment(Item*& item, Entity* entity, Entity* usedBy) // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1122,7 +1402,7 @@ bool item_PotionBlindness(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1133,6 +1413,7 @@ bool item_PotionBlindness(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1154,6 +1435,7 @@ bool item_PotionBlindness(Item*& item, Entity* entity, Entity* usedBy) // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1201,7 +1483,7 @@ bool item_PotionInvisibility(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1210,8 +1492,10 @@ bool item_PotionInvisibility(Item*& item, Entity* entity, Entity* usedBy) } return false; } + if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1238,13 +1522,14 @@ bool item_PotionInvisibility(Item*& item, Entity* entity, Entity* usedBy) } } } - stats->EFFECTS[EFF_INVISIBLE] = true; + stats->setEffectActive(EFF_INVISIBLE, 1); stats->EFFECTS_TIMERS[EFF_INVISIBLE] = item->potionGetEffectDurationRandom(entity, stats); serverUpdateEffects(player); // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1292,7 +1577,7 @@ bool item_PotionLevitation(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1303,6 +1588,7 @@ bool item_PotionLevitation(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1312,19 +1598,20 @@ bool item_PotionLevitation(Item*& item, Entity* entity, Entity* usedBy) //Cursed effect slows you. messagePlayer(player, MESSAGE_HINT, Language::get(2900)); messagePlayer(player, MESSAGE_HINT, Language::get(2901)); - stats->EFFECTS[EFF_SLOW] = true; + stats->setEffectActive(EFF_SLOW, 1); stats->EFFECTS_TIMERS[EFF_SLOW] = item->potionGetCursedEffectDurationRandom(entity, stats); } else { messagePlayer(player, MESSAGE_STATUS, Language::get(767)); - stats->EFFECTS[EFF_LEVITATING] = true; + stats->setEffectActive(EFF_LEVITATING, 1); stats->EFFECTS_TIMERS[EFF_LEVITATING] = item->potionGetEffectDurationRandom(entity, stats); } serverUpdateEffects(player); // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1372,7 +1659,7 @@ bool item_PotionSpeed(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1383,6 +1670,7 @@ bool item_PotionSpeed(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1392,31 +1680,31 @@ bool item_PotionSpeed(Item*& item, Entity* entity, Entity* usedBy) { messagePlayer(player, MESSAGE_HINT, Language::get(2900)); //Cursed effect slows you. - if ( stats->EFFECTS[EFF_FAST] ) + if ( stats->getEffectActive(EFF_FAST) ) { messagePlayer(player, MESSAGE_STATUS, Language::get(769)); - stats->EFFECTS[EFF_FAST] = false; + stats->clearEffect(EFF_FAST); stats->EFFECTS_TIMERS[EFF_FAST] = 0; } else { messagePlayer(player, MESSAGE_HINT, Language::get(2902)); - stats->EFFECTS[EFF_SLOW] = true; + stats->setEffectActive(EFF_SLOW, 1); stats->EFFECTS_TIMERS[EFF_SLOW] = item->potionGetCursedEffectDurationRandom(entity, stats); } } else { - if ( !stats->EFFECTS[EFF_SLOW] ) + if ( !stats->getEffectActive(EFF_SLOW) ) { messagePlayer(player, MESSAGE_STATUS, Language::get(768)); - stats->EFFECTS[EFF_FAST] = true; + stats->setEffectActive(EFF_FAST, 1); stats->EFFECTS_TIMERS[EFF_FAST] += item->potionGetEffectDurationRandom(entity, stats); } else { messagePlayer(player, MESSAGE_STATUS, Language::get(769)); - stats->EFFECTS[EFF_SLOW] = false; + stats->clearEffect(EFF_SLOW); stats->EFFECTS_TIMERS[EFF_SLOW] = 0; } } @@ -1424,6 +1712,7 @@ bool item_PotionSpeed(Item*& item, Entity* entity, Entity* usedBy) // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1471,7 +1760,7 @@ bool item_PotionStrength(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1482,6 +1771,7 @@ bool item_PotionStrength(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1504,13 +1794,14 @@ bool item_PotionStrength(Item*& item, Entity* entity, Entity* usedBy) else { messagePlayer(player, MESSAGE_STATUS, Language::get(3354)); - stats->EFFECTS[EFF_POTION_STR] = true; + stats->setEffectActive(EFF_POTION_STR, 1); stats->EFFECTS_TIMERS[EFF_POTION_STR] = item->potionGetEffectDurationRandom(entity, stats); } serverUpdateEffects(player); // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1565,7 +1856,7 @@ bool item_PotionAcid(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1574,12 +1865,13 @@ bool item_PotionAcid(Item*& item, Entity* entity, Entity* usedBy) } return false; } - if ( multiplayer == CLIENT || player == 0 ) + if ( multiplayer == CLIENT || (player >= 0 && players[player]->isLocalPlayer()) ) { camera_shakex += .1; camera_shakey += 10; if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1614,6 +1906,7 @@ bool item_PotionAcid(Item*& item, Entity* entity, Entity* usedBy) // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1668,7 +1961,7 @@ bool item_PotionUnstableStorm(Item*& item, Entity* entity, Entity* usedBy, Entit return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1686,6 +1979,7 @@ bool item_PotionUnstableStorm(Item*& item, Entity* entity, Entity* usedBy, Entit } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1750,7 +2044,7 @@ bool item_PotionUnstableStorm(Item*& item, Entity* entity, Entity* usedBy, Entit Uint32 color = makeColorRGB(255, 128, 0); messagePlayerColor(player, MESSAGE_STATUS, color, Language::get(3699)); // superheats serverUpdateHunger(player); - for ( int c = 0; c < 100; c++ ) + for ( int c = 0; c < 25; c++ ) { if ( Entity* entity = spawnFlame(players[player]->entity, SPRITE_FLAME) ) { @@ -1762,6 +2056,22 @@ bool item_PotionUnstableStorm(Item*& item, Entity* entity, Entity* usedBy, Entit entity->skill[0] = 5 + local_rng.rand() % 10; } } + + if ( Entity* fx = createParticleAestheticOrbit(players[player]->entity, 233, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->x = players[player]->entity->x; + fx->y = players[player]->entity->y; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->vel_z = -0.05; + fx->actmagicOrbitDist = 2; + fx->fskill[2] = players[player]->entity->yaw + (local_rng.rand() % 8) * PI / 4.0; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + + serverSpawnMiscParticles(players[player]->entity, PARTICLE_EFFECT_FLAMES, 233, 0, fx->skill[0]); + } } else { @@ -1779,6 +2089,7 @@ bool item_PotionUnstableStorm(Item*& item, Entity* entity, Entity* usedBy, Entit // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1826,7 +2137,7 @@ bool item_PotionParalysis(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1837,6 +2148,7 @@ bool item_PotionParalysis(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1862,6 +2174,7 @@ bool item_PotionParalysis(Item*& item, Entity* entity, Entity* usedBy) // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1909,7 +2222,7 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -1920,6 +2233,7 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1932,14 +2246,14 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should { messagePlayer(player, MESSAGE_HINT, Language::get(2900)); messagePlayer(player, MESSAGE_HINT, Language::get(2903)); - stats->EFFECTS[EFF_POISONED] = true; + stats->setEffectActive(EFF_POISONED, 1); stats->EFFECTS_TIMERS[EFF_POISONED] = item->potionGetCursedEffectDurationRandom(entity, stats); } else { messagePlayer(player, MESSAGE_HINT, Language::get(772)); // stop bleeding - if ( stats->EFFECTS[EFF_BLEEDING] ) + if ( stats->getEffectActive(EFF_BLEEDING) ) { entity->setEffect(EFF_BLEEDING, false, 0, false); } @@ -1947,6 +2261,7 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should serverUpdateEffects(player); if ( shouldConsumeItem ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -1958,7 +2273,7 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should if ( stats->type == GOATMAN && entity->behavior == &actMonster ) { amount *= GOATMAN_HEALINGPOTION_MOD; //Goatman special. - stats->EFFECTS[EFF_FAST] = true; + stats->setEffectActive(EFF_FAST, 1); stats->EFFECTS_TIMERS[EFF_FAST] = GOATMAN_HEALING_POTION_SPEED_BOOST_DURATION; } @@ -1968,6 +2283,9 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should amount += 2 * statGetCON(stats, entity); } + real_t healMult = entity->getHealingSpellPotionModifierFromEffects(false); + amount *= healMult; + if ( item->beatitude < 0 ) { amount /= (std::abs(item->beatitude) * 2); @@ -1980,6 +2298,7 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should int heal = std::max(entity->getHP() - oldHP, 0); if ( heal > 0 ) { + entity->getHealingSpellPotionModifierFromEffects(true); serverUpdatePlayerGameplayStats(player, STATISTICS_HEAL_BOT, heal); } @@ -1993,14 +2312,14 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should { messagePlayer(player, MESSAGE_HINT, Language::get(2900)); messagePlayer(player, MESSAGE_HINT, Language::get(2903)); - stats->EFFECTS[EFF_POISONED] = true; + stats->setEffectActive(EFF_POISONED, 1); stats->EFFECTS_TIMERS[EFF_POISONED] = item->potionGetCursedEffectDurationRandom(entity, stats); } else { messagePlayerColor(player, MESSAGE_STATUS, color, Language::get(773)); // stop bleeding - if ( stats->EFFECTS[EFF_BLEEDING] ) + if ( stats->getEffectActive(EFF_BLEEDING) ) { entity->setEffect(EFF_BLEEDING, false, 0, false); } @@ -2008,6 +2327,7 @@ bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool should serverUpdateEffects(player); if ( shouldConsumeItem ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -2057,7 +2377,7 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -2068,6 +2388,7 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -2080,14 +2401,14 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s { messagePlayer(player, MESSAGE_HINT, Language::get(2900)); messagePlayer(player, MESSAGE_HINT, Language::get(2903)); - stats->EFFECTS[EFF_POISONED] = true; + stats->setEffectActive(EFF_POISONED, 1); stats->EFFECTS_TIMERS[EFF_POISONED] = item->potionGetCursedEffectDurationRandom(entity, stats); } else { messagePlayer(player, MESSAGE_HINT, Language::get(772)); // stop bleeding - if ( stats->EFFECTS[EFF_BLEEDING] ) + if ( stats->getEffectActive(EFF_BLEEDING) ) { entity->setEffect(EFF_BLEEDING, false, 0, false); } @@ -2095,6 +2416,7 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s serverUpdateEffects(player); if ( shouldConsumeItem ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -2106,7 +2428,7 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s if ( stats->type == GOATMAN && entity->behavior == &actMonster ) { amount *= GOATMAN_HEALINGPOTION_MOD; //Goatman special. - stats->EFFECTS[EFF_FAST] = true; + stats->setEffectActive(EFF_FAST, 1); stats->EFFECTS_TIMERS[EFF_FAST] = GOATMAN_HEALING_POTION_SPEED_BOOST_DURATION; } @@ -2116,6 +2438,9 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s amount += 4 * statGetCON(stats, entity); } + real_t healMult = entity->getHealingSpellPotionModifierFromEffects(false); + amount *= healMult; + if ( item->beatitude < 0 ) { amount /= (std::abs(item->beatitude) * 2); @@ -2128,6 +2453,7 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s int heal = std::max(entity->getHP() - oldHP, 0); if ( heal > 0 ) { + entity->getHealingSpellPotionModifierFromEffects(true); serverUpdatePlayerGameplayStats(player, STATISTICS_HEAL_BOT, heal); } @@ -2140,14 +2466,14 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s { messagePlayer(player, MESSAGE_HINT, Language::get(2900)); messagePlayer(player, MESSAGE_HINT, Language::get(2903)); - stats->EFFECTS[EFF_POISONED] = true; + stats->setEffectActive(EFF_POISONED, 1); stats->EFFECTS_TIMERS[EFF_POISONED] = item->potionGetCursedEffectDurationRandom(entity, stats); } else { messagePlayerColor(player, MESSAGE_STATUS, color, Language::get(773)); // stop bleeding - if ( stats->EFFECTS[EFF_BLEEDING] ) + if ( stats->getEffectActive(EFF_BLEEDING) ) { entity->setEffect(EFF_BLEEDING, false, 0, false); } @@ -2155,6 +2481,7 @@ bool item_PotionExtraHealing(Item*& item, Entity* entity, Entity* usedBy, bool s serverUpdateEffects(player); if ( shouldConsumeItem ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -2208,7 +2535,7 @@ bool item_PotionRestoreMagic(Item*& item, Entity* entity, Entity* usedBy) return false; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -2219,6 +2546,7 @@ bool item_PotionRestoreMagic(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -2229,13 +2557,14 @@ bool item_PotionRestoreMagic(Item*& item, Entity* entity, Entity* usedBy) { messagePlayer(player, MESSAGE_HINT, Language::get(774)); messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(2902)); - stats->EFFECTS[EFF_SLOW] = true; + stats->setEffectActive(EFF_SLOW, 1); stats->EFFECTS_TIMERS[EFF_SLOW] = item->potionGetCursedEffectDurationRandom(entity, stats); } else { messagePlayer(player, MESSAGE_HINT, Language::get(772)); } + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -2252,7 +2581,7 @@ bool item_PotionRestoreMagic(Item*& item, Entity* entity, Entity* usedBy) amount /= (std::abs(item->beatitude) * 2); messagePlayer(player, MESSAGE_HINT, Language::get(774)); messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(2902)); - stats->EFFECTS[EFF_SLOW] = true; + stats->setEffectActive(EFF_SLOW, 1); stats->EFFECTS_TIMERS[EFF_SLOW] = item->potionGetCursedEffectDurationRandom(entity, stats); } else @@ -2282,6 +2611,7 @@ bool item_PotionRestoreMagic(Item*& item, Entity* entity, Entity* usedBy) // play drink sound playSoundEntity(entity, 52, 64); + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return true; } @@ -2329,7 +2659,7 @@ Entity* item_PotionPolymorph(Item*& item, Entity* entity, Entity* usedBy) return nullptr; } } - if ( stats->EFFECTS[EFF_VOMITING] ) + if ( stats->getEffectActive(EFF_VOMITING) ) { if ( player >= 0 && players[player]->isLocalPlayer() ) { @@ -2340,6 +2670,7 @@ Entity* item_PotionPolymorph(Item*& item, Entity* entity, Entity* usedBy) } if ( multiplayer == CLIENT ) { + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return nullptr; } @@ -2360,6 +2691,7 @@ Entity* item_PotionPolymorph(Item*& item, Entity* entity, Entity* usedBy) transformedEntity = spellEffectPolymorph(entity, usedBy, false); } + potionUseAbundanceEffect(item, entity, usedBy); consumeItem(item, player); return transformedEntity; @@ -2702,17 +3034,68 @@ void item_ScrollEnchantWeapon(Item* item, int player) } else { - if ( toEnchant == &stats[player]->gloves ) + bool hasGold = false; + Sint32 goldSubtract = 0; { - messagePlayer(player, MESSAGE_HINT, Language::get(858), (*toEnchant)->getName()); + int goldCost = 75; + int bless = std::max(abs((int)(*toEnchant)->beatitude), 0); + if ( (*toEnchant)->beatitude > 0 ) + { + bless = std::max(0, bless - 1); + } + for ( int i = 0; i < std::min(10, bless); ++i ) + { + goldCost *= 2; + } + goldCost = std::min(10000, goldCost); + + hasGold = stats[player]->GOLD >= goldCost; + + goldSubtract = std::max(0, std::min(goldCost, stats[player]->GOLD)); + + stats[player]->GOLD -= goldCost; + stats[player]->GOLD = std::max(0, stats[player]->GOLD); + + if ( multiplayer == CLIENT ) + { + strcpy((char*)net_packet->data, "FXGD"); + net_packet->data[4] = player; + SDLNet_Write32((Uint32)goldSubtract, &net_packet->data[5]); + SDLNet_Write32((Uint32)0, &net_packet->data[9]); + + Uint16 spellID = SPELL_NONE; + SDLNet_Write16(spellID, &net_packet->data[13]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 15; + sendPacketSafe(net_sock, -1, net_packet, 0); + } } - else + + if ( goldSubtract > 0 ) { - messagePlayer(player, MESSAGE_HINT, Language::get(854)); + messagePlayer(player, MESSAGE_HINT, Language::get(6973)); + if ( players[player]->isLocalPlayer() ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_GOLD_CASTED, "gold", goldSubtract); + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_GOLD_CASTED_RUN, "gold", goldSubtract); + + steamStatisticUpdate(STEAM_STAT_PAY_TO_WIN, STEAM_STAT_INT, goldSubtract); + } } - if ( (*toEnchant)->beatitude > 0 ) + bool effect = false; + if ( !hasGold ) { + if ( (*toEnchant)->beatitude > 0 ) + { + (*toEnchant)->beatitude = 0; + effect = true; + } + } + else if ( (*toEnchant)->beatitude > 0 ) + { + effect = true; (*toEnchant)->beatitude = -(*toEnchant)->beatitude; if ( stats[player]->type == SUCCUBUS ) { @@ -2721,10 +3104,27 @@ void item_ScrollEnchantWeapon(Item* item, int player) } else { + effect = true; (*toEnchant)->beatitude -= 1; } - if ( multiplayer == CLIENT ) + if ( effect ) + { + if ( toEnchant == &stats[player]->gloves ) + { + messagePlayer(player, MESSAGE_HINT, Language::get(858), (*toEnchant)->getName()); + } + else + { + messagePlayer(player, MESSAGE_HINT, Language::get(854)); + } + } + else + { + messagePlayer(player, MESSAGE_HINT, Language::get(853)); + } + + if ( effect && multiplayer == CLIENT ) { strcpy((char*)net_packet->data, "BEAT"); net_packet->data[4] = player; @@ -2881,18 +3281,86 @@ void item_ScrollEnchantArmor(Item* item, int player) } else if ( armor != nullptr ) { - messagePlayer(player, MESSAGE_HINT, Language::get(858), armor->getName()); + bool hasGold = false; + Sint32 goldSubtract = 0; + { + int goldCost = 75; + int bless = std::max(abs((int)armor->beatitude), 0); + if ( armor->beatitude > 0 ) + { + bless = std::max(0, bless - 1); + } + for ( int i = 0; i < std::min(10, bless); ++i ) + { + goldCost *= 2; + } + goldCost = std::min(10000, goldCost); + + hasGold = stats[player]->GOLD >= goldCost; - if ( armor->beatitude > 0 ) + goldSubtract = std::max(0, std::min(goldCost, stats[player]->GOLD)); + + stats[player]->GOLD -= goldCost; + stats[player]->GOLD = std::max(0, stats[player]->GOLD); + + if ( multiplayer == CLIENT ) + { + strcpy((char*)net_packet->data, "FXGD"); + net_packet->data[4] = player; + SDLNet_Write32((Uint32)goldSubtract, &net_packet->data[5]); + SDLNet_Write32((Uint32)0, &net_packet->data[9]); + + Uint16 spellID = SPELL_NONE; + SDLNet_Write16(spellID, &net_packet->data[13]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 15; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + } + + if ( goldSubtract > 0 ) + { + messagePlayer(player, MESSAGE_HINT, Language::get(6973)); + if ( players[player]->isLocalPlayer() ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_GOLD_CASTED, "gold", goldSubtract); + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_GOLD_CASTED_RUN, "gold", goldSubtract); + + steamStatisticUpdate(STEAM_STAT_PAY_TO_WIN, STEAM_STAT_INT, goldSubtract); + } + } + + bool effect = false; + if ( !hasGold ) + { + if ( armor->beatitude > 0 ) + { + armor->beatitude = 0; + effect = true; + } + } + else if ( armor->beatitude > 0 ) { armor->beatitude = -armor->beatitude; + effect = true; } else { armor->beatitude -= 1; + effect = true; } - if ( multiplayer == CLIENT ) + if ( effect ) + { + messagePlayer(player, MESSAGE_HINT, Language::get(858), armor->getName()); + } + else + { + messagePlayer(player, MESSAGE_HINT, Language::get(857)); + } + + if ( effect && multiplayer == CLIENT ) { strcpy((char*)net_packet->data, "BEAT"); net_packet->data[4] = player; @@ -3134,13 +3602,20 @@ bool item_ScrollFire(Item* item, int player) else { playSoundEntity(players[player]->entity, 153, 128); // "FireballExplode.ogg" - messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(864)); // "The scroll erupts in a tower of flame!" + if ( item->beatitude == 0 ) + { + messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(864)); // "The scroll erupts in a tower of flame!" + } + else if ( item->beatitude > 0 ) + { + messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(6861)); // "The scroll erupts in a cloak of flame!" + } // Attempt to set the Player on fire - players[player]->entity->SetEntityOnFire(); + players[player]->entity->SetEntityOnFire(nullptr); int c; - for (c = 0; c < 100; c++) + for (c = 0; c < 25; c++) { if ( Entity* entity = spawnFlame(players[player]->entity, SPRITE_FLAME) ) { @@ -3153,6 +3628,18 @@ bool item_ScrollFire(Item* item, int player) } } + if ( item->beatitude > 0 ) + { + if ( players[player]->entity->setEffect(EFF_FLAME_CLOAK, (Uint8)75, 30 * TICKS_PER_SECOND, true) ) + { + spawnMagicEffectParticles(players[player]->entity->x, players[player]->entity->y, players[player]->entity->z, 2207); + } + } + else + { + castSpell(players[player]->entity->getUID(), getSpellFromID(SPELL_IGNITE), true, true); + } + onScrollUseAppraisalIncrease(item, player); item->identified = true; return true; @@ -3312,7 +3799,11 @@ void item_ScrollMagicMapping(Item* item, int player) if ( item->beatitude >= 0 ) { messagePlayer(player, MESSAGE_HINT, Language::get(868)); - mapLevel(player); + mapLevel(player, 16 + 8 * item->beatitude, players[player]->entity->x / 16, players[player]->entity->y / 16, false); + + /*int pingx = players[player]->entity->x / 16; + int pingy = players[player]->entity->y / 16; + sendMinimapPing(player, pingx, pingy, 0, true);*/ } else { @@ -3462,7 +3953,8 @@ void item_ScrollRepair(Item* item, int player) { messagePlayer(player, MESSAGE_HINT, Language::get(870)); // you feel a tingling sensation } - else if ( armor != nullptr ) + else if ( armor != nullptr + && !players[player]->entity->spellEffectPreserveItem(armor) ) { messagePlayer(player, MESSAGE_HINT, Language::get(871), armor->getName()); if ( item->type == SCROLL_CHARGING ) @@ -3643,7 +4135,8 @@ void item_ScrollDestroyArmor(Item* item, int player) { messagePlayer(player, MESSAGE_HINT, Language::get(873)); } - else if ( armor != nullptr ) + else if ( armor != nullptr + && !players[player]->entity->spellEffectPreserveItem(armor) ) { if ( item->beatitude < 0 ) { @@ -3681,12 +4174,12 @@ void item_ScrollDestroyArmor(Item* item, int player) if ( armor->type == TOOL_CRYSTALSHARD ) { playSoundPlayer(player, 162, 64); - messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(2351), armor->getName()); + //messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(2351), armor->getName()); } else { playSoundPlayer(player, 76, 64); - messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(682), armor->getName()); + //messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(682), armor->getName()); } } } @@ -3963,31 +4456,33 @@ void item_ToolTowel(Item*& item, int player) } if ( multiplayer != CLIENT ) { - if ( stats[player]->EFFECTS[EFF_GREASY] - || stats[player]->EFFECTS[EFF_MESSY] - || stats[player]->EFFECTS[EFF_BLEEDING] ) + if ( stats[player]->getEffectActive(EFF_GREASY) + || stats[player]->getEffectActive(EFF_MAGIC_GREASE) + || stats[player]->getEffectActive(EFF_MESSY) + || stats[player]->getEffectActive(EFF_BLEEDING) ) { steamAchievementClient(player, "BARONY_ACH_BRING_A_TOWEL"); - if ( stats[player]->EFFECTS[EFF_GREASY] ) + if ( stats[player]->getEffectActive(EFF_GREASY) || stats[player]->getEffectActive(EFF_MAGIC_GREASE) ) { Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_TOWEL_GREASY, item->type, 1); } - else if ( stats[player]->EFFECTS[EFF_MESSY] ) + else if ( stats[player]->getEffectActive(EFF_MESSY) ) { Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_TOWEL_MESSY, item->type, 1); } - else if ( stats[player]->EFFECTS[EFF_BLEEDING] ) + else if ( stats[player]->getEffectActive(EFF_BLEEDING) ) { Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_TOWEL_BLEEDING, item->type, 1); } Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_TOWEL_USES, item->type, 1); } - stats[player]->EFFECTS[EFF_GREASY] = false; - stats[player]->EFFECTS[EFF_MESSY] = false; + stats[player]->clearEffect(EFF_MAGIC_GREASE); + stats[player]->clearEffect(EFF_GREASY); + stats[player]->clearEffect(EFF_MESSY); } // stop bleeding - if ( stats[player]->EFFECTS[EFF_BLEEDING] ) + if ( stats[player]->getEffectActive(EFF_BLEEDING) ) { if ( players[player]->isLocalPlayer() ) { @@ -3996,7 +4491,7 @@ void item_ToolTowel(Item*& item, int player) } if ( multiplayer != CLIENT ) { - stats[player]->EFFECTS[EFF_BLEEDING] = false; + stats[player]->clearEffect(EFF_BLEEDING); stats[player]->EFFECTS_TIMERS[EFF_BLEEDING] = 0; } consumeItem(item, player); @@ -4049,14 +4544,14 @@ void item_ToolMirror(Item*& item, int player) // server/local side if ( multiplayer != CLIENT ) { - if ( stats[player]->EFFECTS[EFF_GREASY] ) + if ( stats[player]->getEffectActive(EFF_GREASY) ) { messagePlayer(player, MESSAGE_WORLD, Language::get(887)); messagePlayer(player, MESSAGE_INVENTORY, Language::get(888)); playSoundEntity(players[player]->entity, 162, 64); // whoops, *break* broken = true; } - else if ( stats[player]->EFFECTS[EFF_BLIND] ) + else if ( stats[player]->getEffectActive(EFF_BLIND) ) { messagePlayer(player, MESSAGE_HINT, Language::get(892)); } @@ -4086,7 +4581,7 @@ void item_ToolMirror(Item*& item, int player) { messagePlayer(player, MESSAGE_HINT, Language::get(3698)); } - else if ( stats[player]->EFFECTS[EFF_DRUNK] ) + else if ( stats[player]->getEffectActive(EFF_DRUNK) ) { if ( stats[player]->sex == MALE ) { @@ -4097,19 +4592,19 @@ void item_ToolMirror(Item*& item, int player) messagePlayer(player, MESSAGE_HINT, Language::get(895)); } } - else if ( stats[player]->EFFECTS[EFF_CONFUSED] ) + else if ( stats[player]->getEffectActive(EFF_CONFUSED) ) { messagePlayer(player, MESSAGE_HINT, Language::get(896)); } - else if ( stats[player]->EFFECTS[EFF_POISONED] ) + else if ( stats[player]->getEffectActive(EFF_POISONED) ) { messagePlayer(player, MESSAGE_HINT, Language::get(897)); } - else if ( stats[player]->EFFECTS[EFF_VOMITING] ) + else if ( stats[player]->getEffectActive(EFF_VOMITING) ) { messagePlayer(player, MESSAGE_HINT, Language::get(898)); } - else if ( stats[player]->EFFECTS[EFF_MESSY] ) + else if ( stats[player]->getEffectActive(EFF_MESSY) ) { messagePlayer(player, MESSAGE_HINT, Language::get(899)); } @@ -4140,11 +4635,11 @@ void item_ToolMirror(Item*& item, int player) if ( players[player]->isLocalPlayer() && item ) { - if ( stats[player]->EFFECTS[EFF_GREASY] ) + if ( stats[player]->getEffectActive(EFF_GREASY) ) { broken = true; } - else if ( stats[player]->EFFECTS[EFF_BLIND] ) + else if ( stats[player]->getEffectActive(EFF_BLIND) ) { } else if ( players[player]->entity->isInvisible() || (stats[player]->type == VAMPIRE) ) @@ -4152,7 +4647,8 @@ void item_ToolMirror(Item*& item, int player) } else if ( beatitude > 0 ) { - if ( local_rng.rand() % 4 == 0 ) + if ( local_rng.rand() % 4 == 0 + && !players[player]->entity->spellEffectPreserveItem(item) ) { if ( item->status > DECREPIT ) { @@ -4365,6 +4861,52 @@ Entity* item_ToolBeartrap(Item*& item, Entity* usedBy) return entity; } +int Item::getBaseFoodSatiation(ItemType type) +{ + int hungerIncrease = 0; + switch ( type ) + { + case FOOD_BREAD: + hungerIncrease = 400; + break; + case FOOD_CREAMPIE: + hungerIncrease = 200; + break; + case FOOD_CHEESE: + hungerIncrease = 100; + break; + case FOOD_APPLE: + case FOOD_SHROOM: + case FOOD_NUT: + hungerIncrease = 200; + break; + case FOOD_MEAT: + hungerIncrease = 600; + break; + case FOOD_FISH: + hungerIncrease = 500; + break; + case FOOD_TOMALLEY: + hungerIncrease = 400; + break; + case FOOD_TIN: + hungerIncrease = 600; + break; + case FOOD_RATION: + case FOOD_RATION_SPICY: + case FOOD_RATION_SOUR: + case FOOD_RATION_BITTER: + case FOOD_RATION_HEARTY: + case FOOD_RATION_HERBAL: + case FOOD_RATION_SWEET: + hungerIncrease = 200; + break; + default: + break; + } + return hungerIncrease; +} + void item_Food(Item*& item, int player) { if ( !item ) @@ -4418,7 +4960,7 @@ void item_Food(Item*& item, int player) } // can't eat while vomiting - if ( stats[player]->EFFECTS[EFF_VOMITING] ) + if ( stats[player]->getEffectActive(EFF_VOMITING) ) { if ( players[player]->isLocalPlayer() ) { @@ -4445,6 +4987,7 @@ void item_Food(Item*& item, int player) if ( multiplayer == CLIENT ) { + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -4519,6 +5062,7 @@ void item_Food(Item*& item, int player) } } } + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -4527,6 +5071,7 @@ void item_Food(Item*& item, int player) if (players[player] && players[player]->entity && !(svFlags & SV_FLAG_HUNGER)) { //if ( !(stats[player]->mask && stats[player]->mask->type == MASK_MARIGOLD) ) + if ( stats[player]->type != MYCONID ) { playSoundEntity(players[player]->entity, 28, 64); players[player]->entity->modHP(-5); @@ -4536,6 +5081,10 @@ void item_Food(Item*& item, int player) { messagePlayer(player, MESSAGE_STATUS, Language::get(3201)); } + else if ( stats[player]->type == MYCONID ) + { + messagePlayer(player, MESSAGE_STATUS, Language::get(6991)); + } else if ( item->type == FOOD_BLOOD ) { messagePlayer(player, MESSAGE_STATUS, Language::get(3203)); @@ -4550,9 +5099,39 @@ void item_Food(Item*& item, int player) { players[player]->entity->char_gonnavomit = 40 + local_rng.rand() % 10; } + else if ( stats[player]->type == MYCONID && local_rng.rand() % 3 == 0 ) + { + // gain some mp regen + players[player]->entity->modMP(1 + local_rng.rand() % 2); + Uint32 color = makeColorRGB(0, 255, 0); + players[player]->entity->setEffect(EFF_MP_REGEN, true, std::max(stats[player]->EFFECTS_TIMERS[EFF_MP_REGEN], 10 * TICKS_PER_SECOND), false); + messagePlayerColor(player, MESSAGE_HINT, color, Language::get(6882)); + playSoundEntity(players[player]->entity, 168, 128); + + if ( auto effectStrength = stats[player]->getEffectActive(EFF_GROWTH) ) + { + int chance = 25; + if ( (stats[player]->type == MYCONID && stats[player]->sex == MALE) ) + { + chance = 50; + } + if ( players[player]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_GROWTH, chance) ) + { + if ( stats[player]->getEffectActive(EFF_GROWTH) < 4 ) + { + players[player]->entity->setEffect(EFF_GROWTH, (Uint8)(std::min(4, effectStrength + 1)), 15 * TICKS_PER_SECOND, false); + messagePlayerColor(player, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6924)); + } + } + } + } + } + if ( stats[player]->type != MYCONID ) + { + foodUseAbundanceEffect(item, player); + consumeItem(item, player); + return; } - consumeItem(item, player); - return; } real_t foodMult = 1.0; @@ -4584,25 +5163,22 @@ void item_Food(Item*& item, int player) switch ( item->type ) { case FOOD_BREAD: - hungerIncrease = 400; - break; case FOOD_CREAMPIE: - hungerIncrease = 200; - break; case FOOD_CHEESE: - hungerIncrease = 100; - break; case FOOD_APPLE: - hungerIncrease = 200; - break; case FOOD_MEAT: - hungerIncrease = 600; - break; case FOOD_FISH: - hungerIncrease = 500; - break; case FOOD_TOMALLEY: - hungerIncrease = 400; + case FOOD_RATION: + case FOOD_RATION_SPICY: + case FOOD_RATION_SOUR: + case FOOD_RATION_BITTER: + case FOOD_RATION_HEARTY: + case FOOD_RATION_HERBAL: + case FOOD_RATION_SWEET: + case FOOD_SHROOM: + case FOOD_NUT: + hungerIncrease = Item::getBaseFoodSatiation(item->type); break; case FOOD_BLOOD: if ( players[player] && players[player]->entity @@ -4632,12 +5208,32 @@ void item_Food(Item*& item, int player) stats[player]->HUNGER += hungerIncrease * foodMult; - if ( stats[player]->mask && stats[player]->mask->type == MASK_MARIGOLD ) + if ( players[player] && players[player]->entity ) { - if ( players[player] && players[player]->entity ) + if ( stats[player]->mask && stats[player]->mask->type == MASK_MARIGOLD ) { players[player]->entity->setEffect(EFF_MARIGOLD, true, stats[player]->EFFECTS_TIMERS[EFF_MARIGOLD] + TICKS_PER_SECOND * 30, false); } + + if ( item->beatitude > 0 ) + { + players[player]->entity->setEffect(EFF_HP_MP_REGEN, true, + stats[player]->EFFECTS_TIMERS[EFF_HP_MP_REGEN] + TICKS_PER_SECOND * 30 * item->beatitude, false); + } + else if ( stats[player]->getEffectActive(EFF_BLESS_FOOD) ) + { + players[player]->entity->setEffect(EFF_HP_MP_REGEN, true, + stats[player]->EFFECTS_TIMERS[EFF_HP_MP_REGEN] + TICKS_PER_SECOND * 30, false); + + int caster = (int)stats[player]->getEffectActive(EFF_BLESS_FOOD) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_BLESS_FOOD, 30.0, 1.0, nullptr); + } + } + } } } else @@ -4654,6 +5250,15 @@ void item_Food(Item*& item, int player) } } + if ( item->beatitude > 0 ) + { + foodMod += 3 * item->beatitude; + } + else if ( stats[player]->getEffectActive(EFF_BLESS_FOOD) ) + { + foodMod += 3; + } + players[player]->entity->modHP(std::max(1, (int)(foodMod * foodMult))); messagePlayer(player, MESSAGE_WORLD, Language::get(911)); @@ -4674,12 +5279,23 @@ void item_Food(Item*& item, int player) manaRegenPercent = 0.1; break; case FOOD_APPLE: + case FOOD_SHROOM: + case FOOD_NUT: manaRegenPercent = 0.2; break; case FOOD_MEAT: case FOOD_FISH: manaRegenPercent = 0.5; break; + case FOOD_RATION: + case FOOD_RATION_SPICY: + case FOOD_RATION_SOUR: + case FOOD_RATION_BITTER: + case FOOD_RATION_HEARTY: + case FOOD_RATION_HERBAL: + case FOOD_RATION_SWEET: + manaRegenPercent = 0.1; + break; case FOOD_BLOOD: if ( players[player] && players[player]->entity && playerRequiresBloodToSustain(player) ) @@ -4705,12 +5321,57 @@ void item_Food(Item*& item, int player) { serverUpdatePlayerGameplayStats(player, STATISTICS_TEMPT_FATE, 1); } + else + { + int effectID = -1; + switch ( item->type ) + { + case FOOD_RATION_SPICY: + effectID = EFF_RATION_SPICY; + serverUpdatePlayerGameplayStats(player, STATISTICS_FLAVORTOWN, 1 << 0); + break; + case FOOD_RATION_SOUR: + effectID = EFF_RATION_SOUR; + serverUpdatePlayerGameplayStats(player, STATISTICS_FLAVORTOWN, 1 << 1); + break; + case FOOD_RATION_BITTER: + effectID = EFF_RATION_BITTER; + serverUpdatePlayerGameplayStats(player, STATISTICS_FLAVORTOWN, 1 << 2); + break; + case FOOD_RATION_HEARTY: + effectID = EFF_RATION_HEARTY; + serverUpdatePlayerGameplayStats(player, STATISTICS_FLAVORTOWN, 1 << 3); + break; + case FOOD_RATION_HERBAL: + effectID = EFF_RATION_HERBAL; + serverUpdatePlayerGameplayStats(player, STATISTICS_FLAVORTOWN, 1 << 4); + break; + case FOOD_RATION_SWEET: + effectID = EFF_RATION_SWEET; + serverUpdatePlayerGameplayStats(player, STATISTICS_FLAVORTOWN, 1 << 5); + break; + default: + break; + } + + if ( effectID >= 0 ) + { + if ( players[player] && players[player]->entity ) + { + if ( item->beatitude >= 0 || (item->beatitude < 0 && stats[player]->type == MYCONID) ) + { + players[player]->entity->setEffect(effectID, true, stats[player]->EFFECTS_TIMERS[effectID] + TICKS_PER_SECOND * 60, false); + } + } + } + } // results of eating if ( players[player] ) { updateHungerMessages(players[player]->entity, stats[player], item); } + foodUseAbundanceEffect(item, player); consumeItem(item, player); } @@ -4763,7 +5424,7 @@ void item_FoodTin(Item*& item, int player) } // can't eat while vomiting - if ( stats[player]->EFFECTS[EFF_VOMITING] ) + if ( stats[player]->getEffectActive(EFF_VOMITING) ) { if ( players[player]->isLocalPlayer() ) { @@ -4778,7 +5439,7 @@ void item_FoodTin(Item*& item, int player) conductFoodless = false; conductVegetarian = false; if ( stats[player]->playerRace == RACE_SKELETON && stats[player]->stat_appearance == 0 - && players[player] && players[player]->entity->effectPolymorph > NUMMONSTERS ) + && players[player] && players[player]->entity && players[player]->entity->effectPolymorph > NUMMONSTERS ) { steamAchievement("BARONY_ACH_MUSCLE_MEMORY"); } @@ -4791,6 +5452,7 @@ void item_FoodTin(Item*& item, int player) if ( multiplayer == CLIENT ) { + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -4861,11 +5523,25 @@ void item_FoodTin(Item*& item, int player) // chance of rottenness pukeChance = item->foodGetPukeChance(stats[player]); + if ( players[player] && players[player]->entity && players[player]->entity->effectShapeshift != NOTHING ) + { + pukeChance = 100; // shapeshifted players don't puke + } + else if ( player >= 0 && stats[player]->type == INSECTOID ) + { + pukeChance = 100; // insectoids can eat anything. + } + else if ( item->beatitude < 0 && pukeChance == 100 ) + { + pukeChance = 99; // make it so you will vomit + } + if ((item->beatitude < 0 || local_rng.rand() % pukeChance == 0) && pukeChance < 100) { if (players[player] && players[player]->entity && !(svFlags & SV_FLAG_HUNGER)) { //if ( !(stats[player]->mask && stats[player]->mask->type == MASK_MARIGOLD) ) + if ( stats[player]->type != MYCONID ) { playSoundEntity(players[player]->entity, 28, 64); players[player]->entity->modHP(-5); @@ -4875,6 +5551,10 @@ void item_FoodTin(Item*& item, int player) { messagePlayer(player, MESSAGE_STATUS | MESSAGE_HINT, Language::get(3201)); } + else if ( stats[player]->type == MYCONID ) + { + messagePlayer(player, MESSAGE_STATUS | MESSAGE_HINT, Language::get(6991)); + } else { messagePlayer(player, MESSAGE_STATUS | MESSAGE_HINT, Language::get(908)); @@ -4886,12 +5566,43 @@ void item_FoodTin(Item*& item, int player) { players[player]->entity->char_gonnavomit = 40 + local_rng.rand() % 10; } + else if ( stats[player]->type == MYCONID && local_rng.rand() % 3 == 0 ) + { + // gain some mp regen + players[player]->entity->modMP(1 + local_rng.rand() % 2); + Uint32 color = makeColorRGB(0, 255, 0); + players[player]->entity->setEffect(EFF_MP_REGEN, true, std::max(stats[player]->EFFECTS_TIMERS[EFF_MP_REGEN], 10 * TICKS_PER_SECOND), false); + messagePlayerColor(player, MESSAGE_HINT, color, Language::get(6882)); + playSoundEntity(players[player]->entity, 168, 128); + + if ( auto effectStrength = stats[player]->getEffectActive(EFF_GROWTH) ) + { + int chance = 25; + if ( (stats[player]->type == MYCONID && stats[player]->sex == MALE) ) + { + chance = 50; + } + if ( players[player]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_GROWTH, chance) ) + { + if ( stats[player]->getEffectActive(EFF_GROWTH) < 4 ) + { + players[player]->entity->setEffect(EFF_GROWTH, (Uint8)(std::min(4, effectStrength + 1)), 15 * TICKS_PER_SECOND, false); + messagePlayerColor(player, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6924)); + } + } + } + } + } + + if ( stats[player]->type != MYCONID ) + { + foodUseAbundanceEffect(item, player); + consumeItem(item, player); + return; } - consumeItem(item, player); - return; } - int buffDuration = item->status * TICKS_PER_SECOND * 4; // (4 - 16 seconds) + int buffDuration = item->status * TICKS_PER_SECOND * 60; if ( item->status > WORN ) { buffDuration -= local_rng.rand() % ((buffDuration / 2) + 1); // 50-100% duration @@ -4927,26 +5638,44 @@ void item_FoodTin(Item*& item, int player) // replenish nutrition points if (svFlags & SV_FLAG_HUNGER) { - stats[player]->HUNGER += 600 * foodMult; + stats[player]->HUNGER += Item::getBaseFoodSatiation(item->type) * foodMult; - if ( stats[player]->mask && stats[player]->mask->type == MASK_MARIGOLD ) + if ( players[player] && players[player]->entity ) { - if ( players[player] && players[player]->entity ) + if ( stats[player]->mask && stats[player]->mask->type == MASK_MARIGOLD ) { players[player]->entity->setEffect(EFF_MARIGOLD, true, stats[player]->EFFECTS_TIMERS[EFF_MARIGOLD] + TICKS_PER_SECOND * 30, false); } + + if ( item->beatitude > 0 ) + { + players[player]->entity->setEffect(EFF_HP_MP_REGEN, true, stats[player]->EFFECTS_TIMERS[EFF_HP_MP_REGEN] + TICKS_PER_SECOND * 30 * item->beatitude, false); + } + else if ( stats[player]->getEffectActive(EFF_BLESS_FOOD) ) + { + players[player]->entity->setEffect(EFF_HP_MP_REGEN, true, stats[player]->EFFECTS_TIMERS[EFF_HP_MP_REGEN] + TICKS_PER_SECOND * 30, false); + + int caster = (int)stats[player]->getEffectActive(EFF_BLESS_FOOD) - 1; + if ( caster >= 0 && caster < MAXPLAYERS ) + { + if ( players[caster]->entity ) + { + players[caster]->mechanics.updateSustainedSpellEvent(SPELL_BLESS_FOOD, 30.0, 1.0, nullptr); + } + } + } } if ( hpBuff ) { - stats[player]->EFFECTS[EFF_HP_REGEN] = hpBuff; - stats[player]->EFFECTS_TIMERS[EFF_HP_REGEN] = buffDuration; + stats[player]->setEffectActive(EFF_HP_REGEN, 1); + stats[player]->EFFECTS_TIMERS[EFF_HP_REGEN] += buffDuration; Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_TIN_REGEN_HP, FOOD_TIN, 1); } if ( mpBuff ) { - stats[player]->EFFECTS[EFF_MP_REGEN] = mpBuff; - stats[player]->EFFECTS_TIMERS[EFF_MP_REGEN] = buffDuration; + stats[player]->setEffectActive(EFF_MP_REGEN, 1); + stats[player]->EFFECTS_TIMERS[EFF_MP_REGEN] += buffDuration; Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_TIN_REGEN_MP, FOOD_TIN, 1); } } @@ -4963,6 +5692,14 @@ void item_FoodTin(Item*& item, int player) foodMod += 3 * std::min(2, abs(stats[player]->mask->beatitude)); } } + if ( item->beatitude > 0 ) + { + foodMod += 3 * item->beatitude; + } + else if ( stats[player]->getEffectActive(EFF_BLESS_FOOD) ) + { + foodMod += 3; + } players[player]->entity->modHP(std::max(1, (int)(foodMod * foodMult))); messagePlayer(player, MESSAGE_WORLD, Language::get(911)); @@ -5008,7 +5745,7 @@ void item_FoodTin(Item*& item, int player) { messagePlayer(player, MESSAGE_WORLD, Language::get(911)); } - + foodUseAbundanceEffect(item, player); consumeItem(item, player); } @@ -5078,7 +5815,33 @@ void item_AmuletSexChange(Item* item, int player) } else { - if ( stats[player]->sex == MALE ) + if ( stats[player]->type == AUTOMATON ) + { + messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(6914)); + } + else if ( stats[player]->type == DRYAD ) + { + if ( stats[player]->sex == MALE ) + { + messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(6912)); + } + else + { + messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(6913)); + } + } + else if ( stats[player]->type == MYCONID ) + { + if ( stats[player]->sex == MALE ) + { + messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(6913)); + } + else + { + messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(6912)); + } + } + else if ( stats[player]->sex == MALE ) { messagePlayer(player, MESSAGE_HINT | MESSAGE_STATUS, Language::get(967)); } @@ -5094,24 +5857,16 @@ void item_Spellbook(Item*& item, int player) { node_t* node, *nextnode; - item->identified = true; if ( players[player] && !players[player]->isLocalPlayer() ) { return; } - if ( players[player] && players[player]->entity && players[player]->entity->isBlind()) + /*if ( itemCategory(item) == TOME_SPELL ) { - messagePlayer(player, MESSAGE_HINT, Language::get(970)); - playSoundPlayer(player, 90, 64); + players[player]->magic.setQuickCastTomeFromInventory(item); return; - } - if ( itemIsEquipped(item, player) ) - { - messagePlayer(player, MESSAGE_MISC, Language::get(3460)); - playSoundPlayer(player, 90, 64); - return; - } + }*/ if ( players[player] && players[player]->entity ) { @@ -5129,6 +5884,21 @@ void item_Spellbook(Item*& item, int player) } } + if ( players[player] && players[player]->entity && players[player]->entity->isBlind() ) + { + messagePlayer(player, MESSAGE_HINT, Language::get(970)); + playSoundPlayer(player, 90, 64); + return; + } + + item->identified = true; + if ( itemIsEquipped(item, player) ) + { + messagePlayer(player, MESSAGE_MISC, Language::get(3460)); + playSoundPlayer(player, 90, 64); + return; + } + conductIlliterate = false; if ( item->beatitude < 0 && !shouldInvertEquipmentBeatitude(stats[player]) ) @@ -5418,7 +6188,22 @@ void item_Spellbook(Item*& item, int player) learned = addSpell(SPELL_CRAB_WEB, player); break; default: - learned = addSpell(SPELL_FORCEBOLT, player); + if ( items[item->type].category == SPELLBOOK ) + { + int spellID = getSpellIDFromSpellbook(item->type); + if ( spellID > SPELL_NONE ) + { + learned = addSpell(spellID, player); + } + } + else if ( items[item->type].category == TOME_SPELL ) + { + int spellID = item->getTomeSpellID(); + if ( auto spell = getSpellFromID(spellID) ) + { + learned = addSpell(spell->ID, player); + } + } break; } @@ -5429,9 +6214,19 @@ void item_Spellbook(Item*& item, int player) if ( learned ) { - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELLBOOK_LEARNT, item->type, 1); - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELLBOOK_CAST_DEGRADES, item->type, 1); - if ( item->type >= SPELLBOOK_RAT_FORM && item->type <= SPELLBOOK_IMP_FORM ) + int spellID = SPELL_NONE; + if ( itemCategory(item) == SPELLBOOK ) + { + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELLBOOK_LEARNT, item->type, 1); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELLBOOK_CAST_DEGRADES, item->type, 1); + spellID = getSpellIDFromSpellbook(item->type); + } + else if ( itemCategory(item) == TOME_SPELL ) + { + spellID = item->getTomeSpellID(); + } + + if ( spellID >= SPELL_RAT_FORM && spellID <= SPELL_IMP_FORM ) { ItemType originalSpellbook = item->type; item->type = SPELLBOOK_REVERT_FORM; @@ -5441,21 +6236,31 @@ void item_Spellbook(Item*& item, int player) } item->type = originalSpellbook; } - item->status = static_cast(item->status - 1); - if ( item->status != BROKEN ) + + if ( itemCategory(item) == SPELLBOOK ) { - messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_EQUIPMENT, Language::get(2595)); + item->status = static_cast(item->status - 1); + if ( item->status != BROKEN ) + { + messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_EQUIPMENT, Language::get(2595)); + } + else + { + messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_EQUIPMENT, Language::get(2596)); + consumeItem(item, player); + } + + if ( stats[player] && stats[player]->playerRace == RACE_INSECTOID && stats[player]->stat_appearance == 0 ) + { + steamStatisticUpdate(STEAM_STAT_BOOKWORM, STEAM_STAT_INT, 1); + } } - else + else if ( itemCategory(item) == TOME_SPELL ) { - messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_EQUIPMENT, Language::get(2596)); + messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_EQUIPMENT, Language::get(6852), items[item->type].getUnidentifiedName()); consumeItem(item, player); } - - if ( stats[player] && stats[player]->playerRace == RACE_INSECTOID && stats[player]->stat_appearance == 0 ) - { - steamStatisticUpdate(STEAM_STAT_BOOKWORM, STEAM_STAT_INT, 1); - } + if ( list_Size(&players[player]->magic.spellList) >= 20 ) { steamAchievement("BARONY_ACH_MAGIC_MASTERY"); @@ -5485,6 +6290,7 @@ void item_FoodAutomaton(Item*& item, int player) if ( multiplayer == CLIENT ) { + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -5516,6 +6322,7 @@ void item_FoodAutomaton(Item*& item, int player) { messagePlayer(player, MESSAGE_COMBAT | MESSAGE_STATUS, Language::get(910)); } + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -5545,9 +6352,19 @@ void item_FoodAutomaton(Item*& item, int player) case FOOD_MEAT: case FOOD_FISH: case FOOD_TIN: + case FOOD_RATION: + case FOOD_RATION_SPICY: + case FOOD_RATION_SOUR: + case FOOD_RATION_BITTER: + case FOOD_RATION_HEARTY: + case FOOD_RATION_HERBAL: + case FOOD_RATION_SWEET: + case FOOD_SHROOM: + case FOOD_NUT: if ( svFlags & SV_FLAG_HUNGER ) { messagePlayer(player, MESSAGE_STATUS, Language::get(3697)); // no effect. + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -5572,6 +6389,7 @@ void item_FoodAutomaton(Item*& item, int player) case GEM_DIAMOND: case GEM_JETSTONE: case GEM_OBSIDIAN: + case GEM_JEWEL: stats[player]->HUNGER += 1000; players[player]->entity->modMP(10); break; @@ -5616,7 +6434,7 @@ void item_FoodAutomaton(Item*& item, int player) } playSoundEntity(players[player]->entity, 153, 128); // "FireballExplode.ogg" - for ( int c = 0; c < 100; c++ ) + for ( int c = 0; c < 25; c++ ) { if ( Entity* entity = spawnFlame(players[player]->entity, SPRITE_FLAME) ) { @@ -5628,6 +6446,22 @@ void item_FoodAutomaton(Item*& item, int player) entity->skill[0] = 5 + local_rng.rand() % 10; } } + + if ( Entity* fx = createParticleAestheticOrbit(players[player]->entity, 233, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->x = players[player]->entity->x; + fx->y = players[player]->entity->y; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->vel_z = -0.05; + fx->actmagicOrbitDist = 2; + fx->fskill[2] = players[player]->entity->yaw + (local_rng.rand() % 8) * PI / 4.0; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + + serverSpawnMiscParticles(players[player]->entity, PARTICLE_EFFECT_FLAMES, 233, 0, fx->skill[0]); + } break; } case TOOL_METAL_SCRAP: @@ -5638,6 +6472,7 @@ void item_FoodAutomaton(Item*& item, int player) if ( stats[player]->HUNGER > 500 ) { messagePlayer(player, MESSAGE_STATUS, Language::get(3707)); // fails to add any more heat. + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -5655,6 +6490,7 @@ void item_FoodAutomaton(Item*& item, int player) if ( stats[player]->HUNGER > 1100 ) { messagePlayer(player, MESSAGE_STATUS, Language::get(3707)); // fails to add any more heat. + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -5691,6 +6527,7 @@ void item_FoodAutomaton(Item*& item, int player) playSoundEntity(players[player]->entity, 28, 64); players[player]->entity->modHP(-5); messagePlayer(player, MESSAGE_WORLD, Language::get(908)); // blecch! rotten food! + foodUseAbundanceEffect(item, player); consumeItem(item, player); return; } @@ -5728,6 +6565,7 @@ void item_FoodAutomaton(Item*& item, int player) } serverUpdateHunger(player); + foodUseAbundanceEffect(item, player); consumeItem(item, player); } @@ -5744,6 +6582,15 @@ bool itemIsConsumableByAutomaton(const Item& item) case FOOD_MEAT: case FOOD_FISH: case FOOD_TIN: + case FOOD_RATION: + case FOOD_RATION_SPICY: + case FOOD_RATION_SOUR: + case FOOD_RATION_BITTER: + case FOOD_RATION_HEARTY: + case FOOD_RATION_HERBAL: + case FOOD_RATION_SWEET: + case FOOD_SHROOM: + case FOOD_NUT: case GEM_ROCK: case GEM_GLASS: @@ -5763,6 +6610,7 @@ bool itemIsConsumableByAutomaton(const Item& item) case GEM_DIAMOND: case GEM_JETSTONE: case GEM_OBSIDIAN: + case GEM_JEWEL: case READABLE_BOOK: diff --git a/src/items.cpp b/src/items.cpp index be65833bb..8974ef58b 100644 --- a/src/items.cpp +++ b/src/items.cpp @@ -150,6 +150,10 @@ void autoHotbarTryAdd(const int player, Item& item) { players[player]->hotbar.magicBoomerangHotbarSlot = index; } + if ( item.type == TOOL_DUCK ) + { + players[player]->hotbar.magicDuckHotbarSlot = index; + } return; } @@ -218,6 +222,7 @@ Item* newItem(const ItemType type, const Status status, const Sint16 beatitude, item->itemSpecialShopConsumable = false; item->interactNPCUid = 0; item->notifyIcon = false; + item->spellNotifyIcon = false; if ( inventory ) { Player::Inventory_t* playerInventoryUI = nullptr; @@ -326,7 +331,7 @@ ItemType itemLevelCurveEntity(Entity& my, Category cat, int minLevel, int maxLev { itemLevelCurveType = ITEM_LEVEL_CURVE_TYPE_DEFAULT; itemLevelCurveShop = -1; - if ( my.behavior == &actMonster && my.getMonsterTypeFromSprite() == SHOPKEEPER ) + if ( my.behavior == &actMonster && (my.getMonsterTypeFromSprite() == SHOPKEEPER || my.monsterCanTradeWith(-1)) ) { itemLevelCurveType = ITEM_LEVEL_CURVE_TYPE_SHOP; itemLevelCurveShop = my.monsterStoreType; @@ -342,6 +347,173 @@ ItemType itemLevelCurveEntity(Entity& my, Category cat, int minLevel, int maxLev return result; } +bool itemLevelCurvePostProcess(Entity* my, Item* item, BaronyRNG& rng, int itemLevel) +{ + if ( !((my && my->behavior == &actItem) || item) ) + { + return false; + } + + bool modified = false; + itemLevelCurveType = ITEM_LEVEL_CURVE_TYPE_DEFAULT; + if ( my ) + { + if ( my->behavior == &actMonster && (my->getMonsterTypeFromSprite() == SHOPKEEPER || my->monsterCanTradeWith(-1)) ) + { + itemLevelCurveType = ITEM_LEVEL_CURVE_TYPE_SHOP; + itemLevelCurveShop = my->monsterStoreType; + } + else if ( my->behavior == &actChest ) + { + itemLevelCurveType = ITEM_LEVEL_CURVE_TYPE_CHEST; + } + } + + int itemType = (my && my->behavior == &actItem) ? my->skill[10] : item->type; + int itemStatus = (my && my->behavior == &actItem) ? my->skill[11] : item->type; + if ( itemType >= BRONZE_TOMAHAWK && itemType <= CRYSTAL_SHURIKEN ) + { + // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) + itemStatus = std::min(static_cast(DECREPIT + (itemType - BRONZE_TOMAHAWK)), EXCELLENT); + if ( my && my->behavior == &actItem ) + { + my->skill[11] = itemStatus; + } + else + { + item->status = static_cast(itemStatus); + } + } + if ( itemType == BOLAS ) + { + itemStatus = SERVICABLE; + if ( my && my->behavior == &actItem ) + { + my->skill[11] = itemStatus; + } + else + { + item->status = static_cast(itemStatus); + } + } + if ( itemType >= 0 && itemType < NUMITEMS ) + { + if ( items[itemType].category == SPELLBOOK ) + { + //if ( itemLevelCurveType == ITEM_LEVEL_CURVE_TYPE_DEFAULT ) + { + std::vector> chances; + chances.reserve(NUM_SPELLS); + std::vector chanceWeights; + chanceWeights.reserve(NUM_SPELLS); + int minDifficulty = std::min(60, (itemLevel / 5) * 20); +#ifndef NDEBUG + std::map debugChances; + std::map debugChancesNum; +#endif + for ( int i = 0; i < NUM_SPELLS; ++i ) + { + auto find = allGameSpells.find(i); + if ( find != allGameSpells.end() ) + { + if ( auto spell = find->second ) + { + if ( spell->ID != SPELL_NONE && !spell->hide_from_ui && itemLevel >= spell->drop_table ) + { + if ( (spell->difficulty / 20) <= (1 + (itemLevel / 5)) + /*&& (spell->difficulty >= minDifficulty)*/ ) + { + chances.push_back(std::make_pair(spell->skillID, spell->ID)); + if ( spell->difficulty == minDifficulty ) + { + chanceWeights.push_back(40); + } + else if ( spell->difficulty > minDifficulty ) + { + chanceWeights.push_back(30); + } + else if ( spell->difficulty == 40 ) + { + chanceWeights.push_back(10); + } + else if ( spell->difficulty == 20 ) + { + chanceWeights.push_back(10); + } + else if ( spell->difficulty == 0 ) + { + chanceWeights.push_back(10); + } + + if ( spell->difficulty == 0 ) + { + if ( minDifficulty == 0 ) + { + chanceWeights.back() += 20; + } + else if ( minDifficulty == 20 ) + { + chanceWeights.back() += 20; + } + chanceWeights.back() *= 2; + } + else if ( spell->difficulty >= 80 ) + { + chanceWeights.back() *= 2; + } +#ifndef NDEBUG + debugChances[spell->difficulty] += chanceWeights.back(); + debugChancesNum[spell->difficulty] += 1; +#endif + } + } + } + } + } + assert(chances.size() == chanceWeights.size()); + if ( chances.size() ) + { + Uint32 appearance = (my && my->behavior == &actItem) ? my->skill[14] : item->appearance; + int pick = rng.discrete(chanceWeights.data(), chanceWeights.size()); + int spellbookType = getSpellbookFromSpellID(chances[pick].second); + if ( items[spellbookType].category == SPELLBOOK ) + { + itemType = spellbookType; + } + else + { + itemType = TOME_SORCERY; + appearance = spellTomeIDToAppearance[chances[pick].second]; + if ( chances[pick].first == PRO_MYSTICISM ) + { + itemType = TOME_MYSTICISM; + } + else if ( chances[pick].first == PRO_THAUMATURGY ) + { + itemType = TOME_THAUMATURGY; + } + } + if ( my && my->behavior == &actItem ) + { + my->skill[10] = itemType; + my->skill[14] = appearance; + } + else + { + item->type = static_cast(itemType); + item->appearance = appearance; + } + } + } + } + } + + itemLevelCurveType = ITEM_LEVEL_CURVE_TYPE_DEFAULT; + itemLevelCurveShop = -1; + + return modified; +} + bool isHatShopItem(ItemType hat) { switch ( hat ) @@ -390,6 +562,10 @@ bool isHatShopItem(ItemType hat) case HAT_HOOD_APPRENTICE: case HAT_HOOD_ASSASSIN: case HAT_HOOD_WHISPERS: + case HAT_FELT: + case HOOD_TEAL: + case HAT_CIRCLET_SORCERY: + case HAT_CIRCLET_THAUMATURGY: return true; default: break; @@ -403,7 +579,7 @@ ItemType itemLevelCurve(const Category cat, const int minLevel, const int maxLev bool chances[NUMITEMS]; int c; - if ( cat < 0 || cat >= NUMCATEGORIES ) + if ( cat < 0 || cat >= Category::CATEGORY_MAX ) { printlog("warning: itemLevelCurve() called with bad category value!\n"); return GEM_ROCK; @@ -422,6 +598,11 @@ ItemType itemLevelCurve(const Category cat, const int minLevel, const int maxLev if ( itemLevelCurveType == ITEM_LEVEL_CURVE_TYPE_SHOP ) { + if ( c == READABLE_BOOK ) + { + continue; + } + if ( itemLevelCurveShop == 1 ) // hat store { if ( !isHatShopItem(static_cast(c)) ) @@ -464,6 +645,12 @@ ItemType itemLevelCurve(const Category cat, const int minLevel, const int maxLev chances[c] = false; } break; + case TOOL_FRYING_PAN: + if ( rng.rand() % 4 ) // 75% chance + { + chances[c] = false; + } + break; default: break; } @@ -591,12 +778,16 @@ char* Item::description() const //No fancy descriptives for empty potions. snprintf(tempstr, 1024, Language::get(982 + status), beatitude); } + /*else if ( type == POTION_GREASE ) + { + snprintf(tempstr, 1024, Language::get(992 + status), Language::get(975), beatitude); + }*/ else { snprintf(tempstr, 1024, Language::get(992 + status), Language::get(974 + items[type].index + appearance % items[type].variations - 50), beatitude); } } - else if ( itemCategory(this) == SCROLL || itemCategory(this) == SPELLBOOK || itemCategory(this) == BOOK ) + else if ( itemCategory(this) == SCROLL || itemCategory(this) == SPELLBOOK || itemCategory(this) == BOOK || itemCategory(this) == TOME_SPELL ) { snprintf(tempstr, 1024, Language::get(997 + status), beatitude); } @@ -619,6 +810,10 @@ char* Item::description() const { snprintf(&tempstr[c], 1024 - c, Language::get(1007), getBookLocalizedNameFromIndex(appearance % numbooks).c_str()); } + else if ( itemCategory(this) == TOME_SPELL ) + { + snprintf(&tempstr[c], 1024 - c, Language::get(6850), getTomeLabel()); + } else { snprintf(&tempstr[c], 1024 - c, "%s", items[type].getIdentifiedName()); @@ -661,12 +856,16 @@ char* Item::description() const //No fancy descriptives for empty potions. snprintf(tempstr, 1024, Language::get(1008 + status), count, beatitude); } + /*else if ( type == POTION_GREASE ) + { + snprintf(tempstr, 1024, Language::get(1018 + status), count, Language::get(975), beatitude); + }*/ else { snprintf(tempstr, 1024, Language::get(1018 + status), count, Language::get(974 + items[type].index + appearance % items[type].variations - 50), beatitude); } } - else if ( itemCategory(this) == SCROLL || itemCategory(this) == SPELLBOOK || itemCategory(this) == BOOK ) + else if ( itemCategory(this) == SCROLL || itemCategory(this) == SPELLBOOK || itemCategory(this) == BOOK || itemCategory(this) == TOME_SPELL ) { snprintf(tempstr, 1024, Language::get(1023 + status), count, beatitude); } @@ -689,6 +888,10 @@ char* Item::description() const { snprintf(&tempstr[c], 1024 - c, Language::get(1033), count, getBookLocalizedNameFromIndex(appearance % numbooks).c_str()); } + else if ( itemCategory(this) == TOME_SPELL ) + { + snprintf(&tempstr[c], 1024 - c, Language::get(6851), getTomeLabel()); + } else { snprintf(&tempstr[c], 1024 - c, "%s", items[type].getIdentifiedName()); @@ -734,12 +937,16 @@ char* Item::description() const //No fancy descriptives for empty potions. snprintf(tempstr, 1024, Language::get(1034 + status), beatitude); } + /*else if ( type == POTION_GREASE ) + { + snprintf(tempstr, 1024, Language::get(1044 + status), Language::get(975), beatitude); + }*/ else { snprintf(tempstr, 1024, Language::get(1044 + status), Language::get(974 + items[type].index + appearance % items[type].variations - 50)); } } - else if ( itemCategory(this) == SCROLL || itemCategory(this) == SPELLBOOK || itemCategory(this) == BOOK ) + else if ( itemCategory(this) == SCROLL || itemCategory(this) == SPELLBOOK || itemCategory(this) == BOOK || itemCategory(this) == TOME_SPELL ) { strncpy(tempstr, Language::get(1049 + status), 1024); } @@ -811,12 +1018,16 @@ char* Item::description() const //No fancy descriptives for empty potions. snprintf(tempstr, 1024, Language::get(1060 + status), count); } + /*else if ( type == POTION_GREASE ) + { + snprintf(tempstr, 1024, Language::get(1070 + status), count, Language::get(975), beatitude); + }*/ else { snprintf(tempstr, 1024, Language::get(1070 + status), count, Language::get(974 + items[type].index + appearance % items[type].variations - 50)); } } - else if ( itemCategory(this) == SCROLL || itemCategory(this) == SPELLBOOK || itemCategory(this) == BOOK ) + else if ( itemCategory(this) == SCROLL || itemCategory(this) == SPELLBOOK || itemCategory(this) == BOOK || itemCategory(this) == TOME_SPELL ) { snprintf(tempstr, 1024, Language::get(1075 + status), count); } @@ -895,6 +1106,10 @@ char* Item::getName() const { snprintf(tempstr, sizeof(tempstr), Language::get(1007), getBookLocalizedNameFromIndex(appearance % numbooks).c_str()); } + else if ( itemCategory(this) == TOME_SPELL ) + { + snprintf(tempstr, sizeof(tempstr), Language::get(6850), getTomeLabel()); + } else { strcpy(tempstr, items[type].getIdentifiedName()); @@ -923,6 +1138,76 @@ char* Item::getName() const return tempstr; } +int getItemVariationFromSpellbookOrTome(const Item& item) +{ + int spellID = SPELL_NONE; + if ( itemCategory(&item) == SPELLBOOK ) + { + spellID = getSpellIDFromSpellbook(item.type); + } + else if ( itemCategory(&item) == TOME_SPELL ) + { + spellID = item.getTomeSpellID(); + } + if ( spellID <= SPELL_NONE ) + { + return -1; + } + if ( auto spell = getSpellFromID(spellID) ) + { + int index = -1; + switch ( spell->skillID ) + { + case PRO_SORCERY: + if ( spell->difficulty <= 20 ) + { + index = SPELLBOOK_COLOR_SORCERY_1; + } + else if ( spell->difficulty <= 60 ) + { + index = SPELLBOOK_COLOR_SORCERY_2; + } + else + { + index = SPELLBOOK_COLOR_SORCERY_3; + } + break; + case PRO_MYSTICISM: + if ( spell->difficulty <= 20 ) + { + index = SPELLBOOK_COLOR_MYSTICISM_1; + } + else if ( spell->difficulty <= 60 ) + { + index = SPELLBOOK_COLOR_MYSTICISM_2; + } + else + { + index = SPELLBOOK_COLOR_MYSTICISM_3; + } + break; + case PRO_THAUMATURGY: + if ( spell->difficulty <= 20 ) + { + index = SPELLBOOK_COLOR_THAUM_1; + } + else if ( spell->difficulty <= 60 ) + { + index = SPELLBOOK_COLOR_THAUM_2; + } + else + { + index = SPELLBOOK_COLOR_THAUM_3; + } + break; + default: + break; + } + return index; + } + return -1; +} + /*------------------------------------------------------------------------------- itemModel @@ -931,13 +1216,70 @@ char* Item::getName() const -------------------------------------------------------------------------------*/ -Sint32 itemModel(const Item* const item, bool shortModel) +Sint32 itemModel(const Item* const item, bool shortModel, Entity* creature) { if ( !item || item->type < 0 || item->type >= NUMITEMS ) { return 0; } + if ( creature && creature->behavior == &actMonster ) + { + if ( item->type == IRON_PAULDRONS ) + { + if ( creature->sprite == 1569 ) + { + return 2142; + } + else if ( creature->sprite == 1570 ) + { + return 2143; + } + return 0; + } + else if ( item->type == SHAWL ) + { + if ( creature->sprite == 1569 ) + { + if ( item->appearance % items[item->type].variations < 2 ) + { + return 2144 + item->appearance % items[item->type].variations; + } + } + else if ( creature->sprite == 1570 ) + { + if ( item->appearance % items[item->type].variations < 2 ) + { + return 2146 + item->appearance % items[item->type].variations; + } + } + return 0; + } + else if ( !shortModel && + (item->type == ROBE_WIZARD + || item->type == ROBE_MONK + || item->type == ROBE_CULTIST + || item->type == ROBE_HEALER) ) + { + if ( item->type == ROBE_WIZARD ) + { + return 2148; + } + else if ( item->type == ROBE_MONK ) + { + return 2151; + } + else if ( item->type == ROBE_CULTIST ) + { + return 2149; + } + else if ( item->type == ROBE_HEALER ) + { + return 2150; + } + } + } + int index = shortModel ? items[item->type].indexShort : items[item->type].index; if ( item->type == TOOL_PLAYER_LOOT_BAG ) @@ -970,6 +1312,29 @@ Sint32 itemModel(const Item* const item, bool shortModel) return index + item->getLootBagPlayer(); } } + else if ( item->type == MAGICSTAFF_SCEPTER ) + { + if ( item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX == 0 ) + { + return index + 2; + } + else + { + return index; + } + } + else if ( item->type == TOOL_DUCK ) + { + return items[TOOL_DUCK].index + (item->appearance % items[item->type].variations) / MAXPLAYERS; + } + else if ( itemCategory(item) == SPELLBOOK || itemCategory(item) == TOME_SPELL ) + { + int variation = getItemVariationFromSpellbookOrTome(*item); + if ( variation >= 0 && variation < items[item->type].variations ) + { + return index + variation; + } + } return index + item->appearance % items[item->type].variations; } @@ -987,90 +1352,31 @@ Sint32 itemModelFirstperson(const Item* const item) { return 0; } - return items[item->type].fpindex + item->appearance % items[item->type].variations; -} - -/*------------------------------------------------------------------------------- - - itemSprite - - returns a pointer to the SDL_Surface used to represent the item --------------------------------------------------------------------------------*/ - -SDL_Surface* itemSprite(Item* const item) -{ - if ( !item || item->type < 0 || item->type >= NUMITEMS ) + if ( item->type == MAGICSTAFF_SCEPTER ) { - return nullptr; - } - if (itemCategory(item) == SPELL_CAT) - { - spell_t* spell = nullptr; - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX == 0 ) { - spell = getSpellFromItem(i, item, false); - if ( spell ) - { - break; - } + return items[item->type].fpindex + 2; } - if (spell) + else { - node_t* node = list_Node(&items[item->type].surfaces, spell->ID); - if ( !node ) - { - return nullptr; - } - SDL_Surface** surface = static_cast(node->element); - return *surface; + return items[item->type].fpindex; } } - else + else if ( itemCategory(item) == SPELLBOOK || itemCategory(item) == TOME_SPELL ) { - node_t* node = nullptr; - if ( item->type == TOOL_PLAYER_LOOT_BAG ) - { - if ( colorblind_lobby ) - { - int playerOwner = item->getLootBagPlayer(); - Uint32 index = 4; - switch ( playerOwner ) - { - case 0: - index = 2; - break; - case 1: - index = 3; - break; - case 2: - index = 1; - break; - case 3: - index = 4; - break; - default: - break; - } - node = list_Node(&items[item->type].surfaces, index); - } - else - { - node = list_Node(&items[item->type].surfaces, item->getLootBagPlayer()); - } - } - else - { - node = list_Node(&items[item->type].surfaces, item->appearance % items[item->type].variations); - } - if ( !node ) + int variation = getItemVariationFromSpellbookOrTome(*item); + if ( variation >= 0 && variation < items[item->type].variations ) { - return nullptr; + return items[item->type].fpindex + variation; } - SDL_Surface** surface = static_cast(node->element); - return *surface; } - return nullptr; + else if ( item->type == TOOL_DUCK ) + { + return items[TOOL_DUCK].fpindex + (item->appearance % items[item->type].variations) / MAXPLAYERS; + } + return items[item->type].fpindex + item->appearance % items[item->type].variations; } /*------------------------------------------------------------------------------- @@ -1131,6 +1437,7 @@ int itemCompare(const Item* const item1, const Item* const item2, bool checkAppe return 1; } else if ( item1->type == SCROLL_MAIL || item1->type == READABLE_BOOK || items[item1->type].category == SPELL_CAT + || items[item1->type].category == TOME_SPELL || item1->type == TOOL_PLAYER_LOOT_BAG ) { if ( comparisonUsedForStacking ) @@ -1175,13 +1482,113 @@ int itemCompare(const Item* const item1, const Item* const item2, bool checkAppe -------------------------------------------------------------------------------*/ Uint32 dropItemSfxTicks[MAXPLAYERS] = { 0 }; +bool playerThrowDuck(const int player, Item* const item, int charge) +{ + if ( !item || item->count == 0 ) { return false; } + if ( player < 0 || player >= MAXPLAYERS ) { return false; } + if ( !players[player]->entity ) { return false; } + + if ( players[player]->isLocalPlayer() ) + { + if ( itemIsEquipped(item, player) ) + { + if ( multiplayer == CLIENT ) + { + strcpy((char*)net_packet->data, "DCKA"); + SDLNet_Write32(static_cast(item->type), &net_packet->data[4]); + SDLNet_Write32(static_cast(item->status), &net_packet->data[8]); + SDLNet_Write32(static_cast(item->beatitude), &net_packet->data[12]); + SDLNet_Write32(static_cast(item->count), &net_packet->data[16]); + SDLNet_Write32(static_cast(item->appearance), &net_packet->data[20]); + net_packet->data[24] = item->identified; + net_packet->data[25] = clientnum; + net_packet->data[26] = charge; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 27; + sendPacketSafe(net_sock, -1, net_packet, 0); + + Item** slot = itemSlot(stats[player], item); + if ( slot != nullptr ) + { + *slot = nullptr; + } + + players[player]->paperDoll.updateSlots(); + + if ( item->node != nullptr ) + { + list_RemoveNode(item->node); + } + else + { + free(item); + } + + return true; + } + } + } + + if ( multiplayer != CLIENT && players[player]->entity ) + { + playSoundEntity(players[player]->entity, 75, 64); + + Entity* entity = newEntity(itemModel(item), 1, map.entities, nullptr); // thrown item + entity->parent = players[player]->entity->getUID(); + entity->x = players[player]->entity->x; + entity->y = players[player]->entity->y; + entity->z = players[player]->entity->z; + entity->yaw = players[player]->entity->yaw; + entity->sizex = 2; + entity->sizey = 2; + entity->behavior = &actThrown; + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + entity->skill[10] = item->type; + entity->skill[11] = item->status; + entity->skill[12] = item->beatitude; + entity->skill[13] = 1; + entity->skill[14] = item->appearance; + entity->skill[15] = item->identified; + + real_t speed = 1.f + 4.f * (-30 + std::min(50, std::max(30, charge))) / (real_t)(20); + entity->vel_x = speed * cos(players[player]->entity->yaw); + entity->vel_y = speed * sin(players[player]->entity->yaw); + entity->vel_z = -.5; + + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_THROWN, + item->type, 1); + + Item** slot = itemSlot(stats[player], item); + + if ( slot != nullptr ) + { + *slot = nullptr; + } + players[player]->paperDoll.updateSlots(); + + if ( item->node != nullptr ) + { + list_RemoveNode(item->node); + } + else + { + free(item); + } + return true; + } + + return false; +} + bool playerGreasyDropItem(const int player, Item* const item) { if ( !item || item->count == 0 ) { return false; } if ( player < 0 || player >= MAXPLAYERS ) { return false; } if ( players[player]->isLocalPlayer() ) { - if ( !stats[player]->EFFECTS[EFF_GREASY] ) { return false; } + if ( !stats[player]->getEffectActive(EFF_GREASY) ) { return false; } if ( itemIsEquipped(item, player) ) { Item** slot = itemSlot(stats[player], item); @@ -1727,6 +2134,19 @@ Entity* dropItemMonster(Item* const item, Entity* const monster, Stat* const mon entity->vel_y *= 0.1; entity->vel_z = -.5; } + else if ( monsterStats->type == DUCK_SMALL ) + { + // drop in center of tile + int ix = static_cast(std::floor(monster->x)) >> 4; + int iy = static_cast(std::floor(monster->y)) >> 4; + entity->x = ix * 16.0 + 8.0; + entity->y = iy * 16.0 + 8.0; + entity->z = 4; + entity->vel_x *= 0.1; + entity->vel_y *= 0.1; + entity->vel_z = -.5; + entity->pitch = -PI / 8; + } else if ( item->type == ARTIFACT_ORB_PURPLE && monsterStats->type == LICH ) { entity->vel_x = 0.0; @@ -1903,7 +2323,12 @@ EquipItemResult equipItem(Item* const item, Item** const slot, const int player, Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_APPRAISED, (*slot)->type, 1); } } + bool prevIdentified = (*slot)->identified; (*slot)->identified = true; + if ( !prevIdentified ) + { + Item::onItemIdentified(player, item); + } return EQUIP_ITEM_FAIL_CANT_UNEQUIP; } } @@ -1918,7 +2343,7 @@ EquipItemResult equipItem(Item* const item, Item** const slot, const int player, { playSoundEntity(players[player]->entity, 33 + local_rng.rand() % 2, 64); } - else if ( item->type == BOOMERANG ) + else if ( item->type == BOOMERANG || itemTypeIsThrownBall(item->type) ) { } else if ( itemCategory(item) == WEAPON || itemCategory(item) == THROWN ) @@ -1927,6 +2352,7 @@ EquipItemResult equipItem(Item* const item, Item** const slot, const int player, } else if ( itemCategory(item) == ARMOR || item->type == TOOL_TINKERING_KIT + || item->type == TOOL_FRYING_PAN || itemTypeIsQuiver(item->type) ) { playSoundEntity(players[player]->entity, 44 + local_rng.rand() % 3, 64); @@ -2015,7 +2441,12 @@ EquipItemResult equipItem(Item* const item, Item** const slot, const int player, Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_APPRAISED, (*slot)->type, 1); } } + bool prevIdentified = (*slot)->identified; (*slot)->identified = true; + if ( !prevIdentified ) + { + Item::onItemIdentified(player, item); + } return EQUIP_ITEM_FAIL_CANT_UNEQUIP; } @@ -2264,6 +2695,10 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi equipItemResult = equipItem(item, &stats[player]->weapon, player, checkInventorySpaceForPaperDoll); break; case IRON_SHIELD: + case SCUTUM: + case BONE_SHIELD: + case BLACKIRON_SHIELD: + case SILVER_SHIELD: equipItemResult = equipItem(item, &stats[player]->shield, player, checkInventorySpaceForPaperDoll); break; case SHORTBOW: @@ -2280,6 +2715,35 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case STEEL_CHAKRAM: case CRYSTAL_SHURIKEN: case BOOMERANG: + case RAPIER: + case GREASE_BALL: + case DUST_BALL: + case BOLAS: + case STEEL_FLAIL: + case SLOP_BALL: + case SHILLELAGH_MACE: + case CLAYMORE_SWORD: + case ANELACE_SWORD: + case LANCE_SPEAR: + case STEEL_FALSHION: + case STEEL_GREATAXE: + case BLACKIRON_AXE: + case BLACKIRON_CROSSBOW: + case BLACKIRON_DART: + case BLACKIRON_MACE: + case BLACKIRON_SWORD: + case BLACKIRON_TRIDENT: + case BONE_AXE: + case BONE_MACE: + case BONE_SHORTBOW: + case BONE_SPEAR: + case BONE_SWORD: + case BONE_THROWING: + case SILVER_AXE: + case SILVER_GLAIVE: + case SILVER_MACE: + case SILVER_PLUMBATA: + case SILVER_SWORD: equipItemResult = equipItem(item, &stats[player]->weapon, player, checkInventorySpaceForPaperDoll); break; case STEEL_SHIELD: @@ -2292,6 +2756,8 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case LONGBOW: case COMPOUND_BOW: case HEAVY_CROSSBOW: + case BRANCH_BOW: + case BRANCH_BOW_INFECTED: equipItemResult = equipItem(item, &stats[player]->weapon, player, checkInventorySpaceForPaperDoll); break; case GLOVES: @@ -2306,6 +2772,11 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case IRON_KNUCKLES: case SPIKED_GAUNTLETS: case SUEDE_GLOVES: + case BONE_BRACERS: + case BLACKIRON_GAUNTLETS: + case SILVER_GAUNTLETS: + case QUILTED_GLOVES: + case CHAIN_GLOVES: equipItemResult = equipItem(item, &stats[player]->gloves, player, checkInventorySpaceForPaperDoll); break; case CLOAK: @@ -2317,6 +2788,7 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case CLOAK_BACKPACK: case CLOAK_SILVER: case CLOAK_GUARDIAN: + case CLOAK_DENDRITE: equipItemResult = equipItem(item, &stats[player]->cloak, player, checkInventorySpaceForPaperDoll); break; case LEATHER_BOOTS: @@ -2329,6 +2801,13 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case ARTIFACT_BOOTS: case CRYSTAL_BOOTS: case SUEDE_BOOTS: + case CLEAT_BOOTS: + case BONE_BOOTS: + case BLACKIRON_BOOTS: + case SILVER_BOOTS: + case QUILTED_BOOTS: + case LOAFERS: + case CHAIN_BOOTS: equipItemResult = equipItem(item, &stats[player]->shoes, player, checkInventorySpaceForPaperDoll); break; case LEATHER_BREASTPIECE: @@ -2342,6 +2821,19 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case ARTIFACT_BREASTPIECE: case TUNIC: case MACHINIST_APRON: + case BANDIT_BREASTPIECE: + case TUNIC_BLOUSE: + case BONE_BREASTPIECE: + case BLACKIRON_BREASTPIECE: + case SILVER_BREASTPIECE: + case IRON_PAULDRONS: + case QUILTED_GAMBESON: + case ROBE_CULTIST: + case ROBE_HEALER: + case ROBE_MONK: + case ROBE_WIZARD: + case SHAWL: + case CHAIN_HAUBERK: equipItemResult = equipItem(item, &stats[player]->breastplate, player, checkInventorySpaceForPaperDoll); break; case HAT_PHRYGIAN: @@ -2381,6 +2873,15 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case HAT_HOOD_APPRENTICE: case HAT_HOOD_ASSASSIN: case HAT_HOOD_WHISPERS: + case BONE_HELM: + case BLACKIRON_HELM: + case SILVER_HELM: + case QUILTED_CAP: + case CHAIN_COIF: + case HAT_FELT: + case HOOD_TEAL: + case HAT_CIRCLET_SORCERY: + case HAT_CIRCLET_THAUMATURGY: equipItemResult = equipItem(item, &stats[player]->helmet, player, checkInventorySpaceForPaperDoll); break; case AMULET_SEXCHANGE: @@ -2410,6 +2911,7 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi } break; case AMULET_POISONRESISTANCE: + case AMULET_BURNINGRESIST: equipItemResult = equipItem(item, &stats[player]->amulet, player, checkInventorySpaceForPaperDoll); break; case POTION_WATER: @@ -2610,6 +3112,8 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case MAGICSTAFF_SUMMON: case MAGICSTAFF_CHARM: case MAGICSTAFF_POISON: + case BRANCH_STAFF: + case MAGICSTAFF_SCEPTER: equipItemResult = equipItem(item, &stats[player]->weapon, player, checkInventorySpaceForPaperDoll); break; case RING_ADORNMENT: @@ -2678,6 +3182,85 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case SPELLBOOK_FLUTTER: case SPELLBOOK_DASH: case SPELLBOOK_SELF_POLYMORPH: + case SPELLBOOK_METEOR: + case SPELLBOOK_ICE_WAVE: + case SPELLBOOK_GUARD_BODY: + case SPELLBOOK_GUARD_SPIRIT: + case SPELLBOOK_DIVINE_GUARD: + case SPELLBOOK_PROF_NIMBLENESS: + case SPELLBOOK_PROF_GREATER_MIGHT: + case SPELLBOOK_PROF_COUNSEL: + case SPELLBOOK_PROF_STURDINESS: + case SPELLBOOK_BLESS_FOOD: + case SPELLBOOK_PINPOINT: + case SPELLBOOK_DONATION: + case SPELLBOOK_SCRY_ALLIES: + case SPELLBOOK_SCRY_TRAPS: + case SPELLBOOK_SCRY_TREASURES: + case SPELLBOOK_DETECT_ENEMY: + case SPELLBOOK_TURN_UNDEAD: + case SPELLBOOK_HEAL_OTHER: + case SPELLBOOK_BLOOD_WARD: + case SPELLBOOK_DIVINE_ZEAL: + case SPELLBOOK_MAXIMISE: + case SPELLBOOK_MINIMISE: + case SPELLBOOK_INCOHERENCE: + case SPELLBOOK_OVERCHARGE: + case SPELLBOOK_ENVENOM_WEAPON: + case SPELLBOOK_PSYCHIC_SPEAR: + case SPELLBOOK_DEFY_FLESH: + case SPELLBOOK_GREASE_SPRAY: + case SPELLBOOK_BLOOD_WAVES: + case SPELLBOOK_COMMAND: + case SPELLBOOK_METALLURGY: + case SPELLBOOK_FORGE_KEY: + case SPELLBOOK_RESHAPE_WEAPON: + case SPELLBOOK_ALTER_ARROW: + case SPELLBOOK_VOID_CHEST: + case SPELLBOOK_LEAD_BOLT: + case SPELLBOOK_NUMBING_BOLT: + case SPELLBOOK_CURSE_FLESH: + case SPELLBOOK_COWARDICE: + case SPELLBOOK_SEEK_ALLY: + case SPELLBOOK_DEEP_SHADE: + case SPELLBOOK_SPIRIT_WEAPON: + case SPELLBOOK_SPORES: + case SPELLBOOK_WINDGATE: + case SPELLBOOK_TELEKINESIS: + case SPELLBOOK_DISARM: + case SPELLBOOK_ABUNDANCE: + case SPELLBOOK_PRESERVE: + case SPELLBOOK_SABOTAGE: + case SPELLBOOK_MIST_FORM: + case SPELLBOOK_FORCE_SHIELD: + case SPELLBOOK_SPLINTER_GEAR: + case SPELLBOOK_ATTRACT_ITEMS: + case SPELLBOOK_ABSORB_MAGIC: + case SPELLBOOK_TUNNEL: + case SPELLBOOK_NULL_AREA: + case SPELLBOOK_FIRE_SPRITE: + case SPELLBOOK_SPIN: + case SPELLBOOK_CLEANSE_FOOD: + case SPELLBOOK_FLAME_CLOAK: + case SPELLBOOK_LIGHTNING_BOLT: + case SPELLBOOK_DISRUPT_EARTH: + case SPELLBOOK_FIRE_WALL: + case SPELLBOOK_SLAM: + case SPELLBOOK_IGNITE: + case SPELLBOOK_SHATTER_OBJECTS: + case SPELLBOOK_KINETIC_FIELD: + case SPELLBOOK_THORNS: + case SPELLBOOK_MAGICIANS_ARMOR: + case SPELLBOOK_HEAL_MINOR: + case SPELLBOOK_SIGIL: + case SPELLBOOK_SANCTUARY: + case SPELLBOOK_HOLY_BEAM: + case SPELLBOOK_DOMINATE: + item_Spellbook(item, player); + break; + case TOME_SORCERY: + case TOME_MYSTICISM: + case TOME_THAUMATURGY: item_Spellbook(item, player); break; case GEM_ROCK: @@ -2698,6 +3281,7 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case GEM_JETSTONE: case GEM_OBSIDIAN: case GEM_GLASS: + case GEM_JEWEL: equipItemResult = equipItem(item, &stats[player]->weapon, player, checkInventorySpaceForPaperDoll); break; case TOOL_PICKAXE: @@ -2734,6 +3318,30 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case QUIVER_KNOCKBACK: case QUIVER_CRYSTAL: case QUIVER_HUNTING: + case QUIVER_BONE: + case QUIVER_BLACKIRON: + case TOOL_FOCI_FIRE: + case TOOL_FOCI_SNOW: + case TOOL_FOCI_NEEDLES: + case TOOL_FOCI_ARCS: + case TOOL_FOCI_SAND: + case TOOL_FOCI_DARK_LIFE: + case TOOL_FOCI_DARK_RIFT: + case TOOL_FOCI_DARK_SILENCE: + case TOOL_FOCI_DARK_VENGEANCE: + case TOOL_FOCI_DARK_SUPPRESS: + case TOOL_FOCI_LIGHT_PEACE: + case TOOL_FOCI_LIGHT_JUSTICE: + case TOOL_FOCI_LIGHT_PROVIDENCE: + case TOOL_FOCI_LIGHT_PURITY: + case TOOL_FOCI_LIGHT_SANCTUARY: + case INSTRUMENT_FLUTE: + case INSTRUMENT_LYRE: + case INSTRUMENT_DRUM: + case INSTRUMENT_LUTE: + case INSTRUMENT_HORN: + case TOOL_FRYING_PAN: + case TOOL_DUCK: equipItemResult = equipItem(item, &stats[player]->shield, player, checkInventorySpaceForPaperDoll); break; case TOOL_BLINDFOLD: @@ -2744,7 +3352,7 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case TOOL_TOWEL: item_ToolTowel(item, player); if ( multiplayer == CLIENT ) - if ( stats[player]->EFFECTS[EFF_BLEEDING] ) + if ( stats[player]->getEffectActive(EFF_BLEEDING) ) { consumeItem(item, player); } @@ -2816,6 +3424,15 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case FOOD_FISH: case FOOD_TOMALLEY: case FOOD_BLOOD: + case FOOD_RATION: + case FOOD_RATION_SPICY: + case FOOD_RATION_SOUR: + case FOOD_RATION_BITTER: + case FOOD_RATION_HEARTY: + case FOOD_RATION_HERBAL: + case FOOD_RATION_SWEET: + case FOOD_SHROOM: + case FOOD_NUT: item_Food(item, player); break; case FOOD_TIN: @@ -3057,6 +3674,9 @@ void useItem(Item* item, const int player, Entity* usedBy, bool unequipForDroppi case AMULET_POISONRESISTANCE: messagePlayer(player, MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(2492)); break; + case AMULET_BURNINGRESIST: + messagePlayer(player, MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(6535)); + break; case RING_ADORNMENT: messagePlayer(player, MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(2384)); break; @@ -3145,7 +3765,7 @@ Item* itemPickup(const int player, Item* const item, Item* addToSpecificInventor } Item* item2; - if ( stats[player]->getProficiency(PRO_APPRAISAL) >= CAPSTONE_UNLOCK_LEVEL[PRO_APPRAISAL] ) + /*if ( stats[player]->getProficiency(PRO_APPRAISAL) >= CAPSTONE_UNLOCK_LEVEL[PRO_APPRAISAL] ) { if ( !(player != 0 && multiplayer == SERVER && !players[player]->isLocalPlayer()) ) { @@ -3160,7 +3780,7 @@ Item* itemPickup(const int player, Item* const item, Item* addToSpecificInventor } } } - } + }*/ if ( item->identified && !intro ) { @@ -3242,7 +3862,7 @@ Item* itemPickup(const int player, Item* const item, Item* addToSpecificInventor if (!itemCompare(item, item2, false)) { - if ( (itemTypeIsQuiver(item2->type) && (item->count + item2->count) >= QUIVER_MAX_AMMO_QTY) + if ( ((itemTypeIsQuiver(item2->type) || itemTypeIsThrownBall(item2->type)) && (item->count + item2->count) >= QUIVER_MAX_AMMO_QTY) || ((item2->type == TOOL_MAGIC_SCRAP || item2->type == TOOL_METAL_SCRAP) && (item->count + item2->count) >= SCRAP_MAX_STACK_QTY) ) { @@ -3273,18 +3893,20 @@ Item* itemPickup(const int player, Item* const item, Item* addToSpecificInventor if ( multiplayer == CLIENT && player >= 0 && players[player]->isLocalPlayer() && itemIsEquipped(item2, player) ) { // if incrementing qty and holding item, then send "equip" for server to update their count of your held item. - strcpy((char*)net_packet->data, "EQUS"); - SDLNet_Write32(static_cast(item2->type), &net_packet->data[4]); - SDLNet_Write32(static_cast(item2->status), &net_packet->data[8]); - SDLNet_Write32(static_cast(item2->beatitude), &net_packet->data[12]); - SDLNet_Write32(static_cast(item2->count), &net_packet->data[16]); - SDLNet_Write32(static_cast(item2->appearance), &net_packet->data[20]); - net_packet->data[24] = item2->identified; - net_packet->data[25] = player; - net_packet->address.host = net_server.host; - net_packet->address.port = net_server.port; - net_packet->len = 27; - sendPacketSafe(net_sock, -1, net_packet, 0); + Item** slot = itemSlot(stats[player], item2); + if ( slot ) + { + if ( slot == &stats[player]->weapon ) + { + clientSendEquipUpdateToServer(EQUIP_ITEM_SLOT_WEAPON, EQUIP_ITEM_SUCCESS_UPDATE_QTY, player, + item2->type, item2->status, item2->beatitude, item2->count, item2->appearance, item2->identified); + } + else if ( slot == &stats[player]->shield ) + { + clientSendEquipUpdateToServer(EQUIP_ITEM_SLOT_SHIELD, EQUIP_ITEM_SUCCESS_UPDATE_QTY, player, + item2->type, item2->status, item2->beatitude, item2->count, item2->appearance, item2->identified); + } + } } item2->ownerUid = item->ownerUid; if ( item->count <= 0 ) @@ -3338,65 +3960,41 @@ Item* itemPickup(const int player, Item* const item, Item* addToSpecificInventor } if ( !appearancesOfSimilarItems.empty() && item && item->type >= 0 && item->type < NUMITEMS ) { - Uint32 originalAppearance = item->appearance; - int originalVariation = originalAppearance % items[item->type].variations; + Item::itemFindUniqueAppearance(item, appearancesOfSimilarItems); + } - int tries = 100; - bool robot = false; - // we need to find a unique appearance within the list. - if ( item->type == TOOL_SENTRYBOT || item->type == TOOL_SPELLBOT || item->type == TOOL_GYROBOT - || item->type == TOOL_DUMMYBOT ) - { - robot = true; - item->appearance += (local_rng.rand() % 100000) * 10; - } - else + item2 = newItem(item->type, item->status, item->beatitude, item->count, item->appearance, item->identified, &stats[player]->inventory); + item2->ownerUid = item->ownerUid; + item2->notifyIcon = item->notifyIcon; + + if ( item2->type == TOOL_DUCK && !stats[player]->shield ) + { + bool shapeshifted = false; + if ( players[player] && players[player]->entity && players[player]->entity->effectShapeshift != NOTHING ) { - item->appearance = local_rng.rand(); - if ( item->appearance % items[item->type].variations != originalVariation ) - { - // we need to match the variation for the new appearance, take the difference so new varation matches - int change = (item->appearance % items[item->type].variations - originalVariation); - if ( item->appearance < change ) // underflow protection - { - item->appearance += items[item->type].variations; - } - item->appearance -= change; - int newVariation = item->appearance % items[item->type].variations; - assert(newVariation == originalVariation); - } + shapeshifted = true; } - auto it = appearancesOfSimilarItems.find(item->appearance); - while ( it != appearancesOfSimilarItems.end() && tries > 0 ) + + if ( !shapeshifted && !intro ) { - if ( robot ) - { - item->appearance += (local_rng.rand() % 100000) * 10; - } - else + useItem(item2, player); + auto& hotbar_t = players[player]->hotbar; + auto& hotbar = hotbar_t.slots(); + if ( hotbar_t.magicDuckHotbarSlot >= 0 ) { - item->appearance = local_rng.rand(); - if ( item->appearance % items[item->type].variations != originalVariation ) + hotbar[hotbar_t.magicDuckHotbarSlot].item = item2->uid; + for ( int i = 0; i < NUM_HOTBAR_SLOTS; ++i ) { - // we need to match the variation for the new appearance, take the difference so new varation matches - int change = (item->appearance % items[item->type].variations - originalVariation); - if ( item->appearance < change ) // underflow protection + if ( i != hotbar_t.magicDuckHotbarSlot && hotbar[i].item == item2->uid ) { - item->appearance += items[item->type].variations; + hotbar[i].item = 0; + hotbar[i].resetLastItem(); } - item->appearance -= change; - int newVariation = item->appearance % items[item->type].variations; - assert(newVariation == originalVariation); } } - it = appearancesOfSimilarItems.find(item->appearance); - --tries; } } - item2 = newItem(item->type, item->status, item->beatitude, item->count, item->appearance, item->identified, &stats[player]->inventory); - item2->ownerUid = item->ownerUid; - item2->notifyIcon = item->notifyIcon; return item2; } @@ -3411,7 +4009,11 @@ int Item::getMaxStackLimit(int player) const } int maxStack = 100; - if ( itemCategory(this) == THROWN || itemCategory(this) == GEM ) + if ( itemTypeIsThrownBall(this->type) ) + { + maxStack = QUIVER_MAX_AMMO_QTY - 1; + } + else if ( itemCategory(this) == THROWN || itemCategory(this) == GEM ) { maxStack = THROWN_GEM_MAX_STACK_QTY; } @@ -3439,7 +4041,7 @@ ItemStackResult getItemStackingBehaviorIndividualItemCheck(const int player, Ite if ( !itemCompare(itemToCheck, itemDestinationStack, false) ) { - if ( (itemTypeIsQuiver(itemDestinationStack->type) && (itemToCheck->count + itemDestinationStack->count) >= QUIVER_MAX_AMMO_QTY) + if ( ((itemTypeIsQuiver(itemDestinationStack->type) || itemTypeIsThrownBall(itemDestinationStack->type)) && (itemToCheck->count + itemDestinationStack->count) >= QUIVER_MAX_AMMO_QTY) || ((itemDestinationStack->type == TOOL_MAGIC_SCRAP || itemDestinationStack->type == TOOL_METAL_SCRAP) && (itemToCheck->count + itemDestinationStack->count) >= SCRAP_MAX_STACK_QTY) ) { @@ -3554,9 +4156,9 @@ ItemStackResult getItemStackingBehaviorIntoChest(const int player, Item* itemToC { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( !chest_inventory ) { @@ -3915,7 +4517,7 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const } if ( itemCategory(this) == MAGICSTAFF ) { - attack += 6; + attack += 1; } else if ( itemCategory(this) == GEM ) { @@ -3929,6 +4531,10 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const { attack += 4; } + else if ( type == ANELACE_SWORD ) + { + attack += 1; + } else if ( type == BRONZE_SWORD ) { attack += 4; @@ -3949,6 +4555,10 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const { attack += 5; } + else if ( type == RAPIER ) + { + attack += 7; + } else if ( type == IRON_MACE ) { attack += 5; @@ -3957,6 +4567,26 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const { attack += 5; } + else if ( type == SHILLELAGH_MACE ) + { + attack += 5; + } + else if ( type == BONE_AXE ) + { + attack += 5; + } + else if ( type == BONE_SWORD ) + { + attack += 5; + } + else if ( type == BONE_MACE ) + { + attack += 5; + } + else if ( type == BONE_SPEAR ) + { + attack += 5; + } else if ( type == STEEL_HALBERD ) { attack += 6; @@ -3973,6 +4603,58 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const { attack += 6; } + else if ( type == STEEL_FLAIL ) + { + attack += 6; + } + else if ( type == CLAYMORE_SWORD ) + { + attack += 6; + } + else if ( type == STEEL_FALSHION ) + { + attack += 6; + } + else if ( type == STEEL_GREATAXE ) + { + attack += 6; + } + else if ( type == LANCE_SPEAR ) + { + attack += 6; + } + else if ( type == BLACKIRON_AXE ) + { + attack += 6; + } + else if ( type == BLACKIRON_MACE ) + { + attack += 6; + } + else if ( type == BLACKIRON_SWORD ) + { + attack += 6; + } + else if ( type == BLACKIRON_TRIDENT ) + { + attack += 6; + } + else if ( type == SILVER_AXE ) + { + attack += 6; + } + else if ( type == SILVER_MACE ) + { + attack += 6; + } + else if ( type == SILVER_SWORD ) + { + attack += 6; + } + else if ( type == SILVER_GLAIVE ) + { + attack += 6; + } else if ( type == ARTIFACT_SWORD ) { return (attack + 2 + status * 2); @@ -3997,17 +4679,33 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const { attack += 6; } + else if ( type == BONE_SHORTBOW ) + { + attack += 6; + } else if ( type == CROSSBOW ) { attack += 7; } + else if ( type == BLACKIRON_CROSSBOW ) + { + attack += 7; + } else if ( type == LONGBOW ) { attack += 10; } + else if ( type == BRANCH_BOW ) + { + attack += 6; + } + else if ( type == BRANCH_BOW_INFECTED ) + { + attack += 8; + } else if ( type == HEAVY_CROSSBOW ) { - attack += 16; + attack += 14; } else if ( type == COMPOUND_BOW ) { @@ -4033,6 +4731,22 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const { attack += 6; } + else if ( type == BONE_THROWING ) + { + attack += 6; + } + else if ( type == BOLAS ) + { + attack += 3; + } + else if ( type == GREASE_BALL || type == DUST_BALL ) + { + attack += 3; + } + else if ( type == SLOP_BALL ) + { + attack += 3; + } else if ( type == IRON_DAGGER ) { attack += 8; @@ -4045,13 +4759,21 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const { attack += 10; } + else if ( type == SILVER_PLUMBATA ) + { + attack += 10; + } + else if ( type == BLACKIRON_DART ) + { + attack += 10; + } else if ( type == CRYSTAL_SHURIKEN ) { attack += 12; } else if ( type == TOOL_WHIP ) { - attack += 2; + attack += 6; } else if ( type == QUIVER_SILVER ) { @@ -4084,7 +4806,8 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const // old formula //attack *= (double)(status / 5.0); // - if ( itemCategory(this) != TOOL && itemCategory(this) != THROWN && itemCategory(this) != GEM && itemCategory(this) != POTION ) + if ( itemCategory(this) != TOOL && itemCategory(this) != THROWN && itemCategory(this) != GEM && itemCategory(this) != POTION + && itemCategory(this) != MAGICSTAFF ) { // new formula attack += status - 3; @@ -4096,6 +4819,9 @@ Sint32 Item::weaponGetAttack(const Stat* const wielder) const bool Item::doesItemProvideBeatitudeAC(ItemType type) { if ( itemTypeIsQuiver(type) || items[type].category == SPELLBOOK + || itemTypeIsFoci(type) + || itemTypeIsInstrument(type) + || type == TOOL_DUCK || items[type].category == AMULET ) { return false; @@ -4106,6 +4832,8 @@ bool Item::doesItemProvideBeatitudeAC(ItemType type) || type == HAT_PLUMED_CAP || type == HAT_BYCOCKET || type == HAT_CIRCLET + || type == HAT_CIRCLET_SORCERY + || type == HAT_CIRCLET_THAUMATURGY || type == HAT_CIRCLET_WISDOM || type == HAT_CROWN || type == HAT_LAURELS @@ -4131,15 +4859,6 @@ bool Item::doesItemProvideBeatitudeAC(ItemType type) return true; } -bool Item::doesItemProvidePassiveShieldBonus() const -{ - if ( itemTypeIsQuiver(type) || itemCategory(this) == SPELLBOOK ) - { - return false; - } - return true; -} - bool Item::doesPotionHarmAlliesOnThrown() const { switch ( type ) @@ -4243,6 +4962,11 @@ Sint32 Item::potionGetEffectDamage(Entity* my, Stat* myStats) const Sint32 Item::potionGetEffectDurationMinimum(Entity* my, Stat* myStats) const { + if ( type == GREASE_BALL ) + { + return 500; + } + if ( itemCategory(this) != POTION ) { return 1; @@ -4316,6 +5040,11 @@ Sint32 Item::potionGetEffectDurationMinimum(Entity* my, Stat* myStats) const Sint32 Item::potionGetEffectDurationMaximum(Entity* my, Stat* myStats) const { + if ( type == GREASE_BALL ) + { + return 750; + } + if ( itemCategory(this) != POTION ) { return 1; @@ -4527,6 +5256,70 @@ Sint32 Item::potionGetCursedEffectDurationRandom(Entity* my, Stat* myStats) cons return potionGetCursedEffectDurationMinimum(my, myStats) + (local_rng.rand() % (range)); } +Sint32 Item::getGoldValue() const +{ + if ( type >= 0 && type < NUMITEMS ) + { + if ( items[type].category == TOME_SPELL ) + { + int spellID = getTomeSpellID(); + if ( spellID > SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + if ( spell->difficulty >= 100 ) + { + return items[type].gold_value + 950; + } + else if ( spell->difficulty >= 80 ) + { + return items[type].gold_value + 550; + } + else if ( spell->difficulty >= 60 ) + { + return items[type].gold_value + 350; + } + else if ( spell->difficulty >= 40 ) + { + return items[type].gold_value + 150; + } + else if ( spell->difficulty >= 20 ) + { + return items[type].gold_value + 50; + } + } + } + } + if ( type == GEM_JEWEL ) + { + int value = items[type].gold_value; + if ( status == BROKEN ) + { + value = 0; + } + else if ( status == DECREPIT ) + { + value = 250; + } + else if ( status == WORN ) + { + value = 500; + } + else if ( status == SERVICABLE ) + { + value = 1000; + } + else if ( status == EXCELLENT ) + { + value = 2000; + } + return value; + } + return items[type].gold_value; + } + return 0; +} + Sint32 Item::getWeight() const { if ( type >= 0 && type < NUMITEMS ) @@ -4642,6 +5435,34 @@ Sint32 Item::armorGetAC(const Stat* const wielder) const { armor += 3; } + else if ( type == BONE_HELM ) + { + armor += 1; + } + else if ( type == BLACKIRON_HELM ) + { + armor += 3; + } + else if ( type == SILVER_HELM ) + { + armor += 3; + } + else if ( type == QUILTED_CAP ) + { + armor += 1; + } + else if ( type == CHAIN_COIF ) + { + armor += 1; + } + else if ( type == HAT_FELT ) + { + armor += 0; + } + else if ( type == HOOD_TEAL ) + { + armor += 0; + } else if ( type == LEATHER_BREASTPIECE ) { armor += 2; @@ -4707,7 +5528,7 @@ Sint32 Item::armorGetAC(const Stat* const wielder) const { armor += 1; } - else if ( type == IRON_BOOTS || type == IRON_BOOTS_WATERWALKING ) + else if ( type == IRON_BOOTS || type == IRON_BOOTS_WATERWALKING || type == CLEAT_BOOTS || type == TOOL_FRYING_PAN ) { armor += 2; } @@ -4715,19 +5536,79 @@ Sint32 Item::armorGetAC(const Stat* const wielder) const { armor += 3; } - else if ( type == WOODEN_SHIELD ) + else if ( type == BONE_BRACERS ) + { + armor += 1; + } + else if ( type == BLACKIRON_GAUNTLETS ) + { + armor += 3; + } + else if ( type == SILVER_GAUNTLETS ) + { + armor += 3; + } + else if ( type == QUILTED_GLOVES ) + { + armor += 1; + } + else if ( type == CHAIN_GLOVES ) + { + armor += 1; + } + else if ( type == BONE_BOOTS ) + { + armor += 1; + } + else if ( type == BLACKIRON_BOOTS ) + { + armor += 3; + } + else if ( type == SILVER_BOOTS ) + { + armor += 3; + } + else if ( type == QUILTED_BOOTS ) + { + armor += 1; + } + else if ( type == LOAFERS ) + { + armor += 0; + } + else if ( type == CHAIN_BOOTS ) + { + armor += 1; + } + else if ( type == WOODEN_SHIELD ) + { + armor += 1; + } + else if ( type == BRONZE_SHIELD ) + { + armor += 2; + } + else if ( type == IRON_SHIELD ) + { + armor += 3; + } + else if ( type == STEEL_SHIELD || type == STEEL_SHIELD_RESISTANCE ) + { + armor += 4; + } + else if ( type == SCUTUM ) { - armor += 1; + armor += 6; } - else if ( type == BRONZE_SHIELD ) + else if ( type == BONE_SHIELD ) { armor += 2; } - else if ( type == IRON_SHIELD ) + else if ( type == BLACKIRON_SHIELD ) { - armor += 3; + armor += 4; } - else if ( type == STEEL_SHIELD || type == STEEL_SHIELD_RESISTANCE ) + else if ( type == SILVER_SHIELD ) { armor += 4; } @@ -4779,6 +5660,10 @@ Sint32 Item::armorGetAC(const Stat* const wielder) const { armor += 0; } + else if ( type == CLOAK_DENDRITE ) + { + armor += 1; + } else if ( type == MIRROR_SHIELD ) { armor += 0; @@ -4795,6 +5680,58 @@ Sint32 Item::armorGetAC(const Stat* const wielder) const { armor += 3; } + else if ( type == BANDIT_BREASTPIECE ) + { + armor += 2; + } + else if ( type == TUNIC_BLOUSE) + { + armor += 0; + } + else if ( type == BONE_BREASTPIECE) + { + armor += 2; + } + else if ( type == BLACKIRON_BREASTPIECE) + { + armor += 4; + } + else if ( type == SILVER_BREASTPIECE) + { + armor += 4; + } + else if ( type == IRON_PAULDRONS) + { + armor += 2; + } + else if ( type == QUILTED_GAMBESON) + { + armor += 2; + } + else if ( type == ROBE_CULTIST) + { + armor += 1; + } + else if ( type == ROBE_HEALER) + { + armor += 1; + } + else if ( type == ROBE_MONK) + { + armor += 1; + } + else if ( type == ROBE_WIZARD) + { + armor += 1; + } + else if ( type == SHAWL) + { + armor += 1; + } + else if ( type == CHAIN_HAUBERK) + { + armor += 2; + } //armor *= (double)(item->status/5.0); if ( wielder ) @@ -4844,6 +5781,11 @@ bool Item::canUnequip(const Stat* const wielder) } } + if ( type == TOOL_DUCK ) + { + return true; + } + if ( wielder ) { if ( wielder->type == AUTOMATON ) @@ -4861,7 +5803,12 @@ bool Item::canUnequip(const Stat* const wielder) Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_APPRAISED, type, 1); } } + bool prevIdentified = identified; identified = true; + if ( !prevIdentified ) + { + Item::onItemIdentified(player, this); + } return false; } else @@ -4880,7 +5827,12 @@ bool Item::canUnequip(const Stat* const wielder) Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_APPRAISED, type, 1); } } + bool prevIdentified = identified; identified = true; + if ( !prevIdentified ) + { + Item::onItemIdentified(player, this); + } return false; } @@ -4897,7 +5849,7 @@ bool Item::canUnequip(const Stat* const wielder) int Item::buyValue(const int player) const { - int value = items[type].value; // base value + int value = this->getGoldValue(); // base value // identified bonus if ( identified ) @@ -4949,10 +5901,10 @@ int Item::buyValue(const int player) const if ( itemTypeIsQuiver(type) ) { - return std::max(value, items[type].value) * count; + return std::max(value, this->getGoldValue()) * count; } - return std::max(value, items[type].value); + return std::max(value, this->getGoldValue()); } /*------------------------------------------------------------------------------- @@ -4965,7 +5917,7 @@ int Item::buyValue(const int player) const int Item::sellValue(const int player) const { - int value = items[type].value; // base value + int value = this->getGoldValue(); // base value // identified bonus if ( identified ) @@ -4976,7 +5928,7 @@ int Item::sellValue(const int player) const { if ( itemCategory(this) == GEM ) { - value = items[GEM_GLASS].value; + value = items[GEM_GLASS].gold_value; } else { @@ -4988,21 +5940,24 @@ int Item::sellValue(const int player) const value *= 1.f + beatitude / 20.f; value *= (static_cast(status) + 5) / 10.f; - // trading bonus - value *= (50 + stats[player]->getModifiedProficiency(PRO_TRADING)) / 150.f; + if ( player >= 0 ) + { + // trading bonus + value *= (50 + stats[player]->getModifiedProficiency(PRO_TRADING)) / 150.f; - // charisma bonus - value *= 1.f + statGetCHR(stats[player], players[player]->entity) / 20.f; + // charisma bonus + value *= 1.f + statGetCHR(stats[player], players[player]->entity) / 20.f; + } // result value = std::max(1, value); if ( itemTypeIsQuiver(type) ) { - return std::min(value, items[type].value) * count; + return std::min(value, this->getGoldValue()) * count; } - return std::min(value, items[type].value); + return std::min(value, this->getGoldValue()); } /*------------------------------------------------------------------------------- @@ -5106,16 +6061,19 @@ void Item::applyLockpickToWall(const int player, const int x, const int y) const { const int skill = std::max(1, stats[player]->getModifiedProficiency(PRO_LOCKPICKING) / 10); bool failed = false; - if ( skill < 2 || local_rng.rand() % skill == 0 ) // 20 skill requirement. + if ( entity->actTrapSabotaged == 0 ) { - // failed. - const Uint32 color = makeColorRGB(255, 0, 0); - messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(3871)); // trap fires. - if ( skill < 2 ) + if ( skill < 2 || local_rng.rand() % skill == 0 ) // 20 skill requirement. { - messagePlayer(player, MESSAGE_INTERACTION, Language::get(3887)); // not skilled enough. + // failed. + const Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(player, MESSAGE_INTERACTION, color, Language::get(3871)); // trap fires. + if ( skill < 2 ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(3887)); // not skilled enough. + } + failed = true; } - failed = true; } if ( failed ) @@ -5134,7 +6092,8 @@ void Item::applyLockpickToWall(const int player, const int x, const int y) const } // degrade lockpick. - if ( !(stats[player]->weapon->type == TOOL_SKELETONKEY) && (local_rng.rand() % 10 == 0 || (failed && local_rng.rand() % 4 == 0)) ) + if ( !(stats[player]->weapon->type == TOOL_SKELETONKEY) && (local_rng.rand() % 10 == 0 || (failed && local_rng.rand() % 4 == 0)) + && !(players[player]->entity && players[player]->entity->spellEffectPreserveItem(stats[player]->weapon)) ) { if ( players[player]->isLocalPlayer() ) { @@ -5159,9 +6118,10 @@ void Item::applyLockpickToWall(const int player, const int x, const int y) const strcpy((char*)net_packet->data, "ARMR"); net_packet->data[4] = 5; net_packet->data[5] = stats[player]->weapon->status; + SDLNet_Write16((int)stats[player]->weapon->type, &net_packet->data[6]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; + net_packet->len = 8; sendPacketSafe(net_sock, -1, net_packet, player - 1); } } @@ -5335,7 +6295,7 @@ void createCustomInventory(Stat* const stats, const int itemLimit, BaronyRNG& rn } } -node_t* itemNodeInInventory(const Stat* const myStats, Sint32 itemToFind, const Category cat) +node_t* itemNodeInInventory(const Stat* const myStats, Sint32 itemToFind, const Category cat, bool randomSlot) { if ( myStats == nullptr ) { @@ -5344,7 +6304,7 @@ node_t* itemNodeInInventory(const Stat* const myStats, Sint32 itemToFind, const node_t* node = nullptr; node_t* nextnode = nullptr; - + std::vector allNodes; for ( node = myStats->inventory.first; node != nullptr; node = nextnode ) { nextnode = node->next; @@ -5353,15 +6313,37 @@ node_t* itemNodeInInventory(const Stat* const myStats, Sint32 itemToFind, const { if ( cat >= WEAPON && itemCategory(item) == cat ) { - return node; + if ( randomSlot ) + { + allNodes.push_back(node); + } + else + { + return node; + } } else if ( itemToFind >= 0 && item->type == static_cast(itemToFind) ) { - return node; + if ( randomSlot ) + { + allNodes.push_back(node); + } + else + { + return node; + } } } } + if ( randomSlot ) + { + if ( allNodes.size() > 0 ) + { + return allNodes.at(local_rng.rand() % allNodes.size()); + } + } + return nullptr; } @@ -5463,6 +6445,10 @@ bool isRangedWeapon(const ItemType type) case LONGBOW: case COMPOUND_BOW: case HEAVY_CROSSBOW: + case BRANCH_BOW: + case BRANCH_BOW_INFECTED: + case BLACKIRON_CROSSBOW: + case BONE_SHORTBOW: return true; default: return false; @@ -5656,7 +6642,7 @@ ItemType itemTypeWithinGoldValue(const int cat, const int minValue, const int ma bool pickAnyCategory = false; int c; - if ( cat < -1 || cat >= NUMCATEGORIES ) + if ( cat < -1 || cat >= Category::CATEGORY_MAX ) { printlog("warning: pickItemWithinGoldValue() called with bad category value!\n"); return GEM_ROCK; @@ -5670,9 +6656,9 @@ ItemType itemTypeWithinGoldValue(const int cat, const int minValue, const int ma // find highest value of items in category for ( c = 0; c < NUMITEMS; ++c ) { - if ( items[c].category == cat || (pickAnyCategory && items[c].category != SPELL_CAT) ) + if ( items[c].category == cat || (pickAnyCategory && items[c].category < Category::CATEGORY_MAX - 2) ) { - if ( items[c].value >= minValue && items[c].value <= maxValue && items[c].level != -1 ) + if ( items[c].gold_value >= minValue && items[c].gold_value <= maxValue && items[c].level != -1 ) { // chance true for an item if it's not forbidden from the global item list. chances[c] = true; @@ -5776,6 +6762,7 @@ bool Item::shouldItemStack(const int player, bool ignoreStackLimit) const && this->type != TOOL_PICKAXE && this->type != TOOL_ALEMBIC && this->type != TOOL_TINKERING_KIT + && this->type != TOOL_FRYING_PAN && this->type != ENCHANTED_FEATHER && this->type != TOOL_LANTERN && this->type != TOOL_GLASSES @@ -5790,6 +6777,7 @@ bool Item::shouldItemStack(const int player, bool ignoreStackLimit) const && this->type != TOOL_PICKAXE && this->type != TOOL_ALEMBIC && this->type != TOOL_TINKERING_KIT + && this->type != TOOL_FRYING_PAN && this->type != ENCHANTED_FEATHER && this->type != TOOL_LANTERN && this->type != TOOL_GLASSES @@ -5801,6 +6789,14 @@ bool Item::shouldItemStack(const int player, bool ignoreStackLimit) const { // THROWN, GEM, TOOLS, POTIONS should stack when equipped. // otherwise most equippables should not stack. + if ( itemTypeIsThrownBall(this->type) ) + { + if ( !ignoreStackLimit && count >= QUIVER_MAX_AMMO_QTY - 1 ) + { + return false; + } + return true; + } if ( itemCategory(this) == THROWN || itemCategory(this) == GEM ) { if ( !ignoreStackLimit && count >= THROWN_GEM_MAX_STACK_QTY ) @@ -5828,6 +6824,10 @@ bool Item::shouldItemStack(const int player, bool ignoreStackLimit) const { return false; } + else if ( items[type].hasAttribute("no_stack") ) + { + return false; + } return true; } } @@ -5861,28 +6861,11 @@ bool isItemEquippableInShieldSlot(const Item* const item) return false; } - if ( itemTypeIsQuiver(item->type) ) - { - return true; - } - - switch ( item->type ) + if ( item->type < 0 || item->type >= NUMITEMS ) { - case WOODEN_SHIELD: - case BRONZE_SHIELD: - case IRON_SHIELD: - case STEEL_SHIELD: - case STEEL_SHIELD_RESISTANCE: - case MIRROR_SHIELD: - case CRYSTAL_SHIELD: - case TOOL_TORCH: - case TOOL_LANTERN: - case TOOL_CRYSTALSHARD: - return true; - default: - break; + return false; } - return false; + return (items[item->type].item_slot == EQUIPPABLE_IN_SLOT_SHIELD); } bool Item::usableWhileShapeshifted(const Stat* const wielder) const @@ -5900,9 +6883,14 @@ bool Item::usableWhileShapeshifted(const Stat* const wielder) const case TOOL: case BOOK: case SCROLL: + if ( wielder->type == CREATURE_IMP && itemTypeIsFoci(type) ) + { + return true; + } return false; case MAGICSTAFF: case SPELLBOOK: + case TOME_SPELL: { if (wielder->type == CREATURE_IMP) { @@ -5931,6 +6919,51 @@ bool Item::usableWhileShapeshifted(const Stat* const wielder) const return false; } +int Item::getTomeSpellID() const +{ + if ( type == TOME_SORCERY ) + { + auto find = spellTomeAppearanceToID[PRO_SORCERY].find(appearance % TOME_APPEARANCE_MAX); + if ( find == spellTomeAppearanceToID[PRO_SORCERY].end() ) + { + return SPELL_FORCEBOLT; + } + return find->second; + } + else if ( type == TOME_MYSTICISM ) + { + auto find = spellTomeAppearanceToID[PRO_MYSTICISM].find(appearance % TOME_APPEARANCE_MAX); + if ( find == spellTomeAppearanceToID[PRO_MYSTICISM].end() ) + { + return SPELL_SLOW; + } + return find->second; + } + else if ( type == TOME_THAUMATURGY ) + { + auto find = spellTomeAppearanceToID[PRO_THAUMATURGY].find(appearance % TOME_APPEARANCE_MAX); + if ( find == spellTomeAppearanceToID[PRO_THAUMATURGY].end() ) + { + return SPELL_LIGHT; + } + return find->second; + } + return SPELL_NONE; +} + +const char* Item::getTomeLabel() const +{ + if ( itemCategory(this) == TOME_SPELL ) + { + int spellID = getTomeSpellID(); + if ( auto spell = getSpellFromID(spellID) ) + { + return spell->getSpellName(true); + } + } + return ""; +} + char* Item::getScrollLabel() const { if ( enchantedFeatherScrollsShuffled.empty() ) @@ -5964,6 +6997,27 @@ char* Item::getScrollLabel() const return scroll_label[chosenLabel]; } +bool itemSpriteIsFociThirdPersonModel(const int sprite) +{ + static std::set fociModels; + if ( fociModels.size() == 0 ) + { + for ( int i = 0; i < NUMITEMS; ++i ) + { + if ( itemTypeIsFoci((ItemType)i) ) + { + fociModels.insert(items[i].index); + if ( items[i].indexShort >= 0 ) + { + fociModels.insert(items[i].indexShort); + } + } + } + } + + return fociModels.find(sprite) != fociModels.end(); +} + bool itemSpriteIsQuiverThirdPersonModel(const int sprite) { for ( int i = QUIVER_SILVER; i <= QUIVER_HUNTING; ++i ) @@ -5976,6 +7030,16 @@ bool itemSpriteIsQuiverThirdPersonModel(const int sprite) return true; } } + for ( int i = QUIVER_BONE; i <= QUIVER_BLACKIRON; ++i ) + { + if ( sprite == items[i].index + || sprite == items[i].index + 1 + || sprite == items[i].index + 2 + || sprite == items[i].index + 3 ) + { + return true; + } + } return false; } @@ -5988,12 +7052,56 @@ bool itemSpriteIsQuiverBaseThirdPersonModel(const int sprite) return true; } } + for ( int i = QUIVER_BONE; i <= QUIVER_BLACKIRON; ++i ) + { + if ( sprite == items[i].index + 1 ) + { + return true; + } + } return false; } bool itemTypeIsQuiver(const ItemType type) { - return (type >= QUIVER_SILVER && type <= QUIVER_HUNTING); + return (type >= QUIVER_SILVER && type <= QUIVER_HUNTING) || (type >= QUIVER_BONE && type <= QUIVER_BLACKIRON); +} + +bool itemTypeIsFoci(const ItemType type) +{ + switch ( type ) + { + case TOOL_FOCI_FIRE: + case TOOL_FOCI_SNOW: + case TOOL_FOCI_NEEDLES: + case TOOL_FOCI_ARCS: + case TOOL_FOCI_SAND: + case TOOL_FOCI_DARK_LIFE: + case TOOL_FOCI_DARK_RIFT: + case TOOL_FOCI_DARK_SILENCE: + case TOOL_FOCI_DARK_VENGEANCE: + case TOOL_FOCI_DARK_SUPPRESS: + case TOOL_FOCI_LIGHT_PEACE: + case TOOL_FOCI_LIGHT_JUSTICE: + case TOOL_FOCI_LIGHT_PROVIDENCE: + case TOOL_FOCI_LIGHT_PURITY: + case TOOL_FOCI_LIGHT_SANCTUARY: + return true; + break; + default: + break; + } + return false; +} + +bool itemTypeIsInstrument(const ItemType type) +{ + return (type >= INSTRUMENT_FLUTE && type <= INSTRUMENT_HORN); +} + +bool itemTypeIsThrownBall(const ItemType type) +{ + return type == DUST_BALL || type == GREASE_BALL || type == SLOP_BALL; } real_t rangedAttackGetSpeedModifier(const Stat* const myStats) @@ -6031,6 +7139,12 @@ real_t rangedAttackGetSpeedModifier(const Stat* const myStats) { bowModifier = 1.25; } + else if ( myStats->weapon->type == BRANCH_BOW + || myStats->weapon->type == BRANCH_BOW_INFECTED + || myStats->weapon->type == BONE_SHORTBOW ) + { + bowModifier = 1.0; + } else if ( myStats->weapon->type == ARTIFACT_BOW ) { bowModifier = 0.75; @@ -6045,6 +7159,11 @@ real_t rangedAttackGetSpeedModifier(const Stat* const myStats) bowModifier = 0.75; arrowModifier = 0.0; // no impact on slings. } + else if ( myStats->weapon->type == CROSSBOW + || myStats->weapon->type == BLACKIRON_CROSSBOW ) + { + arrowModifier /= 2; + } else if ( myStats->weapon->type == HEAVY_CROSSBOW ) { bowModifier = 0.4; @@ -6093,7 +7212,7 @@ real_t getArtifactWeaponEffectChance(const ItemType type, Stat& wielder, real_t* const real_t percent = (wielder.getModifiedProficiency(PRO_SWORD)); //0-100% if ( effectAmount ) { - *effectAmount = (wielder.getModifiedProficiency(PRO_SWORD)) / 200.f + 0.5; //0.5x-1.0x add to weapon multiplier + *effectAmount = (wielder.getModifiedProficiency(PRO_SWORD)) / 400.f + 0.25; //0.25x-0.5x add to weapon multiplier } return percent; @@ -6353,6 +7472,29 @@ void clientSendAppearanceUpdateToServer(const int player, Item* item, const bool sendPacketSafe(net_sock, -1, net_packet, 0); } +void clientSendItemTypeUpdateToServer(const int player, Item* item, ItemType prevItemType) +{ + if ( multiplayer != CLIENT ) { return; } + if ( !item || !itemIsEquipped(item, player) || items[item->type].item_slot == NO_EQUIP ) + { + return; + } + strcpy((char*)net_packet->data, "EQUT"); + SDLNet_Write32(static_cast(prevItemType), &net_packet->data[4]); + SDLNet_Write32(static_cast(item->status), &net_packet->data[8]); + SDLNet_Write32(static_cast(item->beatitude), &net_packet->data[12]); + SDLNet_Write32(static_cast(item->count), &net_packet->data[16]); + SDLNet_Write32(static_cast(item->appearance), &net_packet->data[20]); + net_packet->data[24] = item->identified; + net_packet->data[25] = player; + net_packet->data[26] = items[item->type].item_slot; + SDLNet_Write32(static_cast(item->type), &net_packet->data[27]); + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 31; + sendPacketSafe(net_sock, -1, net_packet, 0); +} + void clientSendEquipUpdateToServer(const EquipItemSendToServerSlot slot, const EquipItemResult equipType, const int player, const ItemType type, const Status status, const Sint16 beatitude, const int count, const Uint32 appearance, const bool identified) { @@ -6432,6 +7574,11 @@ void clientUnequipSlotAndUpdateServer(const int player, const EquipItemSendToSer item->type, item->status, item->beatitude, item->count, item->appearance, item->identified); } +int Item::getDuckPlayer() const +{ + return (int)(appearance % items[type].variations) % MAXPLAYERS; +} + int Item::getLootBagPlayer() const { return (int)(appearance & 0xF) % MAXPLAYERS; @@ -6453,4 +7600,106 @@ int Item::getLootBagNumItems() const return 0; } return 0; +} + +void Item::itemFindUniqueAppearance(Item* tempItem, std::unordered_set& appearancesOfSimilarItems) +{ + if ( !appearancesOfSimilarItems.empty() && tempItem ) + { + Uint32 originalAppearance = tempItem->appearance; + int originalVariation = originalAppearance % items[tempItem->type].variations; + + int tries = 100; + bool robot = false; + // we need to find a unique appearance within the list. + if ( tempItem->type == TOOL_SENTRYBOT || tempItem->type == TOOL_SPELLBOT || tempItem->type == TOOL_GYROBOT + || tempItem->type == TOOL_DUMMYBOT ) + { + robot = true; + tempItem->appearance += (local_rng.rand() % 100000) * 10; + } + else if ( tempItem->type == MAGICSTAFF_SCEPTER ) + { + tempItem->appearance = ((local_rng.rand() % 10000) * (MAGICSTAFF_SCEPTER_CHARGE_MAX)) + (originalAppearance % MAGICSTAFF_SCEPTER_CHARGE_MAX); + } + else if ( itemCategory(tempItem) == TOME_SPELL ) + { + tempItem->appearance = ((local_rng.rand() % 10000) * (TOME_APPEARANCE_MAX)) + (originalAppearance % TOME_APPEARANCE_MAX); + } + else + { + tempItem->appearance = local_rng.rand(); + if ( tempItem->appearance % items[tempItem->type].variations != originalVariation ) + { + // we need to match the variation for the new appearance, take the difference so new varation matches + int change = (tempItem->appearance % items[tempItem->type].variations - originalVariation); + if ( tempItem->appearance < change ) // underflow protection + { + tempItem->appearance += items[tempItem->type].variations; + } + tempItem->appearance -= change; + int newVariation = tempItem->appearance % items[tempItem->type].variations; + assert(newVariation == originalVariation); + } + } + auto it = appearancesOfSimilarItems.find(tempItem->appearance); + while ( it != appearancesOfSimilarItems.end() && tries > 0 ) + { + if ( robot ) + { + tempItem->appearance += (local_rng.rand() % 100000) * 10; + } + else if ( tempItem->type == MAGICSTAFF_SCEPTER ) + { + tempItem->appearance = ((local_rng.rand() % 10000) * (MAGICSTAFF_SCEPTER_CHARGE_MAX)) + (originalAppearance % MAGICSTAFF_SCEPTER_CHARGE_MAX); + } + else if ( itemCategory(tempItem) == TOME_SPELL ) + { + tempItem->appearance = ((local_rng.rand() % 10000) * (TOME_APPEARANCE_MAX)) + (originalAppearance % TOME_APPEARANCE_MAX); + } + else + { + tempItem->appearance = local_rng.rand(); + if ( tempItem->appearance % items[tempItem->type].variations != originalVariation ) + { + // we need to match the variation for the new appearance, take the difference so new varation matches + int change = (tempItem->appearance % items[tempItem->type].variations - originalVariation); + if ( tempItem->appearance < change ) // underflow protection + { + tempItem->appearance += items[tempItem->type].variations; + } + tempItem->appearance -= change; + int newVariation = tempItem->appearance % items[tempItem->type].variations; + assert(newVariation == originalVariation); + } + } + it = appearancesOfSimilarItems.find(tempItem->appearance); + --tries; + } + } +} + +void Item::onItemIdentified(int player, Item* tempItem) +{ + if ( player >= 0 && player < MAXPLAYERS && players[player]->isLocalPlayer() && stats[player] ) + { + std::unordered_set appearancesOfSimilarItems; + for ( node_t* node = stats[player]->inventory.first; node != NULL; node = node->next ) + { + Item* item2 = static_cast(node->element); + if ( item2 && item2 != tempItem && !itemCompare(tempItem, item2, true) ) + { + // items are the same (incl. appearance!) + // if they shouldn't stack, we need to change appearance of the new item. + appearancesOfSimilarItems.insert(item2->appearance); + } + } + + Item::itemFindUniqueAppearance(tempItem, appearancesOfSimilarItems); + + if ( multiplayer == CLIENT && itemIsEquipped(tempItem, player) && players[player]->paperDoll.isItemOnDoll(*tempItem) ) + { + clientSendAppearanceUpdateToServer(player, tempItem, true); + } + } } \ No newline at end of file diff --git a/src/items.hpp b/src/items.hpp index 50a39a82f..858c9671e 100644 --- a/src/items.hpp +++ b/src/items.hpp @@ -13,6 +13,7 @@ #include "main.hpp" #include "prng.hpp" +#include "game.hpp" class Entity; // forward declare class Stat; // forward declare @@ -351,11 +352,203 @@ typedef enum ItemType HAT_HOOD_WHISPERS, RING_RESOLVE, CLOAK_GUARDIAN, - MASK_MARIGOLD + MASK_MARIGOLD, + KEY_STONE, + KEY_BONE, + KEY_BRONZE, + KEY_IRON, + KEY_SILVER, + KEY_GOLD, + KEY_CRYSTAL, + KEY_MACHINE, + TOOL_FOCI_FIRE, + INSTRUMENT_FLUTE, + INSTRUMENT_LYRE, + INSTRUMENT_DRUM, + INSTRUMENT_LUTE, + INSTRUMENT_HORN, + RAPIER, + AMULET_BURNINGRESIST, + GREASE_BALL, + BRANCH_STAFF, + BRANCH_BOW, + BRANCH_BOW_INFECTED, + DUST_BALL, + BOLAS, + STEEL_FLAIL, + FOOD_RATION, + FOOD_RATION_SPICY, + FOOD_RATION_SOUR, + FOOD_RATION_BITTER, + FOOD_RATION_HEARTY, + FOOD_RATION_HERBAL, + FOOD_RATION_SWEET, + SLOP_BALL, + TOOL_FRYING_PAN, + CLEAT_BOOTS, + BANDIT_BREASTPIECE, + TUNIC_BLOUSE, + BONE_BREASTPIECE, + BLACKIRON_BREASTPIECE, + SILVER_BREASTPIECE, + IRON_PAULDRONS, + QUILTED_GAMBESON, + ROBE_CULTIST, + ROBE_HEALER, + ROBE_MONK, + ROBE_WIZARD, + SHAWL, + CHAIN_HAUBERK, + BONE_BRACERS, + BLACKIRON_GAUNTLETS, + SILVER_GAUNTLETS, + QUILTED_GLOVES, + CHAIN_GLOVES, + BONE_BOOTS, + BLACKIRON_BOOTS, + SILVER_BOOTS, + QUILTED_BOOTS, + LOAFERS, + CHAIN_BOOTS, + SCUTUM, + BONE_SHIELD, + BLACKIRON_SHIELD, + SILVER_SHIELD, + CLOAK_DENDRITE, + BONE_HELM, + BLACKIRON_HELM, + SILVER_HELM, + HAT_FELT, + QUILTED_CAP, + HOOD_TEAL, + CHAIN_COIF, + FOOD_SHROOM, + FOOD_NUT, + TOOL_FOCI_SNOW, + TOOL_FOCI_NEEDLES, + TOOL_FOCI_ARCS, + TOOL_FOCI_SAND, + TOOL_FOCI_DARK_LIFE, + TOOL_FOCI_DARK_RIFT, + TOOL_FOCI_DARK_SILENCE, + TOOL_FOCI_DARK_VENGEANCE, + TOOL_FOCI_DARK_SUPPRESS, + TOOL_FOCI_LIGHT_PEACE, + TOOL_FOCI_LIGHT_JUSTICE, + TOOL_FOCI_LIGHT_PROVIDENCE, + TOOL_FOCI_LIGHT_PURITY, + TOOL_FOCI_LIGHT_SANCTUARY, + MAGICSTAFF_SCEPTER, + TOME_SORCERY, + TOME_MYSTICISM, + TOME_THAUMATURGY, + HAT_CIRCLET_SORCERY, + HAT_CIRCLET_THAUMATURGY, + TOOL_DUCK, + SHILLELAGH_MACE, + CLAYMORE_SWORD, + ANELACE_SWORD, + LANCE_SPEAR, + STEEL_FALSHION, + STEEL_GREATAXE, + BLACKIRON_AXE, + BLACKIRON_CROSSBOW, + BLACKIRON_DART, + BLACKIRON_MACE, + BLACKIRON_SWORD, + BLACKIRON_TRIDENT, + BONE_AXE, + BONE_MACE, + BONE_SHORTBOW, + BONE_SPEAR, + BONE_SWORD, + BONE_THROWING, + SILVER_AXE, + SILVER_GLAIVE, + SILVER_MACE, + SILVER_PLUMBATA, + SILVER_SWORD, + QUIVER_BONE, + QUIVER_BLACKIRON, + GEM_JEWEL, + SPELLBOOK_METEOR, + SPELLBOOK_ICE_WAVE, + SPELLBOOK_GUARD_BODY, + SPELLBOOK_GUARD_SPIRIT, + SPELLBOOK_DIVINE_GUARD, + SPELLBOOK_PROF_NIMBLENESS, + SPELLBOOK_PROF_GREATER_MIGHT, + SPELLBOOK_PROF_COUNSEL, + SPELLBOOK_PROF_STURDINESS, + SPELLBOOK_BLESS_FOOD, + SPELLBOOK_PINPOINT, + SPELLBOOK_DONATION, + SPELLBOOK_SCRY_ALLIES, + SPELLBOOK_SCRY_TRAPS, + SPELLBOOK_SCRY_TREASURES, + SPELLBOOK_DETECT_ENEMY, + SPELLBOOK_TURN_UNDEAD, + SPELLBOOK_HEAL_OTHER, + SPELLBOOK_BLOOD_WARD, + SPELLBOOK_DIVINE_ZEAL, + SPELLBOOK_MAXIMISE, + SPELLBOOK_MINIMISE, + SPELLBOOK_INCOHERENCE, + SPELLBOOK_OVERCHARGE, + SPELLBOOK_ENVENOM_WEAPON, + SPELLBOOK_PSYCHIC_SPEAR, + SPELLBOOK_DEFY_FLESH, + SPELLBOOK_GREASE_SPRAY, + SPELLBOOK_BLOOD_WAVES, + SPELLBOOK_COMMAND, + SPELLBOOK_METALLURGY, + SPELLBOOK_FORGE_KEY, + SPELLBOOK_RESHAPE_WEAPON, + SPELLBOOK_ALTER_ARROW, + SPELLBOOK_VOID_CHEST, + SPELLBOOK_LEAD_BOLT, + SPELLBOOK_NUMBING_BOLT, + SPELLBOOK_CURSE_FLESH, + SPELLBOOK_COWARDICE, + SPELLBOOK_SEEK_ALLY, + SPELLBOOK_DEEP_SHADE, + SPELLBOOK_SPIRIT_WEAPON, + SPELLBOOK_SPORES, + SPELLBOOK_WINDGATE, + SPELLBOOK_TELEKINESIS, + SPELLBOOK_DISARM, + SPELLBOOK_ABUNDANCE, + SPELLBOOK_PRESERVE, + SPELLBOOK_SABOTAGE, + SPELLBOOK_MIST_FORM, + SPELLBOOK_FORCE_SHIELD, + SPELLBOOK_SPLINTER_GEAR, + SPELLBOOK_ATTRACT_ITEMS, + SPELLBOOK_ABSORB_MAGIC, + SPELLBOOK_TUNNEL, + SPELLBOOK_NULL_AREA, + SPELLBOOK_FIRE_SPRITE, + SPELLBOOK_SPIN, + SPELLBOOK_CLEANSE_FOOD, + SPELLBOOK_FLAME_CLOAK, + SPELLBOOK_LIGHTNING_BOLT, + SPELLBOOK_DISRUPT_EARTH, + SPELLBOOK_FIRE_WALL, + SPELLBOOK_SLAM, + SPELLBOOK_IGNITE, + SPELLBOOK_SHATTER_OBJECTS, + SPELLBOOK_KINETIC_FIELD, + SPELLBOOK_THORNS, + SPELLBOOK_MAGICIANS_ARMOR, + SPELLBOOK_HEAL_MINOR, + SPELLBOOK_SIGIL, + SPELLBOOK_SANCTUARY, + SPELLBOOK_HOLY_BEAM, + SPELLBOOK_DOMINATE, + ITEM_ENUM_MAX } ItemType; -const int NUMITEMS = 332; +const int NUMITEMS = ITEM_ENUM_MAX; -//NOTE: If you change this, make sure to update NUMCATEGORIES in game.h to reflect the total number of categories. Not doing that will make bad things happen. typedef enum Category { WEAPON, @@ -371,7 +564,9 @@ typedef enum Category TOOL, FOOD, BOOK, - SPELL_CAT + SPELL_CAT, + TOME_SPELL, + CATEGORY_MAX } Category; typedef enum Status @@ -458,6 +653,7 @@ class Item bool playerSoldItemToShop = false; // if item was sold to a shopkeeper bool itemHiddenFromShop = false; // if item needs to be hidden in shop view bool notifyIcon = false; // if item draws exclamation as a 'new' untouched item + bool spellNotifyIcon = false; // if spell can level you up Uint8 itemRequireTradingSkillInShop = 0; // if item hidden in shop view until player has trading req bool itemSpecialShopConsumable = false; // if item is extra non-standard inventory consumable @@ -484,6 +680,8 @@ class Item int sellValue(int player) const; bool usableWhileShapeshifted(const Stat* wielder = nullptr) const; char* getScrollLabel() const; + const char* getTomeLabel() const; + int getTomeSpellID() const; void apply(int player, Entity* entity); void applyLockpickToWall(int player, int x, int y) const; @@ -505,7 +703,6 @@ class Item bool isShield() const; static bool doesItemProvideBeatitudeAC(ItemType type); - bool doesItemProvidePassiveShieldBonus() const; bool doesPotionHarmAlliesOnThrown() const; Sint32 potionGetEffectHealth(Entity* my, Stat* myStats) const; @@ -516,14 +713,17 @@ class Item Sint32 potionGetCursedEffectDurationMinimum(Entity* my, Stat* myStats) const; Sint32 potionGetCursedEffectDurationMaximum(Entity* my, Stat* myStats) const; Sint32 potionGetCursedEffectDurationRandom(Entity* my, Stat* myStats) const; + static int getBaseFoodSatiation(ItemType type); Sint32 getWeight() const; + Sint32 getGoldValue() const; void foodTinGetDescriptionIndices(int* a, int* b, int* c) const; void foodTinGetDescription(std::string& cookingMethod, std::string& protein, std::string& sides) const; int foodGetPukeChance(Stat* eater) const; int getLootBagPlayer() const; int getLootBagNumItems() const; + int getDuckPlayer() const; enum ItemBombPlacement : int { @@ -549,9 +749,12 @@ class Item }; void applyBomb(Entity* parent, ItemType type, ItemBombPlacement placement, ItemBombFacingDirection dir, Entity* thrown, Entity* onEntity); void applyTinkeringCreation(Entity* parent, Entity* thrown); + void applyDuck(Uint32 parentUid, real_t x, real_t y, Entity* hitentity, bool onLevelRespawn); bool unableToEquipDueToSwapWeaponTimer(const int player) const; bool tinkeringBotIsMaxHealth() const; bool isTinkeringItemWithThrownLimit() const; + static void onItemIdentified(int player, Item* tempItem); + static void itemFindUniqueAppearance(Item* tempItem, std::unordered_set& appearancesOfSimilarItems); }; extern Uint32 itemuids; @@ -566,7 +769,7 @@ class ItemGeneric int fpindex; // first person model int variations; // number of model variations int weight; // weight per item - int value; // value per item + int gold_value; // value per item list_t images; // item image filenames (inventory) list_t surfaces; // item image surfaces (inventory) Category category; // item category @@ -604,6 +807,7 @@ bool item_PotionBooze(Item*& item, Entity* entity, Entity* usedBy, bool shouldCo bool item_PotionJuice(Item*& item, Entity* entity, Entity* usedBy); bool item_PotionSickness(Item*& item, Entity* entity, Entity* usedBy); bool item_PotionConfusion(Item*& item, Entity* entity, Entity* usedBy); +bool item_PotionGrease(Item*& item, Entity* entity, Entity* usedBy); bool item_PotionCureAilment(Item*& item, Entity* entity, Entity* usedBy); bool item_PotionBlindness(Item*& item, Entity* entity, Entity* usedBy); bool item_PotionHealing(Item*& item, Entity* entity, Entity* usedBy, bool shouldConsumeItem = true); @@ -640,7 +844,6 @@ Entity* item_ToolBeartrap(Item*& item, Entity* usedBy); void item_Food(Item*& item, int player); void item_FoodTin(Item*& item, int player); void item_FoodAutomaton(Item*& item, int player); -void item_Gem(Item* item, int player); void item_Spellbook(Item*& item, int player); void item_ToolLootBag(Item*& item, int player); @@ -648,18 +851,25 @@ void item_ToolLootBag(Item*& item, int player); Item* newItem(ItemType type, Status status, Sint16 beatitude, Sint16 count, Uint32 appearance, bool identified, list_t* inventory); Item* uidToItem(Uint32 uid); ItemType itemLevelCurveEntity(Entity& my, Category cat, int minLevel, int maxLevel, BaronyRNG& rng); +bool itemLevelCurvePostProcess(Entity* my, Item* item, BaronyRNG& rng, +#ifdef EDITOR + int itemLevel = 0 +#else + int itemLevel = currentlevel +#endif +); ItemType itemLevelCurve(Category cat, int minLevel, int maxLevel, BaronyRNG& rng); Item* newItemFromEntity(const Entity* entity, bool discardUid = false); //Make sure to call free(item). discardUid will free the new items uid if this is for temp purposes Entity* dropItemMonster(Item* item, Entity* monster, Stat* monsterStats, Sint16 count = 1); Item** itemSlot(Stat* myStats, Item* item); enum Category itemCategory(const Item* item); -Sint32 itemModel(const Item* item, bool shortModel = false); +Sint32 itemModel(const Item* item, bool shortModel = false, Entity* creature = nullptr); Sint32 itemModelFirstperson(const Item* item); -SDL_Surface* itemSprite(Item* item); void consumeItem(Item*& item, int player); //NOTE: Items have to be unequipped before calling this function on them. NOTE: THIS CAN FREE THE ITEM POINTER. Sets item to nullptr if it does. bool dropItem(Item* item, int player, const bool notifyMessage = true, const bool dropAll = false); // return true on free'd item bool playerGreasyDropItem(const int player, Item* const item); +bool playerThrowDuck(const int player, Item* const item, int charge); void useItem(Item* item, int player, Entity* usedBy = nullptr, bool unequipForDropping = false); enum EquipItemResult : int { @@ -686,6 +896,7 @@ void clientSendEquipUpdateToServer(EquipItemSendToServerSlot slot, EquipItemResu ItemType type, Status status, Sint16 beatitude, int count, Uint32 appearance, bool identified); void clientUnequipSlotAndUpdateServer(const int player, EquipItemSendToServerSlot slot, Item* item); void clientSendAppearanceUpdateToServer(const int player, Item* item, const bool onIdentify); +void clientSendItemTypeUpdateToServer(const int player, Item* item, ItemType prevItemType); EquipItemResult equipItem(Item* item, Item** slot, int player, bool checkInventorySpaceForPaperDoll); enum ItemStackResults : int { @@ -748,6 +959,8 @@ static const int ENCHANTED_FEATHER_MAX_DURABILITY = 101; static const int QUIVER_MAX_AMMO_QTY = 51; static const int SCRAP_MAX_STACK_QTY = 101; static const int THROWN_GEM_MAX_STACK_QTY = 9; +static const int MAGICSTAFF_SCEPTER_CHARGE_MAX = 101; +static const int TOME_APPEARANCE_MAX = 1024; //-----ITEM COMPARISON FUNCS----- /* @@ -769,14 +982,18 @@ void copyItem(Item* itemToSet, const Item* itemToCopy); bool swapMonsterWeaponWithInventoryItem(Entity* my, Stat* myStats, node_t* inventoryNode, bool moveStack, bool overrideCursed); bool monsterUnequipSlot(Stat* myStats, Item** slot, Item* itemToUnequip); bool monsterUnequipSlotFromCategory(Stat* myStats, Item** slot, Category cat); -node_t* itemNodeInInventory(const Stat* myStats, Sint32 itemToFind, Category cat); +node_t* itemNodeInInventory(const Stat* myStats, Sint32 itemToFind, Category cat, bool randomSlot = false); node_t* spellbookNodeInInventory(const Stat* myStats, int spellIDToFind); node_t* getRangedWeaponItemNodeInInventory(const Stat* myStats, bool includeMagicstaff); node_t* getMeleeWeaponItemNodeInInventory(const Stat* myStats); ItemType itemTypeWithinGoldValue(int cat, int minValue, int maxValue, BaronyRNG& rng); bool itemSpriteIsQuiverThirdPersonModel(int sprite); bool itemSpriteIsQuiverBaseThirdPersonModel(int sprite); +bool itemSpriteIsFociThirdPersonModel(const int sprite); bool itemTypeIsQuiver(ItemType type); +bool itemTypeIsFoci(ItemType type); +bool itemTypeIsInstrument(ItemType type); +bool itemTypeIsThrownBall(ItemType type); real_t rangedAttackGetSpeedModifier(const Stat* myStats); bool rangedWeaponUseQuiverOnAttack(const Stat* myStats); real_t getArtifactWeaponEffectChance(ItemType type, Stat& wielder, real_t* effectAmount); @@ -790,3 +1007,22 @@ extern int decoyBoxRange; static const int MONSTER_ITEM_UNDROPPABLE_APPEARANCE = 1234567890; static const int ITEM_TINKERING_APPEARANCE = 987654320; static const int ITEM_GENERATED_QUIVER_APPEARANCE = 1122334455; + +enum SpellbookColors +{ + SPELLBOOK_COLOR_THAUM_2, //"items/images/SpellbookYellow.png", + SPELLBOOK_COLOR_THAUM_3, //"items/images/SpellbookWhite.png", + SPELLBOOK_COLOR_THAUM_1, //"items/images/SpellbookBlack.png", + SPELLBOOK_COLOR_MYSTICISM_2, //"items/images/SpellbookRed.png", + SPELLBOOK_COLOR_SORCERY_1, //"items/images/SpellbookBrown.png", + SPELLBOOK_COLOR_SORCERY_3, //"items/images/SpellbookOrange.png", + SPELLBOOK_COLOR_MYSTICISM_1, //"items/images/SpellbookGreen.png", + SPELLBOOK_COLOR_SORCERY_2, //"items/images/SpellbookBlue.png", + SPELLBOOK_COLOR_MYSTICISM_3 //"items/images/SpellbookPurple.png" +}; + +int getItemVariationFromSpellbookOrTome(const Item& item); + +#ifdef EDITOR +SDL_Surface* itemSprite(Item* const item); +#endif \ No newline at end of file diff --git a/src/json.cpp b/src/json.cpp index 23a177feb..3609c470e 100644 --- a/src/json.cpp +++ b/src/json.cpp @@ -14,14 +14,19 @@ const Uint32 BinaryFormatTag = *"spff"; class JsonFileWriter : public FileInterface { public: - JsonFileWriter() + JsonFileWriter(EFileFormat format) : buffer() , writer(buffer) { + if ( format == EFileFormat::Json_Compact ) + { + writer.SetIndent(' ', 2); + writer.SetFormatOptions(rapidjson::PrettyFormatOptions::kFormatSingleLineArray); + } } - static bool writeObject(File* file, const FileHelper::SerializationFunc& serialize) { - JsonFileWriter jfw; + static bool writeObject(File* file, const FileHelper::SerializationFunc& serialize, EFileFormat format) { + JsonFileWriter jfw(format); bool result = false; if (jfw.beginObject()) { @@ -489,8 +494,8 @@ bool FileHelper::writeObjectInternal(const char * filename, EFileFormat format, if (format == EFileFormat::Binary) { success = BinaryFileWriter::writeObject(file, serialize); } - else if (format == EFileFormat::Json) { - success = JsonFileWriter::writeObject(file, serialize); + else if (format == EFileFormat::Json || format == EFileFormat::Json_Compact) { + success = JsonFileWriter::writeObject(file, serialize, format); } else { assert(false); @@ -517,7 +522,7 @@ bool FileHelper::readObjectInternal(const char * filename, const SerializationFu if (format == EFileFormat::Binary) { success = BinaryFileReader::readObject(file, serialize); } - else if(format == EFileFormat::Json) { + else if(format == EFileFormat::Json || format == EFileFormat::Json_Compact) { success = JsonFileReader::readObject(file, serialize); } else { diff --git a/src/json.hpp b/src/json.hpp index 3d613833d..33d57ebdd 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -24,7 +24,8 @@ enum class EFileFormat { Json, - Binary + Binary, + Json_Compact }; class FileInterface { diff --git a/src/light.cpp b/src/light.cpp index 37be82177..f9102fe01 100644 --- a/src/light.cpp +++ b/src/light.cpp @@ -22,12 +22,13 @@ -------------------------------------------------------------------------------*/ -light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r, float g, float b, float exp) +light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r, float g, float b, float a, float exp) { light_t* light = newLight(index, x, y, radius); r = r * 255.f; g = g * 255.f; b = b * 255.f; + a = a * 255.f; for (int v = y - radius; v <= y + radius; ++v) { for (int u = x - radius; u <= x + radius; ++u) { @@ -45,7 +46,7 @@ light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r bool wallhit = true; const int mapindex = v * MAPLAYERS + u * MAPLAYERS * map.height; for (int z = 0; z < MAPLAYERS; z++) { - if (!map.tiles[mapindex + z]) { + if ( !map.tiles[mapindex + z] || map.tiles[mapindex + z] == TRANSPARENT_TILE ) { wallhit = false; break; } @@ -64,7 +65,8 @@ light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r v2 -= sgn(dy); } if (u2 >= 0 && u2 < map.width && v2 >= 0 && v2 < map.height) { - if (map.tiles[OBSTACLELAYER + v2 * MAPLAYERS + u2 * MAPLAYERS * map.height]) { + if ( map.tiles[OBSTACLELAYER + v2 * MAPLAYERS + u2 * MAPLAYERS * map.height] + && map.tiles[OBSTACLELAYER + v2 * MAPLAYERS + u2 * MAPLAYERS * map.height] != TRANSPARENT_TILE ) { wallhit = true; break; } @@ -80,7 +82,8 @@ light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r u2 -= sgn(dx); } if (u2 >= 0 && u2 < map.width && v2 >= 0 && v2 < map.height) { - if (map.tiles[OBSTACLELAYER + v2 * MAPLAYERS + u2 * MAPLAYERS * map.height]) { + if (map.tiles[OBSTACLELAYER + v2 * MAPLAYERS + u2 * MAPLAYERS * map.height] + && map.tiles[OBSTACLELAYER + v2 * MAPLAYERS + u2 * MAPLAYERS * map.height] != TRANSPARENT_TILE) { wallhit = true; break; } @@ -92,7 +95,6 @@ light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r if (wallhit == false || (wallhit == true && u2 == u && v2 == v)) { const float dist = exp != 1.f ? powf(dx * dx + dy * dy, exp) : dx * dx + dy * dy; const auto falloff = std::min(dist / radius, 1.0f); - constexpr float a = 255.f; const auto soff = (dy + radius) + (dx + radius) * (radius * 2 + 1); auto& s = light->tiles[soff]; s.x += r - r * falloff; @@ -131,12 +133,13 @@ light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r -------------------------------------------------------------------------------*/ -light_t* lightSphere(int index, Sint32 x, Sint32 y, Sint32 radius, float r, float g, float b, float exp) +light_t* lightSphere(int index, Sint32 x, Sint32 y, Sint32 radius, float r, float g, float b, float a, float exp) { light_t* light = newLight(index, x, y, radius); r = r * 255.f; g = g * 255.f; b = b * 255.f; + a = a * 255.f; for (int v = y - radius; v <= y + radius; ++v) { for (int u = x - radius; u <= x + radius; ++u) { @@ -145,7 +148,6 @@ light_t* lightSphere(int index, Sint32 x, Sint32 y, Sint32 radius, float r, floa const int dy = v - y; const float dist = exp != 1.f ? powf(dx * dx + dy * dy, exp) : dx * dx + dy * dy; const auto falloff = std::min(dist / radius, 1.0f); - constexpr float a = 255.f; const auto soff = (dy + radius) + (dx + radius) * (radius * 2 + 1); auto& s = light->tiles[soff]; s.x += r - r * falloff; @@ -235,6 +237,14 @@ bool loadLights(bool forceLoadBaseDirectory) { const auto& r = it.value["r"]; def.r = r.GetFloat(); const auto& g = it.value["g"]; def.g = g.GetFloat(); const auto& b = it.value["b"]; def.b = b.GetFloat(); + if ( it.value.HasMember("a") ) + { + def.a = it.value["a"].GetFloat(); + } + else + { + def.a = 0.f; + } const auto& exp = it.value["falloff_exp"]; def.falloff_exp = exp.GetFloat(); const auto& shadows = it.value["shadows"]; def.shadows = shadows.GetBool(); lightDefs[name] = def; @@ -262,8 +272,8 @@ light_t* addLight(Sint32 x, Sint32 y, const char* name, int range_bonus, int ind } const auto& def = find->second; if (def.shadows) { - return lightSphereShadow(index, x, y, std::max(def.radius + range_bonus, 1), def.r, def.g, def.b, def.falloff_exp); + return lightSphereShadow(index, x, y, std::max(def.radius + range_bonus, 1), def.r, def.g, def.b, def.a, def.falloff_exp); } else { - return lightSphere(index, x, y, std::max(def.radius + range_bonus, 1), def.r, def.g, def.b, def.falloff_exp); + return lightSphere(index, x, y, std::max(def.radius + range_bonus, 1), def.r, def.g, def.b, def.a, def.falloff_exp); } } diff --git a/src/light.hpp b/src/light.hpp index 0003b1be7..ef6eef344 100644 --- a/src/light.hpp +++ b/src/light.hpp @@ -21,8 +21,8 @@ typedef struct light_t node_t* node; } light_t; -light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r, float g, float b, float exp); -light_t* lightSphere(int index, Sint32 x, Sint32 y, Sint32 radius, float r, float g, float b, float exp); +light_t* lightSphereShadow(int index, Sint32 x, Sint32 y, Sint32 radius, float r, float g, float b, float a, float exp); +light_t* lightSphere(int index, Sint32 x, Sint32 y, Sint32 radius, float r, float g, float b, float a, float exp); light_t* newLight(int index, Sint32 x, Sint32 y, Sint32 radius); light_t* addLight(Sint32 x, Sint32 y, const char* name, int range_bonus = 0, int index = 0); bool loadLights(bool forceLoadBaseDirectory = false); @@ -32,6 +32,7 @@ struct LightDef { float r = 0.f; float g = 0.f; float b = 0.f; + float a = 0.f; float falloff_exp = 1.f; bool shadows = false; }; diff --git a/src/list.cpp b/src/list.cpp index 6391844fe..cb7bd3b4b 100644 --- a/src/list.cpp +++ b/src/list.cpp @@ -91,9 +91,9 @@ void list_RemoveNode(node_t* node) { chest_inventory = &chestInv[i]; } - else if ( openedChest[i]->children.first && openedChest[i]->children.first->element ) + else if ( openedChest[i] ) { - chest_inventory = (list_t*)openedChest[i]->children.first->element; + chest_inventory = openedChest[i]->getChestInventoryList(); } if ( chest_inventory ) diff --git a/src/magic/act_HandMagic.cpp b/src/magic/act_HandMagic.cpp index 27af30a3c..60b5e8ee7 100644 --- a/src/magic/act_HandMagic.cpp +++ b/src/magic/act_HandMagic.cpp @@ -23,10 +23,19 @@ #include "../ui/MainMenu.hpp" #include "../prng.hpp" #include "../mod_tools.hpp" +#include "../collision.hpp" //The spellcasting animation stages: -#define CIRCLE 0 //One circle -#define THROW 1 //Throw spell! +#define ANIM_SPELL_CIRCLE 0 //One circle +#define ANIM_SPELL_THROW 1 //Throw spell! +#define ANIM_SPELL_TOUCH 2 +#define ANIM_SPELL_COMPLETE_SPELL 3 +#define ANIM_SPELL_TOUCH_THROW 4 +#define ANIM_SPELL_COMPLETE_NOCAST 5 +#define ANIM_SPELL_TOUCH_CHARGE 6 +#define ANIM_SPELL_OVERCHARGE_READY 7 +#define ANIM_SPELL_OVERCHARGE_CHARGE 8 +#define ANIM_SPELL_OVERCHARGE_THROW 9 spellcasting_animation_manager_t cast_animation[MAXPLAYERS]; bool overDrawDamageNotify = false; @@ -40,8 +49,867 @@ bool overDrawDamageNotify = false; #define HANDMAGIC_PARTICLESPRAY1 my->skill[3] #define HANDMAGIC_CIRCLE_RADIUS 0.8 #define HANDMAGIC_CIRCLE_SPEED 0.3 +#define HANDMAGIC_TICKS_PER_CIRCLE 20 -void fireOffSpellAnimation(spellcasting_animation_manager_t* animation_manager, Uint32 caster_uid, spell_t* spell, bool usingSpellbook) +void spellcasting_animation_manager_t::resetRangefinder() +{ + target_x = 0.0; + target_y = 0.0; + caster_x = 0.0; + caster_y = 0.0; + targetUid = 0; + rangefinder = RANGEFINDER_NONE; +} + +bool spellcasting_animation_manager_t::hideShieldFromBasicCast() +{ + if ( player < 0 || player >= MAXPLAYERS ) { return false; } + if ( active ) + { + if ( stage == ANIM_SPELL_TOUCH + || stage == ANIM_SPELL_TOUCH_THROW + || stage == ANIM_SPELL_TOUCH_CHARGE + || stage == ANIM_SPELL_OVERCHARGE_READY + || stage == ANIM_SPELL_OVERCHARGE_CHARGE + || stage == ANIM_SPELL_OVERCHARGE_THROW ) + { + return false; + } + return true; + } + return false; +} + +static ConsoleVariable cvar_vibe_spell_x("/vibe_spell_x", 4000); +static ConsoleVariable cvar_vibe_spell_y("/vibe_spell_y", 0); +static ConsoleVariable cvar_vibe_spell_s("/vibe_spell_s", 0); +void spellcasting_animation_manager_t::executeAttackSpell(bool swingweapon) +{ + if ( player < 0 || player >= MAXPLAYERS ) { return; } + if ( !spellWaitingAttackInput() ) + { + return; + } + + if ( stage == ANIM_SPELL_TOUCH ) + { + if ( swingweapon ) + { + stage = ANIM_SPELL_TOUCH_CHARGE; + throw_count = 0; + } + } + else if ( stage == ANIM_SPELL_TOUCH_CHARGE ) + { + if ( !swingweapon ) + { + if ( rangefinder == RANGEFINDER_TOUCH_FLOOR_TILE ) + { + stage = ANIM_SPELL_TOUCH_THROW; + throw_count = 0; + } + else if ( (rangefinder == RANGEFINDER_TOUCH + || rangefinder == RANGEFINDER_TOUCH_INTERACT + || rangefinder == RANGEFINDER_TOUCH_INTERACT_TEST) && uidToEntity(targetUid) ) + { + stage = ANIM_SPELL_TOUCH_THROW; + throw_count = 0; + } + else if ( rangefinder == RANGEFINDER_TOUCH_WALL_TILE && wallDir >= 1 ) + { + stage = ANIM_SPELL_TOUCH_THROW; + throw_count = 0; + } + else + { + stage = ANIM_SPELL_TOUCH; + throw_count = 0; + } + /*else + { + spellcastingAnimationManager_deactivate(this); + messagePlayer(player, MESSAGE_COMBAT, Language::get(6496)); + playSoundEntityLocal(players[player]->entity, 163, 64); + }*/ + } + } + else if ( stage == ANIM_SPELL_OVERCHARGE_READY ) + { + if ( swingweapon ) + { + stage = ANIM_SPELL_OVERCHARGE_CHARGE; + throw_count = 0; + } + } + else if ( stage == ANIM_SPELL_OVERCHARGE_CHARGE ) + { + if ( !swingweapon ) + { + if ( rangefinder == RANGEFINDER_TOUCH_FLOOR_TILE ) + { + stage = ANIM_SPELL_OVERCHARGE_THROW; + throw_count = 0; + } + else if ( (rangefinder == RANGEFINDER_TOUCH + || rangefinder == RANGEFINDER_TOUCH_INTERACT + || rangefinder == RANGEFINDER_TOUCH_INTERACT_TEST) && uidToEntity(targetUid) ) + { + stage = ANIM_SPELL_OVERCHARGE_THROW; + throw_count = 0; + } + else if ( rangefinder == RANGEFINDER_TOUCH_WALL_TILE && wallDir >= 1 ) + { + stage = ANIM_SPELL_OVERCHARGE_THROW; + throw_count = 0; + } + else if ( rangefinder != RANGEFINDER_NONE ) + { + stage = ANIM_SPELL_OVERCHARGE_READY; + throw_count = 0; + } + else + { + stage = ANIM_SPELL_OVERCHARGE_THROW; + throw_count = 0; + } + /*else + { + spellcastingAnimationManager_deactivate(this); + messagePlayer(player, MESSAGE_COMBAT, Language::get(6496)); + playSoundEntityLocal(players[player]->entity, 163, 64); + }*/ + } + } +} + +bool spellcasting_animation_manager_t::spellWaitingAttackInput() +{ + if ( (active || active_spellbook) + && (stage == ANIM_SPELL_TOUCH + || stage == ANIM_SPELL_TOUCH_CHARGE + || stage == ANIM_SPELL_OVERCHARGE_READY + || stage == ANIM_SPELL_OVERCHARGE_CHARGE) ) + { + return true; + } + return false; +} + +bool spellcasting_animation_manager_t::spellIgnoreAttack() +{ + if ( (active || active_spellbook) + && (stage == ANIM_SPELL_COMPLETE_NOCAST + || stage == ANIM_SPELL_COMPLETE_SPELL + || stage == ANIM_SPELL_TOUCH_THROW + || stage == ANIM_SPELL_OVERCHARGE_THROW) ) + { + return true; + } + return false; +} + +bool rangefinderTargetEnemyType(spell_t& spell, Entity& entity) +{ + if ( entity.behavior == &actMonster && !entity.monsterIsTargetable(true) ) + { + return false; + } + + if ( spell.ID == SPELL_TELEKINESIS ) + { + if ( entity.behavior == &actBoulder && !entity.flags[PASSABLE] && entity.z >= -8 ) + { + return true; + } + else if ( entity.behavior == &actItem && !entity.flags[INVISIBLE] ) + { + return true; + } + else if ( entity.behavior == &actGoldBag && !entity.flags[INVISIBLE] ) + { + return true; + } + else if ( entity.behavior == &actMonster && entity.getMonsterTypeFromSprite() == EARTH_ELEMENTAL ) + { + return true; + } + else if ( entity.behavior == &actSwitch || + entity.behavior == &actSwitchWithTimer + /*|| entity.sprite == 184*/ + ) + { + return true; + } + else if ( entity.behavior == &::actWallButton + /*|| entity.sprite == 1151 + || entity.sprite == 1152*/ + ) + { + return true; + } + /*else if ( (entity.behavior == &::actWallLock + || (entity.sprite >= 1585 && entity.sprite <= 1592)) ) + { + return true; + }*/ + else if ( entity.behavior == &actBell ) + { + return true; + } + return false; + } + else if ( spell.ID == SPELL_SABOTAGE + || spell.ID == SPELL_HARVEST_TRAP ) + { + if ( entity.behavior == &actArrowTrap ) + { + return true; + } + else if ( entity.behavior == &actBoulderTrapHole ) + { + return true; + } + else if ( entity.behavior == &actMagicTrapCeiling ) + { + return true; + } + else if ( entity.behavior == &actMagicTrap ) + { + return true; + } + else if ( entity.behavior == &actSpearTrap ) + { + return true; + } + else if ( entity.behavior == &actMonster ) + { + if ( spell.ID == SPELL_HARVEST_TRAP ) + { + } + else + { + Monster type = entity.getMonsterTypeFromSprite(); + if ( type == MIMIC + || type == AUTOMATON + || type == CRYSTALGOLEM + || type == MINIMIMIC + || type == SENTRYBOT + || type == SPELLBOT + || type == DUMMYBOT ) + { + return true; + } + } + } + else if ( entity.behavior == &actChest ) + { + if ( spell.ID == SPELL_HARVEST_TRAP ) + { + } + else + { + return true; + } + } + return false; + } + else if ( spell.ID == SPELL_KINETIC_PUSH ) + { + if ( entity.behavior == &actBoulder && !entity.flags[PASSABLE] && entity.z >= -8 ) + { + return true; + } + else if ( (entity.behavior == &actMonster && !entity.isInertMimic()) || entity.behavior == &actPlayer ) + { + return true; + } + return false; + } + else if ( spell.ID == SPELL_VOID_CHEST ) + { + return (entity.behavior == &actMonster && entity.getMonsterTypeFromSprite() == MIMIC) || entity.behavior == &actChest; + } + else if ( spell.ID == SPELL_BOOBY_TRAP ) + { + return entity.isDamageableCollider() || entity.behavior == &actFurniture + || entity.behavior == &actChest || entity.behavior == &actDoor + || (entity.behavior == &actMonster && entity.getMonsterTypeFromSprite() == MIMIC); + } + else if ( spell.ID == SPELL_SPLINTER_GEAR ) + { + return entity.behavior == &actMonster || entity.behavior == &actPlayer || entity.behavior == &actChest; + } + else if ( spell.ID == SPELL_DISARM || spell.ID == SPELL_STRIP || spell.ID == SPELL_COMMAND ) + { + return entity.behavior == &actMonster; + } + else if ( spell.ID == SPELL_DEFACE ) + { + return (!entity.flags[INVISIBLE] && entity.behavior == &actHeadstone) + || entity.behavior == &actSink + || entity.behavior == &actCauldron + || entity.behavior == &actMailbox + || entity.behavior == &actWorkbench; + } + else if ( spell.ID == SPELL_DEMESNE_DOOR ) + { + return (entity.behavior == &actDoorFrame && !entity.flags[INVISIBLE]) /*|| entity.behavior == &actDoor*/; + } + else + { + return (entity.behavior == &actMonster && !entity.isInertMimic()) || entity.behavior == &actPlayer; + } + return false; +} + +void spellcasting_animation_manager_t::setRangeFinderLocation() +{ + Entity* caster = uidToEntity(this->caster); + rangefinder = RANGEFINDER_NONE; + if ( !caster ) + { + return; + } + + if ( !spell ) + { + return; + } + + if ( !spell->rangefinder ) + { + return; + } + + rangefinder = spell->rangefinder; + + if ( stage == ANIM_SPELL_TOUCH_THROW || stage == ANIM_SPELL_OVERCHARGE_THROW ) + { + return; + } + if ( (stage == ANIM_SPELL_TOUCH_CHARGE || stage == ANIM_SPELL_OVERCHARGE_CHARGE) + && targetUid != 0 && uidToEntity(targetUid) ) + { + return; + } + + if ( rangefinder == RANGEFINDER_TOUCH_INTERACT_TEST ) + { + targetUid = 0; + if ( players[player]->worldUI.isEnabled() ) + { + if ( players[player]->worldUI.tooltipsInRange.size() > 0 ) + { + for ( node_t* node = map.worldUI->first; node; node = node->next ) + { + Entity* tooltip = (Entity*)node->element; + if ( !tooltip || tooltip->behavior != &actSpriteWorldTooltip ) + { + continue; + } + if ( players[player]->worldUI.bTooltipActiveForPlayer(*tooltip) ) + { + targetUid = tooltip->parent; + break; + } + } + } + } + } + + static ConsoleVariable cvar_rangefinderStartZ("/rangefinder_start_z", -2.5); + static ConsoleVariable cvar_rangefinderMoveTo("/rangefinder_moveto_z", 1.0); + static ConsoleVariable cvar_rangefinderStartZLimit("/rangefinder_start_z_limit", 7.5); + static ConsoleVariable cvar_rangefinder_cam("/rangefinder_cam", false); + real_t startx = caster->x; + real_t starty = caster->y; + real_t startz = caster->z; + real_t pitch = caster->pitch; + real_t yaw = caster->yaw; + if ( *cvar_rangefinder_cam ) + { + startx = cameras[player].x * 16.0; + starty = cameras[player].y * 16.0; + startz = cameras[player].z + (4.5 - cameras[player].z) / 2.0 + *cvar_rangefinderStartZ; + pitch = cameras[player].vang; + yaw = cameras[player].ang; + } + if ( pitch < 0 || pitch > PI ) + { + pitch = 0; + } + + // draw line from the players height and direction until we hit the ground. + real_t previousx = startx; + real_t previousy = starty; + int index = 0; + wallDir = 0; + + real_t spellDistance = getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_DISTANCE, spell->ID, caster, nullptr, caster); + static ConsoleVariable cvar_rangefinder_linetrace("/rangefinder_linetrace", true); + + if ( rangefinder == RANGEFINDER_TOUCH_WALL_TILE ) + { + real_t dist = lineTrace(nullptr, startx, starty, yaw, spellDistance, LINETRACE_TELEKINESIS, false); + if ( dist < spellDistance ) + { + previousx = hit.x; + previousy = hit.y; + if ( hit.side == HORIZONTAL ) + { + if ( yaw >= 3 * PI / 2 || yaw <= PI / 2 ) + { + wallDir = 3; + previousx = static_cast((hit.x + 8.0) / 16); + previousy = static_cast(hit.y / 16); + } + else + { + wallDir = 1; + previousx = static_cast((hit.x - 8.0) / 16); + previousy = static_cast(hit.y / 16); + } + } + else if ( hit.side == VERTICAL ) + { + if ( yaw >= 0 && yaw < PI ) + { + wallDir = 4; + previousx = static_cast(hit.x / 16); + previousy = static_cast((hit.y + 8.0) / 16); + } + else + { + wallDir = 2; + previousx = static_cast(hit.x / 16); + previousy = static_cast((hit.y - 8.0) / 16); + } + } + else + { + wallDir = 0; + //messagePlayer(0, MESSAGE_DEBUG, "??"); + } + } + } + else + { + bool invertZ = false; + /*if ( (*cvar_rangefinder_linetrace || !(svFlags & SV_FLAG_CHEATS)) ) + { + invertZ = true; + }*/ + real_t increment = abs((*cvar_rangefinderMoveTo) * tan(pitch)); + real_t end = *cvar_rangefinderStartZLimit; + if ( invertZ ) + { + end = caster->z; + increment *= -1; + startz = *cvar_rangefinderStartZLimit; + startx += spellDistance * cos(yaw); + starty += spellDistance * sin(yaw); + } + + /*list_t entityList; + entityList.first = nullptr; + entityList.last = nullptr; + + node_t* node2 = list_AddNodeLast(&entityList); + node2->element = caster; + node2->deconstructor = &emptyDeconstructor; + node2->size = sizeof(Entity*); + + for ( auto node = map.entities->first; node; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( ((entity->behavior == &actGate || entity->behavior == &actIronDoor) && !entity->flags[PASSABLE]) ) + { + if ( entityDist(caster, entity) >= spellDistance + 4.0 ) + { + continue; + } + node_t* node2 = list_AddNodeLast(&entityList); + node2->element = entity; + node2->deconstructor = &emptyDeconstructor; + node2->size = sizeof(Entity*); + } + } + }*/ + + for ( ; invertZ ? startz > end : startz < end; startz += increment ) + { + startx += (invertZ ? -1.0 : 1.0) * cos(yaw); + starty += (invertZ ? -1.0 : 1.0) * sin(yaw); + const int index_x = static_cast(startx) >> 4; + const int index_y = static_cast(starty) >> 4; + index = (index_y)*MAPLAYERS + (index_x)*MAPLAYERS * map.height; + + if ( index_x < 0 || index_x >= map.width || index_y < 0 || index_y >= map.height ) + { + break; + } + + if ( (map.tiles[index] || rangefinder != RANGEFINDER_TARGET) && !map.tiles[OBSTACLELAYER + index] ) + { + // store the last known good coordinate + previousx = startx;// + 16 * cos(yaw); + previousy = starty;// + 16 * sin(yaw); + } + if ( map.tiles[OBSTACLELAYER + index] ) + { + break; + } + if ( pow(startx - caster->x, 2) + pow(starty - caster->y, 2) > (spellDistance * spellDistance) ) + { + // break if distance reached + break; + } + } + + if ( caster->bodyparts.size() && (*cvar_rangefinder_linetrace || !(svFlags & SV_FLAG_CHEATS)) ) + { + real_t lineTraceDist = sqrt(pow(previousx - caster->x, 2) + pow(previousy - caster->y, 2)); + Entity* ohit = hit.entity; + real_t tangent = atan2(caster->y - previousy, caster->x - previousx); + auto bodypart = caster->bodyparts[0]; // temporary entity for lineTraceTarget + real_t tempx = bodypart->x; + real_t tempy = bodypart->y; + bodypart->x = previousx; + bodypart->y = previousy; + real_t dist = lineTraceTarget(bodypart, bodypart->x, bodypart->y, tangent, lineTraceDist, LINETRACE_TELEKINESIS, false, caster, nullptr); + if ( hit.entity != caster ) + { + previousx = bodypart->x + dist * cos(tangent); + previousy = bodypart->y + dist * sin(tangent); + } + bodypart->x = tempx; + bodypart->y = tempy; + hit.entity = ohit; + } + + //list_FreeAll(&entityList); + } + + target_x = previousx; + target_y = previousy; + caster_x = caster->x; + caster_y = caster->y; + + if ( rangefinder == RANGEFINDER_TOUCH + || rangefinder == RANGEFINDER_TOUCH_INTERACT ) + { + real_t maxDist = spellDistance + 8.0; + real_t minDist = 4.0; + real_t rangeFinderDist = std::max(0.0, sqrt(pow(startx - caster->x, 2) + pow(starty - caster->y, 2)) - 8.0); + + targetUid = 0; + + bool interactBonusWidthEnemies = true; + bool interactBonusWidthAllies = false; + list_t* entityList = map.creatures; + if ( spell->ID == SPELL_TELEKINESIS ) + { + minDist = 16.0; + } + + if ( spell->ID == SPELL_BOOBY_TRAP || spell->ID == SPELL_VOID_CHEST || spell->ID == SPELL_TELEKINESIS + || spell->ID == SPELL_KINETIC_PUSH || spell->ID == SPELL_SABOTAGE || spell->ID == SPELL_HARVEST_TRAP + || spell->ID == SPELL_SPLINTER_GEAR || spell->ID == SPELL_DEFACE || spell->ID == SPELL_SUNDER_MONUMENT + || spell->ID == SPELL_DEMESNE_DOOR ) + { + entityList = map.entities; + } + else if ( spell->ID == SPELL_HEAL_OTHER ) + { + interactBonusWidthAllies = true; + interactBonusWidthEnemies = false; + } + + struct EntitySpellTargetLocation + { + Entity* entity = nullptr; + real_t dist = 0.0; + real_t crossDist = 0.0; + int wallDir = 0; + }; + + auto compFunc = [](EntitySpellTargetLocation& lhs, EntitySpellTargetLocation& rhs) + { + if ( abs(lhs.crossDist - rhs.crossDist) <= 0.00001 ) + { + return lhs.dist > rhs.dist; + } + return lhs.crossDist > rhs.crossDist; + }; + std::priority_queue, decltype(compFunc)> entitiesInRange(compFunc); + for ( auto node = entityList->first; node; node = node->next ) // TODO - grab a shortened list from somewhere else iterating entities + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( rangefinderTargetEnemyType(*spell, *entity) ) + { + real_t dist = entityDist(caster, entity); + if ( dist > minDist && dist < maxDist + /*&& rangeFinderDist >= dist*/ + ) + { + real_t tangent = atan2(entity->y - caster->y, entity->x - caster->x); + while ( tangent >= 2 * PI ) + { + tangent -= 2 * PI; + } + while ( tangent < 0 ) + { + tangent += 2 * PI; + } + real_t playerYaw = caster->yaw; + while ( playerYaw >= 2 * PI ) + { + playerYaw -= 2 * PI; + } + while ( playerYaw < 0 ) + { + playerYaw += 2 * PI; + } + + real_t interactAngle = PI / 8; + bool bonusRange = false; + if ( spell->ID == SPELL_BOOBY_TRAP ) + { + bonusRange = true; + } + else if ( interactBonusWidthAllies && (entity->behavior == &actPlayer + || (entity->behavior == &actMonster && entity->monsterAllyGetPlayerLeader())) ) + { + // monsters have wider interact angle for aim assist + bonusRange = true; + + } + else if ( interactBonusWidthEnemies && entity->behavior == &actMonster + && !(entity->isInertMimic()) + && ((multiplayer != CLIENT && entity->checkEnemy(caster)) + || (multiplayer == CLIENT + && !entity->monsterAllyGetPlayerLeader() && !monsterally[entity->getMonsterTypeFromSprite()][caster->getStats()->type])) ) + { + // monsters have wider interact angle for aim assist + bonusRange = true; + } + + if ( bonusRange ) + { + interactAngle = (PI / 6); + if ( dist > STRIKERANGE ) + { + // for every x units, shrink the selection angle + int units = static_cast(dist / 16); + interactAngle -= (units * PI / 64); + interactAngle = std::max(interactAngle, PI / 32); + } + } + else + { + if ( dist > STRIKERANGE ) + { + // for every x units, shrink the selection angle + int units = static_cast(dist / 8); + interactAngle -= (units * PI / 64); + interactAngle = std::max(interactAngle, PI / 64); + } + } + + if ( (abs(tangent - playerYaw) < (interactAngle)) || (abs(tangent - playerYaw) > (2 * PI - interactAngle)) ) + { + if ( entity->behavior == &actArrowTrap || entity->behavior == &actMagicTrap ) + { + Entity* ohit = hit.entity; + real_t dist2 = lineTrace(nullptr, caster->x, caster->y, yaw, dist, LINETRACE_TELEKINESIS, false); + if ( dist2 < dist ) + { + int tempx = hit.x; + int tempy = hit.y; + int wallDir = 0; + if ( hit.side == HORIZONTAL ) + { + if ( yaw >= 3 * PI / 2 || yaw <= PI / 2 ) + { + wallDir = 3; + tempx = static_cast((hit.x + 8.0) / 16); + tempy = static_cast(hit.y / 16); + } + else + { + wallDir = 1; + tempx = static_cast((hit.x - 8.0) / 16); + tempy = static_cast(hit.y / 16); + } + } + else if ( hit.side == VERTICAL ) + { + if ( yaw >= 0 && yaw < PI ) + { + wallDir = 4; + tempx = static_cast(hit.x / 16); + tempy = static_cast((hit.y + 8.0) / 16); + } + else + { + wallDir = 2; + tempx = static_cast(hit.x / 16); + tempy = static_cast((hit.y - 8.0) / 16); + } + } + else + { + wallDir = 0; + //messagePlayer(0, MESSAGE_DEBUG, "??"); + } + + if ( wallDir > 0 ) + { + if ( tempx == static_cast(entity->x / 16) + && tempy == static_cast(entity->y / 16) ) + { + real_t crossDist = abs(dist2 * sin(tangent - playerYaw)); + entitiesInRange.push(EntitySpellTargetLocation{ entity, dist2, crossDist, wallDir }); + } + //messagePlayer(0, MESSAGE_DEBUG, "x: %d y: %d | entity: x: %d y: %d", + // tempx, tempy, (int)(entity->x / 16), (int)(entity->y / 16)); + } + } + hit.entity = ohit; + } + else + { + Entity* ohit = hit.entity; + //int lineTraceFlags = spell->ID == SPELL_TELEKINESIS ? LINETRACE_TELEKINESIS : 0; + lineTraceTarget(entity, entity->x, entity->y, tangent + PI, dist, LINETRACE_TELEKINESIS, false, caster); + if ( hit.entity == caster ) + { + real_t crossDist = abs(dist * sin(tangent - playerYaw)); + if ( spell->ID == SPELL_TELEKINESIS + && (entity->behavior == &actItem + || entity->behavior == &actGoldBag + || entity->behavior == &actSwitch + || entity->behavior == &actSwitchWithTimer) ) + { + real_t targetTileDist = 4.0 + sqrt(pow(target_x - entity->x, 2) + pow(target_y - entity->y, 2)); + crossDist = sqrt(pow(targetTileDist, 2) + pow(crossDist, 2)); + } + entitiesInRange.push(EntitySpellTargetLocation{entity, dist, crossDist, 0}); + } + hit.entity = ohit; + } + } + } + } + } + } + + if ( entitiesInRange.size() ) + { + targetUid = entitiesInRange.top().entity->getUID(); + wallDir = entitiesInRange.top().wallDir; + //messagePlayer(0, MESSAGE_DEBUG, "Dist: %.2f, Cross: %.2f", entitiesInRange.top().dist, entitiesInRange.top().crossDist); + } + } +} + +void spellcastAnimationUpdate(int player, int attackPose, int castTime) +{ + if ( multiplayer == CLIENT ) + { + // send to server with impulse + strcpy((char*)net_packet->data, "SANM"); + net_packet->data[4] = player; + net_packet->data[5] = attackPose; + SDLNet_Write16(castTime, &net_packet->data[6]); + net_packet->len = 8; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + + spellcastAnimationUpdateReceive(player, attackPose, castTime); +} + +void spellcastAnimationUpdateReceive(int player, int attackPose, int castTime) +{ + if ( player < 0 || player >= MAXPLAYERS ) + { + return; + } + + if ( multiplayer == SERVER ) + { + for ( int i = 1; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] && !players[i]->isLocalPlayer() ) + { + if ( i != player ) + { + // forward on packet to other clients + strcpy((char*)net_packet->data, "SANM"); + net_packet->data[4] = player; + net_packet->data[5] = attackPose; + SDLNet_Write16(castTime, &net_packet->data[6]); + net_packet->len = 8; + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + } + } + } + + if ( players[player]->entity ) + { + if ( attackPose == MONSTER_POSE_MAGIC_WINDUP1 ) + { + players[player]->entity->playerCastTimeAnim = castTime; + if ( players[player]->entity->skill[9] == MONSTER_POSE_MAGIC_WINDUP1 ) + { + // add to the counter to not stagger restart + players[player]->entity->playerCastTimeAnim += players[player]->entity->skill[10]; + } + else + { + players[player]->entity->skill[10] = 0; + } + if ( !players[player]->isLocalPlayer() ) + { + playSoundEntityLocal(players[player]->entity, 170, 128); + } + players[player]->entity->skill[9] = MONSTER_POSE_MAGIC_WINDUP1; + } + else if ( attackPose == MONSTER_POSE_MAGIC_WINDUP2 ) + { + if ( !players[player]->isLocalPlayer() ) + { + if ( players[player]->entity->skill[9] != MONSTER_POSE_MAGIC_WINDUP2 ) + { + playSoundEntityLocal(players[player]->entity, 759 + local_rng.rand() % 4, 92); // charge up sound + } + } + players[player]->entity->skill[9] = attackPose; + players[player]->entity->skill[10] = 0; + } + else if ( attackPose == MONSTER_POSE_MAGIC_CAST2 || attackPose == 1 ) + { + if ( attackPose == MONSTER_POSE_MAGIC_CAST2 && !players[player]->isLocalPlayer() ) + { + if ( players[player]->entity->skill[9] == MONSTER_POSE_MAGIC_WINDUP2 ) + { + playSoundEntityLocal(players[player]->entity, 163, 64); // fizzle sound + } + } + players[player]->entity->skill[9] = attackPose; + players[player]->entity->skill[10] = 0; + } + } +} + +void fireOffSpellAnimation(spellcasting_animation_manager_t* animation_manager, Uint32 caster_uid, spell_t* spell, bool usingSpellbook, bool usingTome) { //This function triggers the spellcasting animation and sets up everything. @@ -68,7 +936,12 @@ void fireOffSpellAnimation(spellcasting_animation_manager_t* animation_manager, return; } - playSoundEntity(caster, 170, 128 ); + bool overchargeRepeat = animation_manager->overcharge > 0; + + if ( !overchargeRepeat ) + { + playSoundEntityLocal(caster, 170, 128 ); + } Stat* stat = caster->getStats(); //Save these three very important pieces of data. @@ -84,51 +957,93 @@ void fireOffSpellAnimation(spellcasting_animation_manager_t* animation_manager, { animation_manager->active_spellbook = true; } - animation_manager->stage = CIRCLE; - //Make the HUDWEAPON disappear, or somesuch? - players[player]->hud.magicLeftHand->flags[INVISIBLE_DITHER] = false; - players[player]->hud.magicRightHand->flags[INVISIBLE_DITHER] = false; - if ( stat->type != RAT ) + animation_manager->stage = ANIM_SPELL_CIRCLE; + if ( overchargeRepeat ) { - if ( !usingSpellbook ) + // + } + else + { + + //Make the HUDWEAPON disappear, or somesuch? + players[player]->hud.magicLeftHand->flags[INVISIBLE_DITHER] = false; + players[player]->hud.magicRightHand->flags[INVISIBLE_DITHER] = false; + if ( stat->type != RAT ) { - players[player]->hud.magicLeftHand->flags[INVISIBLE] = false; + if ( !usingSpellbook ) + { + players[player]->hud.magicLeftHand->flags[INVISIBLE] = false; + if ( caster->isInvisible() ) + { + players[player]->hud.magicLeftHand->flags[INVISIBLE] = true; + players[player]->hud.magicLeftHand->flags[INVISIBLE_DITHER] = true; + } + } + players[player]->hud.magicRightHand->flags[INVISIBLE] = false; if ( caster->isInvisible() ) { - players[player]->hud.magicLeftHand->flags[INVISIBLE] = true; - players[player]->hud.magicLeftHand->flags[INVISIBLE_DITHER] = true; + players[player]->hud.magicRightHand->flags[INVISIBLE] = true; + players[player]->hud.magicRightHand->flags[INVISIBLE_DITHER] = true; } } - players[player]->hud.magicRightHand->flags[INVISIBLE] = false; - if ( caster->isInvisible() ) - { - players[player]->hud.magicRightHand->flags[INVISIBLE] = true; - players[player]->hud.magicRightHand->flags[INVISIBLE_DITHER] = true; - } } animation_manager->lefthand_angle = 0; animation_manager->lefthand_movex = 0; animation_manager->lefthand_movey = 0; int spellCost = getCostOfSpell(spell, caster); + if ( !usingSpellbook && spell->ID != SPELL_OVERCHARGE ) + { + if ( usingTome ) + { + animation_manager->overcharge_init = 1; + + if ( multiplayer == CLIENT ) + { + strcpy((char*)net_packet->data, "OVRC"); + net_packet->data[4] = clientnum; + net_packet->data[5] = 1; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, 0); + } + } + if ( animation_manager->overcharge_init ) + { + animation_manager->overcharge = animation_manager->overcharge_init; + animation_manager->overcharge_init = 0; + } + if ( animation_manager->overcharge ) + { + spellCost = std::max(1, spellCost / 2); + } + if ( stats[player]->type == SALAMANDER && stats[player]->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + spellCost = 0; + } + } animation_manager->circle_count = 0; - animation_manager->times_to_circle = (spellCost / 10) + 1; //Circle once for every 10 mana the spell costs. + animation_manager->throw_count = 0; + animation_manager->active_count = 0; + animation_manager->times_to_circle = getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_SPELL_CAST_TIME, spell->ID, caster, nullptr, caster) * HANDMAGIC_TICKS_PER_CIRCLE;// (spellCost / 10) + 1; //Circle once for every 10 mana the spell costs. animation_manager->mana_left = spellCost; + animation_manager->mana_cost = spellCost; animation_manager->consumeMana = true; - if ( spell->ID == SPELL_FORCEBOLT && caster->skillCapstoneUnlockedEntity(PRO_SPELLCASTING) ) + /*if ( spell->ID == SPELL_FORCEBOLT && caster->skillCapstoneUnlockedEntity(PRO_LEGACY_SPELLCASTING) ) { animation_manager->consumeMana = false; - } + }*/ - if (stat->getModifiedProficiency(PRO_SPELLCASTING) < SPELLCASTING_BEGINNER) //There's a chance that caster is newer to magic (and thus takes longer to cast a spell). + if ( isSpellcasterBeginner(player, caster, spell->skillID) ) //There's a chance that caster is newer to magic (and thus takes longer to cast a spell). { int chance = local_rng.rand() % 10; - if (chance >= stat->getModifiedProficiency(PRO_SPELLCASTING) / 15) + if (chance >= stat->getModifiedProficiency(spell->skillID) / 15) { - int amount = (local_rng.rand() % 50) / std::max(stat->getModifiedProficiency(PRO_SPELLCASTING) + statGetINT(stat, caster), 1); + int amount = (local_rng.rand() % 50) / std::max(stat->getModifiedProficiency(spell->skillID) + statGetINT(stat, caster), 1); amount = std::min(amount, CASTING_EXTRA_TIMES_CAP); - animation_manager->times_to_circle += amount; + animation_manager->times_to_circle += amount * HANDMAGIC_TICKS_PER_CIRCLE; } } if ( usingSpellbook && stat->shield && itemCategory(stat->shield) == SPELLBOOK ) @@ -136,7 +1051,7 @@ void fireOffSpellAnimation(spellcasting_animation_manager_t* animation_manager, if ( !playerLearnedSpellbook(player, stat->shield) || (stat->shield->beatitude < 0 && !shouldInvertEquipmentBeatitude(stat)) ) { // for every tier below the spell you are, add 3 circle for 1 tier, or add 2 for every additional tier. - int casterAbility = std::min(100, std::max(0, stat->getModifiedProficiency(PRO_SPELLCASTING) + statGetINT(stat, caster))) / 20; + int casterAbility = std::min(100, std::max(0, stat->getModifiedProficiency(spell->skillID) + statGetINT(stat, caster))) / 20; if ( stat->shield->beatitude < 0 ) { casterAbility = 0; // cursed book has cast penalty. @@ -144,25 +1059,51 @@ void fireOffSpellAnimation(spellcasting_animation_manager_t* animation_manager, int difficulty = spell->difficulty / 20; if ( difficulty > casterAbility ) { - animation_manager->times_to_circle += (std::min(5, 1 + 2 * (difficulty - casterAbility))); + animation_manager->times_to_circle += (std::min(5, 1 + 2 * (difficulty - casterAbility))) * HANDMAGIC_TICKS_PER_CIRCLE; } } - else if ( stat->getModifiedProficiency(PRO_SPELLCASTING) >= SPELLCASTING_BEGINNER ) + else if ( !isSpellcasterBeginner(player, caster, spell->skillID) ) { - animation_manager->times_to_circle = (spellCost / 20) + 1; //Circle once for every 20 mana the spell costs. + if ( animation_manager->times_to_circle >= HANDMAGIC_TICKS_PER_CIRCLE ) + { + //int skillLVL = std::max(0, std::min(100, stat->getModifiedProficiency(spell->skillID))); + //animation_manager->times_to_circle = std::max(HANDMAGIC_TICKS_PER_CIRCLE, (int)(animation_manager->times_to_circle - HANDMAGIC_TICKS_PER_CIRCLE * skillLVL / 100.0)); + } + //animation_manager->times_to_circle = (spellCost / 20) + 1; //Circle once for every 20 mana the spell costs. } } - animation_manager->consume_interval = (animation_manager->times_to_circle * ((2 * PI) / HANDMAGIC_CIRCLE_SPEED)) / spellCost; + animation_manager->times_to_circle = std::max(HANDMAGIC_TICKS_PER_CIRCLE / 2, animation_manager->times_to_circle); + if ( overchargeRepeat ) + { + animation_manager->times_to_circle = 0; + } + animation_manager->consume_interval = (animation_manager->times_to_circle / std::max(1, spellCost)); animation_manager->consume_timer = animation_manager->consume_interval; + animation_manager->setRangeFinderLocation(); + + spellcastAnimationUpdate(player, MONSTER_POSE_MAGIC_WINDUP1, animation_manager->times_to_circle + HANDMAGIC_TICKS_PER_CIRCLE); } void spellcastingAnimationManager_deactivate(spellcasting_animation_manager_t* animation_manager) { + if ( animation_manager->stage == ANIM_SPELL_TOUCH + || animation_manager->stage == ANIM_SPELL_TOUCH_CHARGE + || animation_manager->stage == ANIM_SPELL_OVERCHARGE_READY + || animation_manager->stage == ANIM_SPELL_OVERCHARGE_CHARGE ) + { + spellcastAnimationUpdate(animation_manager->player, MONSTER_POSE_MAGIC_CAST2, 0); + } + animation_manager->caster = -1; animation_manager->spell = NULL; animation_manager->active = false; animation_manager->active_spellbook = false; animation_manager->stage = 0; + animation_manager->circle_count = 0; + animation_manager->active_count = 0; + animation_manager->overcharge = 0; + //animation_manager->overcharge_init = 0; + animation_manager->resetRangefinder(); if ( animation_manager->player == -1 ) { @@ -180,13 +1121,111 @@ void spellcastingAnimationManager_deactivate(spellcasting_animation_manager_t* a players[animation_manager->player]->hud.magicRightHand->flags[INVISIBLE] = true; players[animation_manager->player]->hud.magicRightHand->flags[INVISIBLE_DITHER] = false; } + if ( players[animation_manager->player]->hud.magicRangefinder ) + { + players[animation_manager->player]->hud.magicRangefinder->flags[INVISIBLE] = true; + players[animation_manager->player]->hud.magicRangefinder->flags[INVISIBLE_DITHER] = false; + } } -void spellcastingAnimationManager_completeSpell(spellcasting_animation_manager_t* animation_manager) +void spellcastingAnimationManager_completeSpell(int player, spellcasting_animation_manager_t* animation_manager, bool deactivate) { - castSpell(animation_manager->caster, animation_manager->spell, false, false, animation_manager->active_spellbook); //Actually cast the spell. + if ( animation_manager->stage == ANIM_SPELL_TOUCH_THROW + || animation_manager->stage == ANIM_SPELL_OVERCHARGE_THROW ) + { + spellcastAnimationUpdate(animation_manager->player, 1, 0); + } + + int overcharge = (animation_manager->overcharge > 0 && animation_manager->stage == ANIM_SPELL_OVERCHARGE_THROW) ? animation_manager->overcharge : 0; + + if ( animation_manager->rangefinder == SpellRangefinderType::RANGEFINDER_TARGET + || animation_manager->rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE + || animation_manager->rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH_INTERACT_TEST + || animation_manager->rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH_WALL_TILE ) + { + CastSpellProps_t castSpellProps; + castSpellProps.caster_x = animation_manager->caster_x; + castSpellProps.caster_y = animation_manager->caster_y; + castSpellProps.target_x = animation_manager->target_x; + castSpellProps.target_y = animation_manager->target_y; + castSpellProps.wallDir = animation_manager->wallDir; + castSpellProps.overcharge = overcharge; + if ( animation_manager->spell ) + { + if ( animation_manager->spell->ID == SPELL_MUSHROOM ) + { + if ( spellInList(&players[player]->magic.spellList, getSpellFromID(SPELL_SPORES)) ) + { + castSpellProps.optionalData = 1; + } + if ( spellInList(&players[player]->magic.spellList, getSpellFromID(SPELL_SPORE_BOMB)) ) + { + castSpellProps.optionalData = 2; + } + } + else if ( animation_manager->spell->ID == SPELL_SHRUB ) + { + if ( spellInList(&players[player]->magic.spellList, getSpellFromID(SPELL_THORNS)) ) + { + castSpellProps.optionalData = 1; + } + if ( spellInList(&players[player]->magic.spellList, getSpellFromID(SPELL_BLADEVINES)) ) + { + castSpellProps.optionalData = 2; + } + } + } + castSpell(animation_manager->caster, animation_manager->spell, false, false, animation_manager->active_spellbook, &castSpellProps); //Actually cast the spell. + } + else if ( animation_manager->rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH + || animation_manager->rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH_INTERACT ) + { + CastSpellProps_t castSpellProps; + castSpellProps.targetUID = animation_manager->targetUid; + castSpellProps.wallDir = animation_manager->wallDir; + castSpellProps.overcharge = overcharge; + castSpell(animation_manager->caster, animation_manager->spell, false, false, animation_manager->active_spellbook, &castSpellProps); //Actually cast the spell. + } + else if ( animation_manager->spell + && (animation_manager->spell->ID == SPELL_BASTION_MUSHROOM + || animation_manager->spell->ID == SPELL_BASTION_ROOTS + || (overcharge > 0)) ) + { + CastSpellProps_t castSpellProps; + if ( animation_manager->spell->ID == SPELL_BASTION_MUSHROOM ) + { + if ( spellInList(&players[player]->magic.spellList, getSpellFromID(SPELL_SPORES)) ) + { + castSpellProps.optionalData = 1; + } + if ( spellInList(&players[player]->magic.spellList, getSpellFromID(SPELL_SPORE_BOMB)) ) + { + castSpellProps.optionalData = 2; + } + } + else if ( animation_manager->spell->ID == SPELL_BASTION_ROOTS ) + { + if ( spellInList(&players[player]->magic.spellList, getSpellFromID(SPELL_THORNS)) ) + { + castSpellProps.optionalData = 1; + } + if ( spellInList(&players[player]->magic.spellList, getSpellFromID(SPELL_BLADEVINES)) ) + { + castSpellProps.optionalData = 2; + } + } + castSpellProps.overcharge = overcharge; + castSpell(animation_manager->caster, animation_manager->spell, false, false, animation_manager->active_spellbook, &castSpellProps); //Actually cast the spell. + } + else + { + castSpell(animation_manager->caster, animation_manager->spell, false, false, animation_manager->active_spellbook); //Actually cast the spell. + } - spellcastingAnimationManager_deactivate(animation_manager); + if ( deactivate ) + { + spellcastingAnimationManager_deactivate(animation_manager); + } } /* @@ -223,6 +1262,8 @@ void actLeftHandMagic(Entity* my) return; } + my->mistformGLRender = players[HANDMAGIC_PLAYERNUM]->entity->mistformGLRender; + //Set the initial values. (For the particle spray) my->x = 8; my->y = -3; @@ -257,6 +1298,30 @@ void actLeftHandMagic(Entity* my) } } + if ( playerRace == GREMLIN ) + { + my->z -= -1 * .5; + } + else if ( playerRace == GNOME ) + { + my->z -= -2 * .5; + } + else if ( playerRace == DRYAD ) + { + if ( players[HANDMAGIC_PLAYERNUM]->entity->z >= 1.5 ) + { + my->z -= -3.0 * .5; + } + else + { + my->z -= -2.0 * .5; + } + } + else if ( playerRace == MYCONID ) + { + my->z -= -1.0 * .5; + } + bool noGloves = false; if ( stats[HANDMAGIC_PLAYERNUM]->gloves == NULL || playerRace == SPIDER @@ -304,6 +1369,26 @@ void actLeftHandMagic(Entity* my) { my->sprite = 803; } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == BONE_BRACERS ) + { + my->sprite = 2108; + } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == BLACKIRON_GAUNTLETS ) + { + my->sprite = 2110; + } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == SILVER_GAUNTLETS ) + { + my->sprite = 2112; + } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == QUILTED_GLOVES ) + { + my->sprite = 2114; + } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == CHAIN_GLOVES ) + { + my->sprite = 2116; + } } @@ -366,6 +1451,28 @@ void actLeftHandMagic(Entity* my) case CREATURE_IMP: my->sprite = 858; break; + case DRYAD: + my->sprite = 2319; + break; + case MYCONID: + my->sprite = 2327; + break; + case GREMLIN: + if ( stats[HANDMAGIC_PLAYERNUM]->sex == FEMALE ) + { + my->sprite = 2323; + } + else + { + my->sprite = 2325; + } + break; + case SALAMANDER: + my->sprite = 2329; + break; + case GNOME: + my->sprite = 2321; + break; default: my->sprite = 656; break; @@ -416,7 +1523,7 @@ void actLeftHandMagic(Entity* my) my->y = 0; my->z += 1; } - else if ( players[HANDMAGIC_PLAYERNUM]->entity->skill[3] == 1 ) // debug cam + else if ( players[HANDMAGIC_PLAYERNUM]->entity->skill[3] != 0 ) // debug cam { my->flags[INVISIBLE] = true; } @@ -427,99 +1534,347 @@ void actLeftHandMagic(Entity* my) } } - if ( (cast_animation[HANDMAGIC_PLAYERNUM].active || cast_animation[HANDMAGIC_PLAYERNUM].active_spellbook) ) - { - switch ( cast_animation[HANDMAGIC_PLAYERNUM].stage) - { - case CIRCLE: - if ( ticks % 5 == 0 && !(players[HANDMAGIC_PLAYERNUM]->entity->skill[3] == 1) ) + if ( (cast_animation[HANDMAGIC_PLAYERNUM].active || cast_animation[HANDMAGIC_PLAYERNUM].active_spellbook) ) + { + cast_animation[HANDMAGIC_PLAYERNUM].setRangeFinderLocation(); + switch ( cast_animation[HANDMAGIC_PLAYERNUM].stage) + { + case ANIM_SPELL_CIRCLE: + if ( ticks % 5 == 0 && !(players[HANDMAGIC_PLAYERNUM]->entity->skill[3] != 0) ) + { + Entity* entity = spawnGib(my, 16); + entity->flags[INVISIBLE] = false; + entity->flags[SPRITE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[OVERDRAW] = true; + entity->lightBonus = vec4(0.2f, 0.2f, 0.2f, 0.f); + entity->scalex = 0.25f; //MAKE 'EM SMALL PLEASE! + entity->scaley = 0.25f; + entity->scalez = 0.25f; + entity->sprite = 16; //TODO: Originally. 22. 16 -- spark sprite instead? + entity->z += 3.0; + if ( cast_animation[HANDMAGIC_PLAYERNUM].active_spellbook ) + { + entity->y -= 1.5; + entity->z += 1; + } + entity->yaw = ((local_rng.rand() % 6) * 60) * PI / 180.0; + entity->pitch = (local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + entity->vel_x = cos(entity->yaw) * .1; + entity->vel_y = sin(entity->yaw) * .1; + entity->vel_z = -.15; + entity->fskill[3] = 0.01; + entity->skill[11] = HANDMAGIC_PLAYERNUM; + } + cast_animation[HANDMAGIC_PLAYERNUM].consume_timer--; + if ( cast_animation[HANDMAGIC_PLAYERNUM].consume_timer < 0 && cast_animation[HANDMAGIC_PLAYERNUM].mana_left > 0 ) + { + //Time to consume mana and reset the ticker! + cast_animation[HANDMAGIC_PLAYERNUM].consume_timer = cast_animation[HANDMAGIC_PLAYERNUM].consume_interval; + if ( multiplayer == SINGLE && cast_animation[HANDMAGIC_PLAYERNUM].consumeMana ) + { + int HP = stats[HANDMAGIC_PLAYERNUM]->HP; + int MP = stats[HANDMAGIC_PLAYERNUM]->MP; + players[HANDMAGIC_PLAYERNUM]->entity->drainMP(1, false); // don't notify otherwise we'll get spammed each 1 mp + + if ( cast_animation[HANDMAGIC_PLAYERNUM].spell ) + { + bool sustainedSpell = false; + auto findSpellDef = ItemTooltips.spellItems.find(cast_animation[HANDMAGIC_PLAYERNUM].spell->ID); + if ( findSpellDef != ItemTooltips.spellItems.end() ) + { + sustainedSpell = (findSpellDef->second.spellType == ItemTooltips_t::SpellItemTypes::SPELL_TYPE_SELF_SUSTAIN); + } + if ( sustainedSpell ) + { + players[HANDMAGIC_PLAYERNUM]->mechanics.sustainedSpellIncrementMP(MP - stats[HANDMAGIC_PLAYERNUM]->MP, cast_animation[HANDMAGIC_PLAYERNUM].spell->skillID); + } + else + { + players[HANDMAGIC_PLAYERNUM]->mechanics.baseSpellIncrementMP(MP - stats[HANDMAGIC_PLAYERNUM]->MP, cast_animation[HANDMAGIC_PLAYERNUM].spell->skillID); + } + } + + if ( (HP > stats[HANDMAGIC_PLAYERNUM]->HP) && !overDrawDamageNotify ) + { + overDrawDamageNotify = true; + cameravars[HANDMAGIC_PLAYERNUM].shakex += 0.1; + cameravars[HANDMAGIC_PLAYERNUM].shakey += 10; + playSoundPlayer(HANDMAGIC_PLAYERNUM, 28, 92); + Uint32 color = makeColorRGB(255, 255, 0); + messagePlayerColor(HANDMAGIC_PLAYERNUM, MESSAGE_STATUS, color, Language::get(621)); + } + } + --cast_animation[HANDMAGIC_PLAYERNUM].mana_left; + } + + cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle += HANDMAGIC_CIRCLE_SPEED; + cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movex = cos(cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle) * HANDMAGIC_CIRCLE_RADIUS; + cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movey = sin(cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle) * HANDMAGIC_CIRCLE_RADIUS; + cast_animation[HANDMAGIC_PLAYERNUM].circle_count++; + if ( cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle >= 2 * PI ) //Completed one loop. + { + cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle -= 2 * PI; + } + if ( cast_animation[HANDMAGIC_PLAYERNUM].circle_count >= cast_animation[HANDMAGIC_PLAYERNUM].times_to_circle) + //Finished circling. Time to move on! + { + cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle = 0; + if ( cast_animation[HANDMAGIC_PLAYERNUM].rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH + || cast_animation[HANDMAGIC_PLAYERNUM].rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH_INTERACT + || cast_animation[HANDMAGIC_PLAYERNUM].rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH_INTERACT_TEST + || cast_animation[HANDMAGIC_PLAYERNUM].rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE + || cast_animation[HANDMAGIC_PLAYERNUM].rangefinder == SpellRangefinderType::RANGEFINDER_TOUCH_WALL_TILE ) + { + cast_animation[HANDMAGIC_PLAYERNUM].stage = ANIM_SPELL_TOUCH; + if ( cast_animation[HANDMAGIC_PLAYERNUM].overcharge > 0 ) + { + cast_animation[HANDMAGIC_PLAYERNUM].stage = ANIM_SPELL_OVERCHARGE_READY; + } + if ( *cvar_vibe_spell_s > 0 ) + { + inputs.rumble(HANDMAGIC_PLAYERNUM, GameController::Haptic_t::RUMBLE_SPELL, *cvar_vibe_spell_x, + *cvar_vibe_spell_y, *cvar_vibe_spell_s, 0); + } + spellcastAnimationUpdate(HANDMAGIC_PLAYERNUM, MONSTER_POSE_MAGIC_WINDUP2, 0); + playSoundEntityLocal(players[HANDMAGIC_PLAYERNUM]->entity, 759 + local_rng.rand() % 4, 92); + } + else if ( cast_animation[HANDMAGIC_PLAYERNUM].overcharge > 0 ) + { + cast_animation[HANDMAGIC_PLAYERNUM].stage = ANIM_SPELL_OVERCHARGE_READY; + spellcastAnimationUpdate(HANDMAGIC_PLAYERNUM, MONSTER_POSE_MAGIC_WINDUP2, 0); + playSoundEntityLocal(players[HANDMAGIC_PLAYERNUM]->entity, 759 + local_rng.rand() % 4, 92); + } + else + { + cast_animation[HANDMAGIC_PLAYERNUM].stage = ANIM_SPELL_THROW; + } + } + break; + case ANIM_SPELL_THROW: + //messagePlayer(HANDMAGIC_PLAYERNUM, "IN THROW"); + //TODO: Throw animation! Or something. + cast_animation[HANDMAGIC_PLAYERNUM].stage = ANIM_SPELL_COMPLETE_SPELL; + break; + case ANIM_SPELL_TOUCH_CHARGE: + case ANIM_SPELL_OVERCHARGE_CHARGE: + { + my->flags[INVISIBLE] = true; + my->flags[INVISIBLE_DITHER] = false; + + bool levitating = isLevitating(stats[HANDMAGIC_PLAYERNUM]); + + //Water walking boots + bool waterwalkingboots = false; + /*if ( skillCapstoneUnlocked(HANDMAGIC_PLAYERNUM, PRO_LEGACY_SWIMMING) ) + { + waterwalkingboots = true; + }*/ + if ( stats[HANDMAGIC_PLAYERNUM]->shoes != NULL ) + { + if ( stats[HANDMAGIC_PLAYERNUM]->shoes->type == IRON_BOOTS_WATERWALKING ) + { + waterwalkingboots = true; + } + } + + //Check if swimming. + if ( !waterwalkingboots && !levitating ) + { + bool swimming = false; + int x = std::min(std::max(0, floor(players[HANDMAGIC_PLAYERNUM]->entity->x / 16)), map.width - 1); + int y = std::min(std::max(0, floor(players[HANDMAGIC_PLAYERNUM]->entity->y / 16)), map.height - 1); + if ( swimmingtiles[map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]] || lavatiles[map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]] ) + { + swimming = true; + } + if ( swimming ) + { + //Can't cast spells while swimming if not levitating or water walking. + messagePlayer(HANDMAGIC_PLAYERNUM, MESSAGE_MISC, Language::get(410)); + + playSoundEntityLocal(players[HANDMAGIC_PLAYERNUM]->entity, 163, 64); + spellcastingAnimationManager_deactivate(&cast_animation[HANDMAGIC_PLAYERNUM]); + break; + } + } + + auto& anim = cast_animation[HANDMAGIC_PLAYERNUM]; + if ( anim.throw_count == 0 ) + { + anim.lefthand_movex = 0.f; + anim.lefthand_movey = 0.f; + anim.lefthand_angle = 0.0; + anim.vibrate_x = 0.f; + anim.vibrate_y = 0.f; + } + if ( ticks % 2 == 0 ) + { + anim.lefthand_movex -= anim.vibrate_x; + anim.lefthand_movey -= anim.vibrate_y; + anim.vibrate_x = (local_rng.rand() % 30 - 10) / 150.f; + anim.vibrate_y = (local_rng.rand() % 30 - 10) / 150.f; + anim.lefthand_movex += anim.vibrate_x; + anim.lefthand_movey += anim.vibrate_y; + } + anim.lefthand_movex = std::max(-1.f, anim.lefthand_movex - 0.1f); + anim.throw_count++; + break; + } + case ANIM_SPELL_TOUCH_THROW: + case ANIM_SPELL_OVERCHARGE_THROW: + { + my->flags[INVISIBLE] = true; + my->flags[INVISIBLE_DITHER] = false; + + bool overchargeRepeat = cast_animation[HANDMAGIC_PLAYERNUM].stage == ANIM_SPELL_OVERCHARGE_THROW + && cast_animation[HANDMAGIC_PLAYERNUM].overcharge > 0; + + auto& anim = cast_animation[HANDMAGIC_PLAYERNUM]; + if ( anim.throw_count == 0 ) + { + spellcastingAnimationManager_completeSpell(HANDMAGIC_PLAYERNUM, &cast_animation[HANDMAGIC_PLAYERNUM], false); + if ( players[HANDMAGIC_PLAYERNUM]->entity->skill[3] == 0 ) // debug cam OFF + { + cameravars[HANDMAGIC_PLAYERNUM].shakex += .03; + cameravars[HANDMAGIC_PLAYERNUM].shakey += 4; + } + anim.lefthand_movex = 5.f; + anim.lefthand_angle = 0.0; + } + else if ( anim.throw_count < 5 ) + { + anim.lefthand_angle = std::min(anim.lefthand_angle + 0.025f, 1.f); + } + else + { + float setpointDiff = std::max(.05f, (1.f - anim.lefthand_angle) / 10.f); + anim.lefthand_angle += setpointDiff; + anim.lefthand_angle = std::min(1.f, anim.lefthand_angle); + } + + float neutralSetpoint = -2.5f; + if ( overchargeRepeat ) + { + neutralSetpoint = -5.f; + } + + + if ( anim.throw_count > 0 ) { - Entity* entity = spawnGib(my); - entity->flags[INVISIBLE] = false; - entity->flags[SPRITE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UPDATENEEDED] = false; - entity->flags[OVERDRAW] = true; - entity->lightBonus = vec4(0.2f, 0.2f, 0.2f, 0.f); - entity->scalex = 0.25f; //MAKE 'EM SMALL PLEASE! - entity->scaley = 0.25f; - entity->scalez = 0.25f; - entity->sprite = 16; //TODO: Originally. 22. 16 -- spark sprite instead? - entity->z += 3.0; - if ( cast_animation[HANDMAGIC_PLAYERNUM].active_spellbook ) + if ( anim.lefthand_angle >= 1.f ) { - entity->y -= 1.5; - entity->z += 1; + anim.lefthand_movex = std::max(neutralSetpoint, anim.lefthand_movex - 0.75f); + if ( overchargeRepeat ) + { + if ( anim.throw_count >= 30 && cast_animation[HANDMAGIC_PLAYERNUM].overcharge > 1 ) + { + anim.lefthand_movex *= 1.0 - std::min(1.0, (anim.throw_count - 30) / (50.0 - 30.0)); + } + } + } + else + { + anim.lefthand_movex = std::max(0.0, 2.5f + -2.5f * sin((-PI / 2) + (anim.lefthand_angle) * PI)); } - entity->yaw = ((local_rng.rand() % 6) * 60) * PI / 180.0; - entity->pitch = (local_rng.rand() % 360) * PI / 180.0; - entity->roll = (local_rng.rand() % 360) * PI / 180.0; - entity->vel_x = cos(entity->yaw) * .1; - entity->vel_y = sin(entity->yaw) * .1; - entity->vel_z = -.15; - entity->fskill[3] = 0.01; - entity->skill[11] = HANDMAGIC_PLAYERNUM; } - cast_animation[HANDMAGIC_PLAYERNUM].consume_timer--; - if ( cast_animation[HANDMAGIC_PLAYERNUM].consume_timer < 0 && cast_animation[HANDMAGIC_PLAYERNUM].mana_left > 0 ) + + + if ( overchargeRepeat || (!overchargeRepeat && anim.lefthand_movex <= neutralSetpoint) ) { - //Time to consume mana and reset the ticker! - cast_animation[HANDMAGIC_PLAYERNUM].consume_timer = cast_animation[HANDMAGIC_PLAYERNUM].consume_interval; - if ( multiplayer == SINGLE && cast_animation[HANDMAGIC_PLAYERNUM].consumeMana ) + if ( overchargeRepeat ) { - int HP = stats[HANDMAGIC_PLAYERNUM]->HP; - int MP = stats[HANDMAGIC_PLAYERNUM]->MP; - players[HANDMAGIC_PLAYERNUM]->entity->drainMP(1, false); // don't notify otherwise we'll get spammed each 1 mp - - if ( cast_animation[HANDMAGIC_PLAYERNUM].spell ) + if ( anim.throw_count >= 50 /*|| (cast_animation[HANDMAGIC_PLAYERNUM].overcharge == 1 && anim.throw_count >= 20)*/ ) { - bool sustainedSpell = false; - auto findSpellDef = ItemTooltips.spellItems.find(cast_animation[HANDMAGIC_PLAYERNUM].spell->ID); - if ( findSpellDef != ItemTooltips.spellItems.end() ) + --cast_animation[HANDMAGIC_PLAYERNUM].overcharge; + if ( cast_animation[HANDMAGIC_PLAYERNUM].overcharge == 0 ) { - sustainedSpell = (findSpellDef->second.spellType == ItemTooltips_t::SpellItemTypes::SPELL_TYPE_SELF_SUSTAIN); + if ( stats[HANDMAGIC_PLAYERNUM]->weapon ) + { + players[HANDMAGIC_PLAYERNUM]->hud.weaponSwitch = true; + } + spellcastingAnimationManager_deactivate(&cast_animation[HANDMAGIC_PLAYERNUM]); } - if ( sustainedSpell ) + else { - players[HANDMAGIC_PLAYERNUM]->mechanics.sustainedSpellIncrementMP(MP - stats[HANDMAGIC_PLAYERNUM]->MP); + fireOffSpellAnimation(&cast_animation[HANDMAGIC_PLAYERNUM], cast_animation[HANDMAGIC_PLAYERNUM].caster, + cast_animation[HANDMAGIC_PLAYERNUM].spell, cast_animation[HANDMAGIC_PLAYERNUM].active_spellbook, false); } } - - if ( (HP > stats[HANDMAGIC_PLAYERNUM]->HP) && !overDrawDamageNotify ) + } + else + { + if ( stats[HANDMAGIC_PLAYERNUM]->weapon ) { - overDrawDamageNotify = true; - cameravars[HANDMAGIC_PLAYERNUM].shakex += 0.1; - cameravars[HANDMAGIC_PLAYERNUM].shakey += 10; - playSoundPlayer(HANDMAGIC_PLAYERNUM, 28, 92); - Uint32 color = makeColorRGB(255, 255, 0); - messagePlayerColor(HANDMAGIC_PLAYERNUM, MESSAGE_STATUS, color, Language::get(621)); + players[HANDMAGIC_PLAYERNUM]->hud.weaponSwitch = true; } + spellcastingAnimationManager_deactivate(&cast_animation[HANDMAGIC_PLAYERNUM]); } - --cast_animation[HANDMAGIC_PLAYERNUM].mana_left; } + anim.throw_count++; + break; + } + case ANIM_SPELL_TOUCH: + case ANIM_SPELL_OVERCHARGE_READY: + { + my->flags[INVISIBLE] = true; + my->flags[INVISIBLE_DITHER] = false; - cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle += HANDMAGIC_CIRCLE_SPEED; - cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movex = cos(cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle) * HANDMAGIC_CIRCLE_RADIUS; - cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movey = sin(cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle) * HANDMAGIC_CIRCLE_RADIUS; - if ( cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle >= 2 * PI) //Completed one loop. + cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle += HANDMAGIC_CIRCLE_SPEED / 4; + cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movex = cos(cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle) * HANDMAGIC_CIRCLE_RADIUS * 0.25; + cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movey = sin(cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle) * HANDMAGIC_CIRCLE_RADIUS * 0.25; + if ( cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle >= 2 * PI ) //Completed one loop. { cast_animation[HANDMAGIC_PLAYERNUM].lefthand_angle = 0; cast_animation[HANDMAGIC_PLAYERNUM].circle_count++; - if ( cast_animation[HANDMAGIC_PLAYERNUM].circle_count >= cast_animation[HANDMAGIC_PLAYERNUM].times_to_circle) - //Finished circling. Time to move on! + } + + bool levitating = isLevitating(stats[HANDMAGIC_PLAYERNUM]); + + //Water walking boots + bool waterwalkingboots = false; + /*if ( skillCapstoneUnlocked(HANDMAGIC_PLAYERNUM, PRO_LEGACY_SWIMMING) ) + { + waterwalkingboots = true; + }*/ + if ( stats[HANDMAGIC_PLAYERNUM]->shoes != NULL ) + { + if ( stats[HANDMAGIC_PLAYERNUM]->shoes->type == IRON_BOOTS_WATERWALKING ) + { + waterwalkingboots = true; + } + } + + //Check if swimming. + if ( !waterwalkingboots && !levitating ) + { + bool swimming = false; + int x = std::min(std::max(0, floor(players[HANDMAGIC_PLAYERNUM]->entity->x / 16)), map.width - 1); + int y = std::min(std::max(0, floor(players[HANDMAGIC_PLAYERNUM]->entity->y / 16)), map.height - 1); + if ( swimmingtiles[map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]] || lavatiles[map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]] ) { - cast_animation[HANDMAGIC_PLAYERNUM].stage++; + swimming = true; + } + if ( swimming ) + { + //Can't cast spells while swimming if not levitating or water walking. + messagePlayer(HANDMAGIC_PLAYERNUM, MESSAGE_MISC, Language::get(410)); + + playSoundEntityLocal(players[HANDMAGIC_PLAYERNUM]->entity, 163, 64); + spellcastingAnimationManager_deactivate(&cast_animation[HANDMAGIC_PLAYERNUM]); } } + } break; - case THROW: - //messagePlayer(HANDMAGIC_PLAYERNUM, "IN THROW"); - //TODO: Throw animation! Or something. - cast_animation[HANDMAGIC_PLAYERNUM].stage++; + case ANIM_SPELL_COMPLETE_NOCAST: + spellcastingAnimationManager_deactivate(&cast_animation[HANDMAGIC_PLAYERNUM]); break; + case ANIM_SPELL_COMPLETE_SPELL: default: //messagePlayer(HANDMAGIC_PLAYERNUM, "DEFAULT CASE"); - spellcastingAnimationManager_completeSpell(&cast_animation[HANDMAGIC_PLAYERNUM]); + spellcastingAnimationManager_completeSpell(HANDMAGIC_PLAYERNUM, &cast_animation[HANDMAGIC_PLAYERNUM], true); break; } } @@ -561,12 +1916,134 @@ void actLeftHandMagic(Entity* my) my->pitch = defaultpitch + HANDMAGIC_PITCH - cameravars[HANDMAGIC_PLAYERNUM].shakey2 / 200.f; my->roll = HANDMAGIC_ROLL; my->focalz = -1.5; + + if ( playerRace == GREMLIN ) + { + my->z -= -1 * .5; + } + else if ( playerRace == GNOME ) + { + my->z -= -2 * .5; + } + else if ( playerRace == DRYAD ) + { + if ( players[HANDMAGIC_PLAYERNUM]->entity->z >= 1.5 ) + { + my->z -= -3.0 * .5; + } + else + { + my->z -= -2.0 * .5; + } + } + else if ( playerRace == MYCONID ) + { + my->z -= -1.0 * .5; + } + + if ( stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MINIMISE) ) + { + my->z += -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MINIMISE) & 0xF) * .5; + } + if ( stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) ) + { + my->z -= -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) & 0xF) * .5; + } } //my->y = 3 + HUDWEAPON_MOVEY; //my->z = (camera.z*.5-players[HANDMAGIC_PLAYERNUM]->z)+7+HUDWEAPON_MOVEZ; //TODO: NOT a PLAYERSWAP my->x += cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movex; my->y += cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movey; + + if ( cast_animation[HANDMAGIC_PLAYERNUM].active || cast_animation[HANDMAGIC_PLAYERNUM].active_spellbook ) + { + if ( cast_animation[HANDMAGIC_PLAYERNUM].stage == ANIM_SPELL_TOUCH + || cast_animation[HANDMAGIC_PLAYERNUM].stage == ANIM_SPELL_TOUCH_CHARGE + || cast_animation[HANDMAGIC_PLAYERNUM].stage == ANIM_SPELL_OVERCHARGE_READY + || cast_animation[HANDMAGIC_PLAYERNUM].stage == ANIM_SPELL_OVERCHARGE_CHARGE ) + { + float x = my->x + 2.5; + float y = -my->y; + float z = my->z - 0.0; + + Monster playerRace = players[HANDMAGIC_PLAYERNUM]->entity->getMonsterFromPlayerRace(stats[HANDMAGIC_PLAYERNUM]->playerRace); + if ( players[HANDMAGIC_PLAYERNUM]->entity->effectShapeshift != NOTHING ) + { + playerRace = static_cast(players[HANDMAGIC_PLAYERNUM]->entity->effectShapeshift); + if ( playerRace == RAT ) + { + y = 0.0; + } + else if ( playerRace == SPIDER ) + { + y = 0.0; + z -= 2; + } + } + + // boosty boost + Uint32 castLoopDuration = 4 * TICKS_PER_SECOND / 10; + for ( int i = 1; i < 3 && !(players[HANDMAGIC_PLAYERNUM]->entity->skill[3] != 0); ++i ) + { + //if ( i == 1 || i == 3 ) { continue; } + Uint32 animTick = cast_animation[HANDMAGIC_PLAYERNUM].active_count >= castLoopDuration + ? castLoopDuration + : cast_animation[HANDMAGIC_PLAYERNUM].active_count; + + Entity* entity = newEntity(1243, 1, map.entities, nullptr); //Particle entity. + entity->x = x - 0.01 * (5 + local_rng.rand() % 11); + entity->y = y - 0.01 * (5 + local_rng.rand() % 11); + entity->z = z - 0.01 * (10 + local_rng.rand() % 21); + if ( i == 1 ) + { + entity->y += -2; + entity->y += -2 * sin(2 * PI * (animTick % 40) / 40.f); + } + else + { + entity->y += -(-2); + entity->y -= -2 * sin(2 * PI * (animTick % 40) / 40.f); + } + + entity->focalz = -2; + + real_t scale = 0.05f; + scale += (animTick) * 0.025f; + scale = std::min(scale, 0.5); + + entity->scalex = scale; + entity->scaley = scale; + entity->scalez = scale; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = 0; + entity->roll = i * 2 * PI / 3; + entity->pitch = PI + ((animTick % 40) / 40.f) * 2 * PI; + entity->ditheringDisabled = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[OVERDRAW] = true; + entity->lightBonus = vec4(0.25f, 0.25f, + 0.25f, 0.f); + entity->behavior = &actHUDMagicParticle; + entity->vel_z = 0; + entity->skill[11] = HANDMAGIC_PLAYERNUM; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } + ++cast_animation[HANDMAGIC_PLAYERNUM].active_count; + } + } + else + { + cast_animation[HANDMAGIC_PLAYERNUM].active_count = 0; + } } void actRightHandMagic(Entity* my) @@ -593,6 +2070,8 @@ void actRightHandMagic(Entity* my) return; } + my->mistformGLRender = players[HANDMAGIC_PLAYERNUM]->entity->mistformGLRender; + my->x = 8; my->y = 3; my->z = (cameras[HANDMAGIC_PLAYERNUM].z * .5 - players[HANDMAGIC_PLAYERNUM]->entity->z) + 7; @@ -626,6 +2105,39 @@ void actRightHandMagic(Entity* my) } } + if ( playerRace == GREMLIN ) + { + my->z -= -1 * .5; + } + else if ( playerRace == GNOME ) + { + my->z -= -2 * .5; + } + else if ( playerRace == DRYAD ) + { + if ( players[HANDMAGIC_PLAYERNUM]->entity->z >= 1.5 ) + { + my->z -= -3.0 * .5; + } + else + { + my->z -= -2.0 * .5; + } + } + else if ( playerRace == MYCONID ) + { + my->z -= -1.0 * .5; + } + + if ( stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MINIMISE) ) + { + my->z += -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MINIMISE) & 0xF) * .5; + } + if ( stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) ) + { + my->z -= -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) & 0xF) * .5; + } + bool noGloves = false; if ( stats[HANDMAGIC_PLAYERNUM]->gloves == NULL || playerRace == SPIDER @@ -673,6 +2185,26 @@ void actRightHandMagic(Entity* my) { my->sprite = 802; } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == BONE_BRACERS ) + { + my->sprite = 2107; + } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == BLACKIRON_GAUNTLETS ) + { + my->sprite = 2109; + } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == SILVER_GAUNTLETS ) + { + my->sprite = 2111; + } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == QUILTED_GLOVES ) + { + my->sprite = 2113; + } + else if ( stats[HANDMAGIC_PLAYERNUM]->gloves->type == CHAIN_GLOVES ) + { + my->sprite = 2115; + } } if ( noGloves ) @@ -733,6 +2265,28 @@ void actRightHandMagic(Entity* my) case CREATURE_IMP: my->sprite = 857; break; + case DRYAD: + my->sprite = 2320; + break; + case MYCONID: + my->sprite = 2328; + break; + case GREMLIN: + if ( stats[HANDMAGIC_PLAYERNUM]->sex == FEMALE ) + { + my->sprite = 2324; + } + else + { + my->sprite = 2326; + } + break; + case SALAMANDER: + my->sprite = 2330; + break; + case GNOME: + my->sprite = 2322; + break; default: my->sprite = 634; break; @@ -765,7 +2319,7 @@ void actRightHandMagic(Entity* my) my->y = 0; my->z += 1; } - else if ( players[HANDMAGIC_PLAYERNUM]->entity->skill[3] == 1 ) // debug cam + else if ( players[HANDMAGIC_PLAYERNUM]->entity->skill[3] != 0 ) // debug cam { my->flags[INVISIBLE] = true; } @@ -800,8 +2354,8 @@ void actRightHandMagic(Entity* my) { switch ( cast_animation[HANDMAGIC_PLAYERNUM].stage) { - case CIRCLE: - if ( ticks % 5 == 0 && !(players[HANDMAGIC_PLAYERNUM]->entity->skill[3] == 1) ) + case ANIM_SPELL_CIRCLE: + if ( ticks % 5 == 0 && !(players[HANDMAGIC_PLAYERNUM]->entity->skill[3] != 0) ) { //messagePlayer(0, "Pingas!"); Entity* entity = spawnGib(my); @@ -828,8 +2382,13 @@ void actRightHandMagic(Entity* my) entity->skill[11] = HANDMAGIC_PLAYERNUM; } break; - case THROW: + case ANIM_SPELL_THROW: break; + case ANIM_SPELL_TOUCH: + break; + case ANIM_SPELL_OVERCHARGE_READY: + case ANIM_SPELL_OVERCHARGE_CHARGE: + case ANIM_SPELL_OVERCHARGE_THROW: default: break; } @@ -864,8 +2423,196 @@ void actRightHandMagic(Entity* my) my->pitch = defaultpitch + HANDMAGIC_PITCH - cameravars[HANDMAGIC_PLAYERNUM].shakey2 / 200.f; my->roll = HANDMAGIC_ROLL; my->focalz = -1.5; + + if ( playerRace == GREMLIN ) + { + my->z -= -1 * .5; + } + else if ( playerRace == GNOME ) + { + my->z -= -2 * .5; + } + else if ( playerRace == DRYAD ) + { + if ( players[HANDMAGIC_PLAYERNUM]->entity->z >= 1.5 ) + { + my->z -= -3.0 * .5; + } + else + { + my->z -= -2.0 * .5; + } + } + else if ( playerRace == MYCONID ) + { + my->z -= -1.0 * .5; + } + + if ( stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MINIMISE) ) + { + my->z += -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MINIMISE) & 0xF) * .5; + } + if ( stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) ) + { + my->z -= -Player::PlayerMovement_t::minimiseMaximiseCameraZ * (stats[HANDMAGIC_PLAYERNUM]->getEffectActive(EFF_MAXIMISE) & 0xF) * .5; + } } my->x += cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movex; my->y -= cast_animation[HANDMAGIC_PLAYERNUM].lefthand_movey; } + +#define HANDMAGIC_RANGEFINDER_ALPHA my->fskill[0] +#define HANDMAGIC_RANGEFINDER_COLOR_R my->fskill[1] +#define HANDMAGIC_RANGEFINDER_COLOR_G my->fskill[2] +#define HANDMAGIC_RANGEFINDER_COLOR_B my->fskill[3] +void actMagicRangefinder(Entity* my) +{ + my->flags[INVISIBLE_DITHER] = false; + if ( intro == true ) + { + my->flags[INVISIBLE] = true; + return; + } + + //Initialize + if ( !HANDMAGIC_INIT ) + { + HANDMAGIC_INIT = 1; + } + + if ( players[HANDMAGIC_PLAYERNUM] == nullptr || players[HANDMAGIC_PLAYERNUM]->entity == nullptr + || (players[HANDMAGIC_PLAYERNUM]->entity && players[HANDMAGIC_PLAYERNUM]->entity->playerCreatedDeathCam != 0) ) + { + if ( auto indicator = AOEIndicators_t::getIndicator(my->actSpriteUseCustomSurface) ) + { + indicator->expired = true; + } + if ( players[HANDMAGIC_PLAYERNUM] ) + { + players[HANDMAGIC_PLAYERNUM]->hud.magicRangefinder = nullptr; + } + list_RemoveNode(my->mynode); + return; + } + + auto& cast_anim = cast_animation[HANDMAGIC_PLAYERNUM]; + + if ( !(cast_anim.active || cast_anim.active_spellbook) || !cast_anim.rangefinder + || cast_anim.stage == ANIM_SPELL_TOUCH_THROW + || cast_anim.stage == ANIM_SPELL_OVERCHARGE_THROW + || cast_anim.stage == ANIM_SPELL_CIRCLE ) + { + my->flags[INVISIBLE] = true; + return; + } + if ( my->flags[INVISIBLE] ) + { + my->bNeedsRenderPositionInit = true; + } + my->flags[INVISIBLE] = false; + my->sprite = 222; + my->x = cast_anim.target_x; + my->y = cast_anim.target_y; + Entity* target = nullptr; + if ( cast_anim.targetUid != 0 + && (cast_anim.rangefinder == RANGEFINDER_TOUCH + || cast_anim.rangefinder == RANGEFINDER_TOUCH_INTERACT_TEST + || cast_anim.rangefinder == RANGEFINDER_TOUCH_INTERACT) ) + { + if ( target = uidToEntity(cast_anim.targetUid) ) + { + my->x = target->x; + my->y = target->y; + if ( target->behavior == &actArrowTrap + || target->behavior == &actMagicTrap ) + { + my->x = static_cast(my->x / 16); + my->y = static_cast(my->y / 16); + } + } + } + my->z = 7.499; + static ConsoleVariable cvar_player_cast_indicator_scale("/player_cast_indicator_scale", 1.0); + static ConsoleVariable cvar_player_cast_indicator_rotate("/player_cast_indicator_rotate", 0.0); + static ConsoleVariable cvar_player_cast_indicator_alpha("/player_cast_indicator_alpha", 0.5); + static ConsoleVariable cvar_player_cast_indicator_alpha_glow("/player_cast_indicator_alpha_glow", 0.0625); + static ConsoleVariable cvar_player_cast_indicator_r("/player_cast_indicator_r", 1.0); + static ConsoleVariable cvar_player_cast_indicator_g("/player_cast_indicator_g", 1.0); + static ConsoleVariable cvar_player_cast_indicator_b("/player_cast_indicator_b", 1.0); + my->ditheringDisabled = true; + my->flags[SPRITE] = true; + my->flags[PASSABLE] = true; + my->flags[NOUPDATE] = true; + my->flags[UNCLICKABLE] = true; + my->flags[BRIGHT] = true; + my->scalex = *cvar_player_cast_indicator_scale; + my->scaley = *cvar_player_cast_indicator_scale; + my->pitch = 0; + my->roll = -PI / 2; + if ( cast_anim.rangefinder == RANGEFINDER_TOUCH_WALL_TILE ) + { + if ( cast_anim.wallDir == 0 ) + { + my->flags[INVISIBLE] = true; + } + } + + if ( (cast_anim.rangefinder == RANGEFINDER_TOUCH && target) + || cast_anim.rangefinder == RANGEFINDER_TOUCH_WALL_TILE ) + { + if ( cast_anim.wallDir > 0 ) + { + my->roll = PI; + my->yaw = cast_anim.wallDir * PI / 2; + my->z = 0.0; + + if ( cast_anim.wallDir == 1 ) + { + my->x = my->x * 16.0 + 16.001; + my->y = my->y * 16.0 + 8.0; + } + else if ( cast_anim.wallDir == 3 ) + { + my->x = my->x * 16.0 - 0.001; + my->y = my->y * 16.0 + 8.0; + } + else if ( cast_anim.wallDir == 2 ) + { + my->x = my->x * 16.0 + 8.0; + my->y = my->y * 16.0 + 16.001; + } + else if ( cast_anim.wallDir == 4 ) + { + my->x = my->x * 16.0 + 8.0; + my->y = my->y * 16.0 - 0.001; + } + } + } + + my->flags[ENTITY_SKIP_CULLING] = true; + + HANDMAGIC_RANGEFINDER_COLOR_R = *cvar_player_cast_indicator_r; + HANDMAGIC_RANGEFINDER_COLOR_G = *cvar_player_cast_indicator_g; + HANDMAGIC_RANGEFINDER_COLOR_B = *cvar_player_cast_indicator_b; + HANDMAGIC_RANGEFINDER_ALPHA = *cvar_player_cast_indicator_alpha + + *cvar_player_cast_indicator_alpha_glow * sin(2 * PI * (my->ticks % TICKS_PER_SECOND) / (real_t)(TICKS_PER_SECOND)); + my->yaw += *cvar_player_cast_indicator_rotate; + + if ( !AOEIndicators_t::getIndicator(my->actSpriteUseCustomSurface) ) + { + int size = 20; + my->actSpriteUseCustomSurface = AOEIndicators_t::createIndicator(4, size, size * 2 + 4, -1); + } + if ( auto indicator = AOEIndicators_t::getIndicator(my->actSpriteUseCustomSurface) ) + { + indicator->cacheType = AOEIndicators_t::CACHE_CASTING; + indicator->gradient = 6; + indicator->radiusMin = 8; + indicator->castingTarget = true; + indicator->loop = true; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 4; + indicator->delayTicks = 0; + } +} \ No newline at end of file diff --git a/src/magic/actmagic.cpp b/src/magic/actmagic.cpp index d7a5d7a5f..0a40c97b1 100644 --- a/src/magic/actmagic.cpp +++ b/src/magic/actmagic.cpp @@ -26,14 +26,233 @@ #include "magic.hpp" #include "../mod_tools.hpp" -static const char* colorForSprite(Entity* my, int sprite, bool darker) { +static ConsoleVariable cvar_magic_fx_light_bonus("/magic_fx_light_bonus", 0.25f); +static ConsoleVariable cvar_magic_fx_use_vismap("/magic_fx_use_vismap", true); + +void spawnAdditionalParticleForMissile(Entity* my) +{ + if ( !my ) { return; } + + if ( my->ticks == 1 ) + { + if ( my->sprite == 2191 ) + { + if ( Entity* fx = createParticleAestheticOrbit(my, 2192, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_SCEPTER_BLAST_ORBIT1) ) + { + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->yaw = my->yaw + PI / 4; + } + if ( Entity* fx = createParticleAestheticOrbit(my, 2193, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_SCEPTER_BLAST_ORBIT1) ) + { + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->yaw = my->yaw - PI / 4; + } + } + else if ( my->sprite == 2364 ) + { + if ( Entity* fx = createParticleAestheticOrbit(my, 2365, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_BLOOD_WAVES_ORBIT) ) + { + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->fskill[1] = 1.0; + } + if ( Entity* fx = createParticleAestheticOrbit(my, 2366, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_BLOOD_WAVES_ORBIT) ) + { + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->fskill[1] = -1.0; + } + } + else if ( my->sprite == 2407 ) + { + if ( Entity* fx = createParticleAestheticOrbit(my, 2406, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_HOLY_BEAM_ORBIT) ) + { + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->fskill[1] = 1.0; + } + if ( Entity* fx = createParticleAestheticOrbit(my, 2406, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_HOLY_BEAM_ORBIT) ) + { + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->fskill[1] = -1.0; + } + } + else if ( my->sprite == 2209 ) + { + if ( Entity* fx = createParticleAestheticOrbit(my, 2210, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_SCEPTER_BLAST_ORBIT1) ) + { + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->yaw = my->yaw + PI / 4; + } + if ( Entity* fx = createParticleAestheticOrbit(my, 2211, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_SCEPTER_BLAST_ORBIT1) ) + { + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->yaw = my->yaw - PI / 4; + } + if ( multiplayer == SERVER ) + { + if ( my->actmagicDelayMove > 2 * TICKS_PER_SECOND || (my->actmagicDelayMove == TICKS_PER_SECOND - 1) ) + { + serverSpawnMiscParticlesAtLocation(my->x, my->y, my->z, PARTICLE_EFFECT_METEOR_STATIONARY_ORBIT, 2209, my->actmagicDelayMove, my->yaw * 256.0); + } + } + } + } +} + +void spawnBasicMagicParticleForMissile(Entity* my) +{ + if ( !my ) { return; } + + if ( my->sprite == 2407 ) + { + my->lightBonus = vec4{ *cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, 0.f }; + } + + if ( *cvar_magic_fx_use_vismap && !intro ) + { + int x = my->x / 16.0; + int y = my->y / 16.0; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) + { + if ( my->sprite == 233 && my->flags[SPRITE] ) + { + if ( my->sprite >= 233 && my->sprite <= 244 ) + { + if ( my->ticks % 2 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, 1.0, 1.0) ) + { + fx->sprite = my->sprite; + fx->flags[SPRITE] = my->flags[SPRITE]; + fx->ditheringDisabled = true; + fx->fskill[1] = 0.025; // decay size + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + + real_t dir = atan2(my->vel_y, my->vel_x); + fx->x -= 2.0 * cos(dir); + fx->y -= 2.0 * sin(dir); + } + } + } + } + else if ( Entity* particle = spawnMagicParticle(my) ) + { + particle->flags[SPRITE] = my->flags[SPRITE]; + particle->flags[INVISIBLE] = my->flags[INVISIBLE]; + particle->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + if ( my->sprite == 2191 || my->sprite == 2209 ) + { + particle->scalex = my->scalex * 0.5; + particle->scaley = my->scaley * 0.5; + particle->scalez = my->scalez * 0.5; + particle->ditheringDisabled = true; + particle->x -= 2.0 * cos(my->yaw); + particle->y -= 2.0 * sin(my->yaw); + particle->lightBonus = vec4_t{ 0.f, 0.f, 0.f, 0.f }; + } + } + break; + } + } + } + } + else + { + if ( Entity* particle = spawnMagicParticle(my) ) + { + particle->flags[SPRITE] = my->flags[SPRITE]; + particle->flags[INVISIBLE] = my->flags[INVISIBLE]; + particle->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + if ( my->sprite == 2191 || my->sprite == 2209 ) + { + particle->scalex = my->scalex * 0.5; + particle->scaley = my->scaley * 0.5; + particle->scalez = my->scalez * 0.5; + particle->ditheringDisabled = true; + particle->x -= 2.0 * cos(my->yaw); + particle->y -= 2.0 * sin(my->yaw); + particle->lightBonus = vec4_t{ 0.f, 0.f, 0.f, 0.f }; + } + } + } +} + +const char* magicLightColorForSprite(Entity* my, int sprite, bool darker) { if ( my && my->flags[SPRITE] ) { + if ( darker ) + { + switch ( sprite ) + { + case 233: + case 234: + case 235: + case 236: + case 237: + case 238: + case 239: + case 240: + case 241: + case 242: + case 243: + case 244: + return "magic_foci_red"; + case 256: + return "magic_foci_blue"; + default: + break; + } + } + else + { + switch ( sprite ) + { + case 233: + case 234: + case 235: + case 236: + case 237: + case 238: + case 239: + case 240: + case 241: + case 242: + case 243: + case 244: + return "magic_foci_red_flicker"; + case 256: + return "magic_foci_blue_flicker"; + default: + break; + } + } return nullptr; } if (darker) { switch (sprite) { case 672: + case 2209: + case 2361: case 168: return "magic_red_flicker"; case 169: return "magic_orange_flicker"; case 670: @@ -42,20 +261,42 @@ static const char* colorForSprite(Entity* my, int sprite, bool darker) { case 171: return "magic_green_flicker"; case 592: case 1244: - case 172: return "magic_blue_flicker"; + case 172: + case 1801: + case 1818: return "magic_blue_flicker"; case 625: + case 2191: + case 2357: case 173: return "magic_purple_flicker"; default: case 669: case 680: + case 2356: case 174: return "magic_white_flicker"; case 593: case 175: return "magic_black_flicker"; - case 678: return "magic_pink_flicker"; + case 678: + case 1817: + case 1886: + case 2355: + case 2364: + return "magic_pink_flicker"; + case 1816: return "magic_green_flicker"; + case 2407: + case 2153: return "magic_foci_yellow_flicker"; + case 2179: + case 2180: + case 2181: + case 2182: + case 2183: + case 2154: return "magic_foci_purple_flicker"; + case 2156: return "magic_foci_brown_flicker"; } } else { switch (sprite) { case 672: + case 2209: + case 2361: case 168: return "magic_red"; case 169: return "magic_orange"; case 670: @@ -64,18 +305,39 @@ static const char* colorForSprite(Entity* my, int sprite, bool darker) { case 171: return "magic_green"; case 592: case 1244: - case 172: return "magic_blue"; + case 172: + case 1801: + case 1818: return "magic_blue"; case 625: + case 2191: + case 2357: case 173: return "magic_purple"; default: case 669: case 680: + case 2356: case 174: return "magic_white"; case 593: case 175: return "magic_black"; - case 678: return "magic_pink"; + case 678: + case 1817: + case 1886: + case 2355: + case 2364: + return "magic_pink"; + case 1816: return "magic_green"; + case 2407: + case 2153: return "magic_foci_yellow"; + case 2179: + case 2180: + case 2181: + case 2182: + case 2183: + case 2154: return "magic_foci_purple"; + case 2156: return "magic_foci_brown"; } } + return nullptr; } void actMagiclightMoving(Entity* my) @@ -104,14 +366,28 @@ void actMagiclightMoving(Entity* my) { if ( Entity* particle = spawnMagicParticle(my) ) { - particle->x = my->x; - particle->y = my->y; - //particle->z = my->z; particle->flags[INVISIBLE] = my->flags[INVISIBLE]; particle->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + if ( my->skill[3] == 1 ) + { + particle->scalex = my->scalex * 0.8; + particle->scaley = my->scaley * 0.8; + particle->scalez = my->scalez * 0.8; + } + else + { + particle->x = my->x; + particle->y = my->y; + //particle->z = my->z; + } } } + if ( my->skill[3] == 1 ) + { + my->roll += 0.3; + } + real_t dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); if ( dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y) ) { @@ -120,19 +396,22 @@ void actMagiclightMoving(Entity* my) return; } - if ( Entity* parent = uidToEntity(my->parent) ) + if ( my->parent != 0 ) { - if ( parent->behavior == &actDaedalusShrine && parent->skill[13] != 0 ) // shrine source + if ( Entity* parent = uidToEntity(my->parent) ) { - if ( Entity* exitEntity = uidToEntity(parent->skill[13]) ) + if ( parent->behavior == &actDaedalusShrine && parent->skill[13] != 0 ) // shrine source { - if ( (int)(my->x / 16) == (int)(exitEntity->x / 16) ) + if ( Entity* exitEntity = uidToEntity(parent->skill[13]) ) { - if ( (int)(my->y / 16) == (int)(exitEntity->y / 16) ) + if ( (int)(my->x / 16) == (int)(exitEntity->x / 16) ) { - my->removeLightField(); - list_RemoveNode(my->mynode); - return; + if ( (int)(my->y / 16) == (int)(exitEntity->y / 16) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } } } } @@ -142,20 +421,25 @@ void actMagiclightMoving(Entity* my) void actMagiclightBall(Entity* my) { - Entity* caster = NULL; if (!my) { return; } my->skill[2] = -10; // so the client sets the behavior of this entity - + const char* light = (my->sprite == 1800 || my->sprite == 1801) ? "magic_shade" : "magic_light"; + const char* light_flicker = (my->sprite == 1800 || my->sprite == 1801) ? "magic_shade_flicker" : "magic_light_flicker"; + auto& lightball_travelled_distance = my->fskill[3]; + /*if ( my->sprite == 174 ) + { + my->ditheringOverride = 4; + }*/ if (clientnum != 0 && multiplayer == CLIENT) { my->removeLightField(); //Light up the area. - my->light = addLight(my->x / 16, my->y / 16, "magic_light"); + my->light = addLight(my->x / 16, my->y / 16, light); if ( flickerLights ) @@ -171,12 +455,12 @@ void actMagiclightBall(Entity* my) if (lightball_lighting == 1) { my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, "magic_light"); + my->light = addLight(my->x / 16, my->y / 16, light); } else { my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, "magic_light_flicker"); + my->light = addLight(my->x / 16, my->y / 16, light_flicker); } lightball_flicker = 0; } @@ -235,7 +519,7 @@ void actMagiclightBall(Entity* my) return; //We need the source spell! } - caster = uidToEntity(spell->caster); + Entity* caster = (spell->caster != 0 ? uidToEntity(spell->caster) : nullptr); if (caster) { Stat* stats = caster->getStats(); @@ -249,7 +533,7 @@ void actMagiclightBall(Entity* my) } } } - else if (spell->caster >= 1) //So no caster...but uidToEntity returns NULL if entity is already dead, right? And if the uid is supposed to point to an entity, but it doesn't...it means the caster has died. + else if ( spell->caster != 0 ) { my->removeLightField(); list_RemoveNode(my->mynode); @@ -285,22 +569,37 @@ void actMagiclightBall(Entity* my) return; } + bool followParent = my->sprite == 174 || my->sprite == 1800 || (my->sprite == 1801 && my->parent != 0) || (my->sprite == 1802 && my->parent != 0); + if (magic_init) { my->removeLightField(); if (lightball_timer <= 0) { - if ( spell->sustain ) + if ( my->sprite == 1801 || my->sprite == 1802 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else if ( spell->sustain ) { //Attempt to sustain the magic light. if (caster) { //Deduct mana from caster. Cancel spell if not enough mana (simply leave sustained at false). - bool deducted = caster->safeConsumeMP(1); //Consume 1 mana every duration / mana seconds - if (deducted) + if ( spell->magicstaff ) + { + lightball_timer = spell->channel_duration; + } + else if ( caster->safeConsumeMP(1) ) { - lightball_timer = spell->channel_duration / getCostOfSpell(spell); + if ( caster->behavior == &actPlayer && !spell->magicstaff ) + { + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(1, spell->skillID); + } + lightball_timer = spell->channel_duration; } else { @@ -336,6 +635,7 @@ void actMagiclightBall(Entity* my) { lightball_hoverangle = 0; } + if (map.tiles[(int)((my->y / 16) * MAPLAYERS + (my->x / 16) * MAPLAYERS * map.height)]) { //Ceiling. @@ -347,178 +647,259 @@ void actMagiclightBall(Entity* my) //my->z = lightball_hover_basez - 4 + ((lightball_hover_basez + LIGHTBALL_HOVER_HIGHPEAK - 4 + lightball_hover_basez + LIGHTBALL_HOVER_LOWPEAK - 4) / 2) * sin(lightball_hoverangle * (12.568f / 360.0f)) * 0.1f; my->z = lightball_hover_basez + ((lightball_hover_basez + LIGHTBALL_HOVER_HIGHPEAK + lightball_hover_basez + LIGHTBALL_HOVER_LOWPEAK) / 2) * sin(lightball_hoverangle * (12.568f / 360.0f)) * 0.1f; } + + my->fskill[2] = std::min(1.0, my->fskill[2] + 0.025); + my->z -= my->fskill[1] - my->fskill[1] * sin(my->fskill[2] * PI / 2); + lightball_hoverangle += 1; //Lightball moving. //messagePlayer(0, "*"); - Entity* parent = uidToEntity(my->parent); - if ( !parent ) + + if ( !followParent ) { - return; + // stay still } - double distance = sqrt(pow(my->x - parent->x, 2) + pow(my->y - parent->y, 2)); - if ( distance > MAGICLIGHT_BALL_FOLLOW_DISTANCE || my->path) + else if ( followParent ) { - lightball_player_lastmove_timer = 0; - if (lightball_movement_timer > 0) + Entity* parent = uidToEntity(my->parent); + if ( !parent ) { - lightball_movement_timer--; + return; } - else + + real_t follow_x = parent->x; + real_t follow_y = parent->y; + if ( spell && (spell->ID == SPELL_LIGHT || spell->ID == SPELL_DEEP_SHADE) && spell->caster == my->parent ) { - //messagePlayer(0, "****Moving."); - double tangent = atan2(parent->y - my->y, parent->x - my->x); - lineTraceTarget(my, my->x, my->y, tangent, 1024, LINETRACE_IGNORE_ENTITIES, false, parent); - if ( !hit.entity || hit.entity == parent ) //Line of sight to caster? - { - if (my->path != NULL) - { - list_FreeAll(my->path); - my->path = NULL; - } - //double tangent = atan2(parent->y - my->y, parent->x - my->x); - my->vel_x = cos(tangent) * ((distance - MAGICLIGHT_BALL_FOLLOW_DISTANCE) / MAGICLIGHTBALL_DIVIDE_CONSTANT); - my->vel_y = sin(tangent) * ((distance - MAGICLIGHT_BALL_FOLLOW_DISTANCE) / MAGICLIGHTBALL_DIVIDE_CONSTANT); - my->x += (my->vel_x < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_x : MAGIC_LIGHTBALL_SPEEDLIMIT; - my->y += (my->vel_y < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_y : MAGIC_LIGHTBALL_SPEEDLIMIT; - //} else if (!map.tiles[(int)(OBSTACLELAYER + (my->y / 16) * MAPLAYERS + (my->x / 16) * MAPLAYERS * map.height)]) { //If not in wall.. - } - else + real_t vel = sqrt(pow(parent->vel_x, 2) + pow(parent->vel_y, 2)); + //if ( abs(vel) > 0.1 ) { - //messagePlayer(0, "******Pathfinding."); - //Caster is not in line of sight. Calculate a move path. - /*if (my->children->first != NULL) { - list_RemoveNode(my->children->first); - my->children->first = NULL; - }*/ - if (!my->path) + real_t dir = parent->yaw;// atan2(parent->vel_y, parent->vel_x); + + // draw line from the leaders direction until we hit a wall or 24 dist + real_t startx = follow_x + 16.0 * cos(dir + PI / 4); + real_t starty = follow_y + 16.0 * sin(dir + PI / 4); + real_t previousx = startx; + real_t previousy = starty; + real_t followDist = spell->ID == SPELL_DEEP_SHADE ? 32.0 : 48.0; + std::map checkedTiles; + real_t furthestDist = 0.0; + for ( int iterations = 0; iterations < 7; ++iterations ) { - //messagePlayer(0, "[Light ball] Generating path."); - list_t* path = generatePath((int)floor(my->x / 16), (int)floor(my->y / 16), - (int)floor(parent->x / 16), (int)floor(parent->y / 16), my, parent, - GeneratePathTypes::GENERATE_PATH_DEFAULT); - if ( path != NULL ) + startx += 8 * cos(dir); + starty += 8 * sin(dir); + if ( sqrt(pow(startx - follow_x, 2) + pow(starty - follow_y, 2)) > followDist ) { - my->path = path; + break; } - else + int mapx = (static_cast(startx) >> 4); + int mapy = (static_cast(starty) >> 4); + if ( !(mapx >= 0 && mapx < map.width && mapy >= 0 && mapy < map.height) ) + { + continue; + } + int index = (mapy)*MAPLAYERS + (mapx)*MAPLAYERS * map.height; + if ( !map.tiles[OBSTACLELAYER + index] ) + { + bool foundObstacle = false; + if ( checkedTiles.find(mapx + mapy * 10000) == checkedTiles.end() ) + { + if ( checkObstacle((mapx << 4) + 8, (mapy << 4) + 8, my, nullptr, true, true, false, false) ) + { + foundObstacle = true; + } + checkedTiles[mapx + mapy * 10000] = foundObstacle; + } + else + { + foundObstacle = checkedTiles[mapx + mapy * 10000]; + } + if ( foundObstacle ) + { + break; + } + + // store the last known good coordinate + real_t dist = sqrt(pow(follow_x - startx, 2) + pow(follow_y - starty, 2)); + if ( dist > furthestDist ) + { + furthestDist = dist; + previousx = startx; + previousy = starty; + } + } + else if ( map.tiles[OBSTACLELAYER + index] ) { - //messagePlayer(0, "[Light ball] Failed to generate path (%s line %d).", __FILE__, __LINE__); + break; } } - if (my->path) + startx = follow_x; // retry straight line + starty = follow_y; + for ( int iterations = 0; iterations < 7; ++iterations ) { - double total_distance = 0; //Calculate the total distance to the player to get the right move speed. - double prevx = my->x; - double prevy = my->y; - if (my->path != NULL) + startx += 8 * cos(dir); + starty += 8 * sin(dir); + if ( sqrt(pow(startx - follow_x, 2) + pow(starty - follow_y, 2)) > followDist ) + { + break; + } + int mapx = (static_cast(startx) >> 4); + int mapy = (static_cast(starty) >> 4); + if ( !(mapx >= 0 && mapx < map.width && mapy >= 0 && mapy < map.height) ) { - for (node = my->path->first; node != NULL; node = node->next) + continue; + } + int index = (mapy)*MAPLAYERS + (mapx)*MAPLAYERS * map.height; + if ( !map.tiles[OBSTACLELAYER + index] ) + { + bool foundObstacle = false; + if ( checkedTiles.find(mapx + mapy * 10000) == checkedTiles.end() ) { - if (node->element) + if ( checkObstacle((mapx << 4) + 8, (mapy << 4) + 8, my, nullptr, true, true, false, false) ) { - auto pathnode = (pathnode_t*)node->element; - //total_distance += sqrt(pow(pathnode->y - prevy, 2) + pow(pathnode->x - prevx, 2) ); - total_distance += sqrt(pow(prevx - pathnode->x, 2) + pow(prevy - pathnode->y, 2) ); - prevx = pathnode->x; - prevy = pathnode->y; + foundObstacle = true; } + checkedTiles[mapx + mapy * 10000] = foundObstacle; + } + else + { + foundObstacle = checkedTiles[mapx + mapy * 10000]; + } + if ( foundObstacle ) + { + break; + } + + // store the last known good coordinate + real_t dist = sqrt(pow(follow_x - startx, 2) + pow(follow_y - starty, 2)); + dist -= 8.0; + + /*Entity* particle = spawnMagicParticle(my); + particle->sprite = 576; + particle->x = startx; + particle->y = starty; + particle->z = 7.5;*/ + + if ( dist > furthestDist ) + { + furthestDist = dist; + previousx = startx; + previousy = starty; } } - else if (my->path) //If the path has been traversed, reset it. + else if ( map.tiles[OBSTACLELAYER + index] ) { - list_FreeAll(my->path); - my->path = NULL; + break; } - total_distance -= MAGICLIGHT_BALL_FOLLOW_DISTANCE; + } + follow_x = previousx; + follow_y = previousy; + + /*Entity* particle = spawnMagicParticle(my); + particle->sprite = 942; + particle->x = follow_x; + particle->y = follow_y; + particle->z = 7.5;*/ + + } + } + double distance = sqrt(pow(my->x - follow_x, 2) + pow(my->y - follow_y, 2)); + if ( distance > MAGICLIGHT_BALL_FOLLOW_DISTANCE || my->path) + { + lightball_player_lastmove_timer = 0; + if (lightball_movement_timer > 0) + { + lightball_movement_timer--; + } + else + { + //messagePlayer(0, "****Moving."); + double tangent = atan2(follow_y - my->y, follow_x - my->x); + //lineTraceTarget(my, my->x, my->y, tangent, 1024, LINETRACE_IGNORE_ENTITIES, false, parent); + if ( true/*!hit.entity || hit.entity == parent*/ ) //Line of sight to caster? + { if (my->path != NULL) { - if (my->path->first != NULL) + list_FreeAll(my->path); + my->path = NULL; + } + + my->vel_x = cos(tangent) * ((distance - MAGICLIGHT_BALL_FOLLOW_DISTANCE) / MAGICLIGHTBALL_DIVIDE_CONSTANT); + my->vel_y = sin(tangent) * ((distance - MAGICLIGHT_BALL_FOLLOW_DISTANCE) / MAGICLIGHTBALL_DIVIDE_CONSTANT); + real_t xMove = (my->vel_x < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_x : MAGIC_LIGHTBALL_SPEEDLIMIT; + real_t yMove = (my->vel_y < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_y : MAGIC_LIGHTBALL_SPEEDLIMIT; + my->x += xMove; + my->y += yMove; + + if ( spell ) + { + if ( caster ) { - auto pathnode = (pathnode_t*)my->path->first->element; - //double distance = sqrt(pow(pathnode->y * 16 + 8 - my->y, 2) + pow(pathnode->x * 16 + 8 - my->x, 2) ); - //double distance = sqrt(pow((my->y) - ((pathnode->y + 8) * 16), 2) + pow((my->x) - ((pathnode->x + 8) * 16), 2)); - double distance = sqrt(pow(((pathnode->y * 16) + 8) - (my->y), 2) + pow(((pathnode->x * 16) + 8) - (my->x), 2)); - if (distance <= 4) - { - list_RemoveNode(my->path->first); - if (!my->path->first) - { - list_FreeAll(my->path); - my->path = NULL; - } - } - else + if ( lightball_travelled_distance >= 0 ) { - double target_tangent = atan2((pathnode->y * 16) + 8 - my->y, (pathnode->x * 16) + 8 - my->x); - if (target_tangent > my->yaw) //TODO: Do this yaw thing for all movement. + real_t dist = sqrt(pow(xMove, 2) + pow(yMove, 2)); + if ( dist > 0.05 ) { - my->yaw = (target_tangent >= my->yaw + MAGIC_LIGHTBALL_TURNSPEED) ? my->yaw + MAGIC_LIGHTBALL_TURNSPEED : target_tangent; - } - else if (target_tangent < my->yaw) - { - my->yaw = (target_tangent <= my->yaw - MAGIC_LIGHTBALL_TURNSPEED) ? my->yaw - MAGIC_LIGHTBALL_TURNSPEED : target_tangent; + lightball_travelled_distance += abs(dist); + if ( lightball_travelled_distance >= 16 * 16.0 ) + { + lightball_travelled_distance = 0.0; + if ( spell->magicstaff ) + { + Uint32 flags = spell_t::SPELL_LEVEL_EVENT_SUSTAIN; + flags |= spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF; + if ( magicOnSpellCastEvent(caster, my, nullptr, spell->ID, + flags, 1) ) + { + lightball_travelled_distance = -1.0; + } + } + else + { + if ( caster->behavior == &actPlayer ) + { + if ( players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(spell->ID, 150.0, 1.0, nullptr) ) + { + //lightball_travelled_distance = -1.0; + } + } + } + } } - my->vel_x = cos(my->yaw) * (total_distance / MAGICLIGHTBALL_DIVIDE_CONSTANT); - my->vel_y = sin(my->yaw) * (total_distance / MAGICLIGHTBALL_DIVIDE_CONSTANT); - my->x += (my->vel_x < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_x : MAGIC_LIGHTBALL_SPEEDLIMIT; - my->y += (my->vel_y < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_y : MAGIC_LIGHTBALL_SPEEDLIMIT; } } - } //else assertion error, hehehe - } - else //Path failed to generate. Fallback on moving straight to the player. - { - //messagePlayer(0, "**************NO PATH WHEN EXPECTED PATH."); - my->vel_x = cos(tangent) * ((distance) / MAGICLIGHTBALL_DIVIDE_CONSTANT); - my->vel_y = sin(tangent) * ((distance) / MAGICLIGHTBALL_DIVIDE_CONSTANT); - my->x += (my->vel_x < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_x : MAGIC_LIGHTBALL_SPEEDLIMIT; - my->y += (my->vel_y < MAGIC_LIGHTBALL_SPEEDLIMIT) ? my->vel_y : MAGIC_LIGHTBALL_SPEEDLIMIT; + } + + if ( map.tiles[(int)(OBSTACLELAYER + static_cast(my->y / 16) * MAPLAYERS + static_cast(my->x / 16) * MAPLAYERS * map.height)] ) //If the ball has come to rest in a wall, move its butt. + { + double tangent = atan2(parent->y - my->y, parent->x - my->x); + my->vel_x = cos(tangent) * ((distance) / MAGICLIGHTBALL_DIVIDE_CONSTANT); + my->vel_y = sin(tangent) * ((distance) / MAGICLIGHTBALL_DIVIDE_CONSTANT); + my->x += my->vel_x; + my->y += my->vel_y; + } } - } /*else { - //In a wall. Get out of it. + } + } + else + { + lightball_movement_timer = 0;// LIGHTBALL_MOVE_DELAY; + if (map.tiles[(int)(OBSTACLELAYER + static_cast(my->y / 16) * MAPLAYERS + static_cast(my->x / 16) * MAPLAYERS * map.height)]) //If the ball has come to rest in a wall, move its butt. + { double tangent = atan2(parent->y - my->y, parent->x - my->x); - my->vel_x = cos(tangent) * ((distance) / 100); - my->vel_y = sin(tangent) * ((distance) / 100); + my->vel_x = cos(tangent) * ((distance) / MAGICLIGHTBALL_DIVIDE_CONSTANT); + my->vel_y = sin(tangent) * ((distance) / MAGICLIGHTBALL_DIVIDE_CONSTANT); my->x += my->vel_x; my->y += my->vel_y; - }*/ - } - } - else - { - lightball_movement_timer = LIGHTBALL_MOVE_DELAY; - /*if (lightball_player_lastmove_timer < LIGHTBALL_CIRCLE_TIME) { - lightball_player_lastmove_timer++; - } else { - //TODO: Orbit the player. Maybe. - my->x = parent->x + (lightball_orbit_length * cos(lightball_orbit_angle)); - my->y = parent->y + (lightball_orbit_length * sin(lightball_orbit_angle)); - - lightball_orbit_angle++; - if (lightball_orbit_angle > 360) { - lightball_orbit_angle = 0; } - }*/ - if (my->path != NULL) - { - list_FreeAll(my->path); - my->path = NULL; - } - if (map.tiles[(int)(OBSTACLELAYER + (my->y / 16) * MAPLAYERS + (my->x / 16) * MAPLAYERS * map.height)]) //If the ball has come to rest in a wall, move its butt. - { - double tangent = atan2(parent->y - my->y, parent->x - my->x); - my->vel_x = cos(tangent) * ((distance) / MAGICLIGHTBALL_DIVIDE_CONSTANT); - my->vel_y = sin(tangent) * ((distance) / MAGICLIGHTBALL_DIVIDE_CONSTANT); - my->x += my->vel_x; - my->y += my->vel_y; } } //Light up the area. - my->light = addLight(my->x / 16, my->y / 16, "magic_light"); + my->light = addLight(my->x / 16, my->y / 16, light); if ( flickerLights ) { @@ -533,12 +914,12 @@ void actMagiclightBall(Entity* my) if (lightball_lighting == 1) { my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, "magic_light"); + my->light = addLight(my->x / 16, my->y / 16, light); } else { my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, "magic_light_flicker"); + my->light = addLight(my->x / 16, my->y / 16, light_flicker); } lightball_flicker = 0; } @@ -550,18 +931,26 @@ void actMagiclightBall(Entity* my) //Init the lightball. That is, shoot out from the player. //Move out from the player. - my->vel_x = cos(my->yaw) * 4; - my->vel_y = sin(my->yaw) * 4; - double dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); - - unsigned int distance = sqrt(pow(my->x - lightball_player_startx, 2) + pow(my->y - lightball_player_starty, 2)); - if (distance > MAGICLIGHT_BALL_FOLLOW_DISTANCE * 2 || dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y)) + if ( followParent ) { + /*my->vel_x = cos(my->yaw) * 4; + my->vel_y = sin(my->yaw) * 4; + double dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); + + unsigned int distance = sqrt(pow(my->x - lightball_player_startx, 2) + pow(my->y - lightball_player_starty, 2)); + if (distance > MAGICLIGHT_BALL_FOLLOW_DISTANCE * 2 || dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y)) + { + }*/ magic_init = 1; - my->sprite = 174; //Go from black ball to white ball. lightball_lighting = 1; lightball_movement_timer = 0; //Start off at 0 so that it moves towards the player as soon as it's created (since it's created farther away from the player). } + else + { + magic_init = 1; + lightball_lighting = 1; + lightball_movement_timer = 0; + } } } @@ -584,7 +973,7 @@ void spawnBloodVialOnMonsterDeath(Entity* entity, Stat* hitstats, Entity* killer if ( tryBloodVial ) { bool spawnBloodVial = false; - if ( hitstats->EFFECTS[EFF_BLEEDING] ) + if ( hitstats->getEffectActive(EFF_BLEEDING) ) { if ( hitstats->EFFECTS_TIMERS[EFF_BLEEDING] >= 250 ) { @@ -648,1974 +1037,2625 @@ void spawnBloodVialOnMonsterDeath(Entity* entity, Stat* hitstats, Entity* killer } } -static ConsoleVariable cvar_magic_fx_use_vismap("/magic_fx_use_vismap", true); -void magicOnEntityHit(Entity* parent, Entity* particle, Entity* hitentity, Stat* hitstats, Sint32 preResistanceDamage, Sint32 damage, Sint32 oldHP, int spellID) +bool magicOnSpellCastEvent(Entity* parent, Entity* projectile, Entity* hitentity, int spellID, Uint32 eventType, int eventValue, bool allowedLevelup) { - if ( !hitentity ) { return; } + if ( !parent ) + { + return false; + } - if ( hitentity->behavior == &actPlayer ) + if ( parent->behavior != &actPlayer ) { - Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_RES_SPELLS_HIT, "res", 1); + return false; + } - if ( hitstats ) + if ( multiplayer == CLIENT ) + { + if ( players[clientnum]->entity && players[clientnum]->entity == parent ) { - Sint32 damageTaken = oldHP - hitstats->HP; - if ( damageTaken > 0 ) + if ( projectile == parent || !projectile ) { - Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_RES_DMG_TAKEN, "res", damageTaken); - Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_HP_MOST_DMG_LOST_ONE_HIT, "hp", damageTaken); - if ( preResistanceDamage > damage ) - { - Sint32 noResistDmgTaken = oldHP - std::max(0, oldHP - preResistanceDamage); - if ( noResistDmgTaken > damageTaken ) - { - Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_RES_DMG_RESISTED, "res", noResistDmgTaken - damageTaken); - Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_RES_DMG_RESISTED_RUN, "res", noResistDmgTaken - damageTaken); - } - } + // request to server + strcpy((char*)net_packet->data, "SPLV"); + net_packet->data[4] = clientnum; + SDLNet_Write16(spellID, &net_packet->data[5]); + SDLNet_Write32(eventType, &net_packet->data[7]); + SDLNet_Write32(eventValue, &net_packet->data[11]); + + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + net_packet->len = 15; + sendPacketSafe(net_sock, -1, net_packet, 0); } } + return false; } - if ( parent && parent->behavior == &actMonster && spellID > SPELL_NONE ) + + if ( spellID <= SPELL_NONE ) { - if ( hitstats ) + return false; + } + + bool magicstaff = eventType & spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF; + bool spellbook = eventType & spell_t::SPELL_LEVEL_EVENT_SPELLBOOK; + + if ( projectile && + (projectile->behavior == &actMagicMissile + || projectile->behavior == &actParticleFloorMagic + || projectile->behavior == &actParticleWave + || projectile->behavior == &actParticleAestheticOrbit + || projectile->behavior == &actParticleTimer) ) + { + magicstaff = projectile->actmagicCastByMagicstaff == 1; + spellbook = projectile->actmagicFromSpellbook == 1; + if ( magicstaff && projectile->actmagicReflectionCount > 0 ) { - Sint32 damageTaken = oldHP - hitstats->HP; - if ( damageTaken > 0 ) + allowedLevelup = false; + } + if ( projectile->actmagicSpray > 0 ) + { + if ( spellID == SPELL_BREATHE_FIRE + || spellID == SPELL_GREASE_SPRAY ) { - if ( Stat* stats = parent->getStats() ) + eventType |= spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE; + } + else if ( spellID == SPELL_FOCI_ARCS + || spellID == SPELL_FOCI_NEEDLES + || spellID == SPELL_FOCI_FIRE + || spellID == SPELL_FOCI_SNOW + || spellID == SPELL_FOCI_SANDBLAST ) + { + magicstaff = true; + } + else + { + allowedLevelup = false; // foci/sprays + } + } + } + + auto spell = getSpellFromID(spellID); + if ( !spell ) { return false; } + + int player = parent->skill[2]; + if ( player < 0 || player >= MAXPLAYERS ) + { + return false; + } + auto findSpellDef = ItemTooltips.spellItems.find(spellID); + if ( findSpellDef == ItemTooltips.spellItems.end() ) + { + return false; + } + auto& spellDef = findSpellDef->second; + int& procsToLevel = players[player]->mechanics.baseSpellLevelUpProcs[spell->ID]; + int highSkillProcsToLevel = std::max(0, stats[player]->getProficiency(spell->skillID) - spell->difficulty) / 5; + + bool skillTooHigh = false; + if ( allowedLevelup ) + { + if ( hitentity ) + { + Stat* hitstats = (hitentity->behavior == &actMonster || hitentity->behavior == &actPlayer) ? hitentity->getStats() : nullptr; + if ( hitstats ) + { + if ( hitstats->getEffectActive(EFF_STASIS) ) { - if ( stats->type == SPELLBOT ) + allowedLevelup = false; + } + else if ( (hitentity->behavior == &actMonster + && (hitentity->monsterAllyGetPlayerLeader() || (hitstats && achievementObserver.checkUidIsFromPlayer(hitstats->leader_uid) >= 0))) + || hitentity->behavior == &actPlayer ) + { + if ( spellDef.spellTags.find(ItemTooltips_t::SPELL_TAG_BUFF) == spellDef.spellTags.end() + && spellDef.spellTags.find(ItemTooltips_t::SPELL_TAG_HEALING) == spellDef.spellTags.end() + && spellDef.spellTags.find(ItemTooltips_t::SPELL_TAG_CURE) == spellDef.spellTags.end() ) { - if ( Entity* leader = parent->monsterAllyGetPlayerLeader() ) + allowedLevelup = false; // no level up on allies + } + } + + if ( allowedLevelup ) + { + if ( parent->isInvisible() ) + { + if ( spellID != SPELL_INVISIBILITY ) { - Compendium_t::Events_t::eventUpdate(leader->skill[2], - Compendium_t::CPDM_SENTRY_DEPLOY_DMG, TOOL_SPELLBOT, damageTaken); + players[player]->mechanics.updateSustainedSpellEvent(SPELL_INVISIBILITY, 10.0, 1.0, hitentity); + } + } + if ( projectile && projectile->behavior == &actMagicMissile && projectile->actmagicOrbitCastFromSpell == 1 ) + { + if ( stats[player]->getEffectActive(EFF_MAGICAMPLIFY) ) + { + players[player]->mechanics.updateSustainedSpellEvent(SPELL_AMPLIFY_MAGIC, 5.0, 1.0, hitentity); } } } } } + + if ( magicstaff ) + { + if ( stats[player]->getProficiency(spell->skillID) >= std::min(SKILL_LEVEL_LEGENDARY, (spell->difficulty + 20)) ) + { + //allowedLevelup = false; + skillTooHigh = true; + } + else + { + /*if ( procsToLevel < 2 + (spell->difficulty / 30) ) + { + allowedLevelup = false; + if ( local_rng.rand() % 4 == 0 ) + { + ++procsToLevel; + } + }*/ + } + } + else + { + if ( stats[player]->getProficiency(spell->skillID) >= std::min(SKILL_LEVEL_LEGENDARY, (spell->difficulty + 20)) ) + { + //allowedLevelup = false; + skillTooHigh = true; + } + } } - else if ( parent && parent->behavior == &actPlayer && spellID > SPELL_NONE ) + + for ( int flag = 0; flag < 32; ++flag ) { - Sint32 damageTaken = 0; - if ( hitstats ) + Uint32 tag = 1 << flag; + if ( tag >= spell_t::SPELL_LEVEL_EVENT_ENUM_END ) { - damageTaken = oldHP - hitstats->HP; + break; } - if ( !particle || (particle && particle->behavior != &actMagicMissile)) { return; } - if ( particle->actmagicCastByMagicstaff != 0 ) + if ( eventType & tag ) { - auto find = ItemTooltips.spellItems.find(spellID); - if ( find != ItemTooltips.spellItems.end() ) + if ( tag != spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF + && tag != spell_t::SPELL_LEVEL_EVENT_SPELLBOOK + && tag != spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE + && tag != spell_t::SPELL_LEVEL_EVENT_ALWAYS ) { - if ( damageTaken > 0 ) + bool found = spellDef.spellLevelTags.find((spell_t::SpellOnCastEventTypes)tag) != spellDef.spellLevelTags.end(); + assert(found); + } + } + } + + bool nothingElseToLearnMsg = false; + bool skillIncreased = false; + + if ( spell->skillID > 0 ) + { + if ( magicstaff ) + { + real_t percentChance = 100.0 / (real_t)((eventType & spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE) ? 12 : 8); + if ( players[player]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_SPELL_LEVELS, + std::max(1, std::min(100, (int)percentChance)), spellID) ) //16.67% + { + if ( allowedLevelup ) { - if ( find->second.magicstaffId >= 0 && find->second.magicstaffId < NUMITEMS && items[find->second.magicstaffId].category == MAGICSTAFF ) + if ( (procsToLevel < ((skillTooHigh ? highSkillProcsToLevel : 0) + 2 + (spell->difficulty / 30))) ) { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, (ItemType)find->second.magicstaffId, damageTaken); + ++procsToLevel; } - } - else if ( damage == 0 && oldHP == 0 ) - { - if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) + else { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, - (ItemType)find->second.magicstaffId, 1); + parent->increaseSkill(spell->skillID); + skillIncreased = true; + procsToLevel = 0; } } + else + { + nothingElseToLearnMsg = true; + } } } - else if ( particle->actmagicFromSpellbook != 0 ) + else { - auto find = ItemTooltips.spellItems.find(spellID); - if ( find != ItemTooltips.spellItems.end() ) + int chance = 4 + (stats[player]->getProficiency(spell->skillID) / 20) * 1; // 4 - 8 + if ( eventType & spell_t::SPELL_LEVEL_EVENT_SHAPESHIFT + || eventType & spell_t::SPELL_LEVEL_EVENT_SUMMON ) { - if ( find->second.spellbookId >= 0 && find->second.spellbookId < NUMITEMS && items[find->second.spellbookId].category == SPELLBOOK ) + chance /= 2; + } + + if ( (eventType & spell_t::SPELL_LEVEL_EVENT_SUSTAIN) ) + { + bool sustainedChance = players[player]->mechanics.sustainedSpellLevelChance(spell->skillID); + int baseSpellChance = players[player]->mechanics.baseSpellLevelChance(spell->skillID); + if ( eventType & spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE ) + { + chance += 8; + } + chance = std::max(2, chance - baseSpellChance); + + real_t percentChance = 100.0 / chance; + if ( sustainedChance + && players[player]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_SPELL_LEVELS, + std::max(1, std::min(100, (int)percentChance)), spellID) ) { - if ( damageTaken > 0 ) + if ( allowedLevelup ) { - if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_HEALING) != find->second.spellTags.end() ) + if ( skillTooHigh && (procsToLevel < highSkillProcsToLevel) ) { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_HEAL, (ItemType)find->second.spellbookId, damageTaken); + ++procsToLevel; } else { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, (ItemType)find->second.spellbookId, damageTaken); + players[player]->mechanics.sustainedSpellClearMP(spell->skillID); + players[player]->mechanics.baseSpellClearMP(spell->skillID); + parent->increaseSkill(spell->skillID); + skillIncreased = true; + procsToLevel = 0; } } - else if ( damage == 0 && oldHP == 0 ) + else { - if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) - { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, - (ItemType)find->second.spellbookId, 1); - } + nothingElseToLearnMsg = true; } } } - } - else if ( particle->actmagicCastByTinkerTrap == 0 ) - { - if ( particle->actmagicIsOrbiting == 2 && particle->actmagicOrbitCastFromSpell == 0 ) + else { - // cast by firestorm potion etc - if ( damageTaken > 0 && parent != hitentity ) + int baseSpellChance = players[player]->mechanics.baseSpellLevelChance(spell->skillID); + if ( eventType & spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE ) { - auto find = ItemTooltips.spellItems.find(spellID); - if ( find != ItemTooltips.spellItems.end() ) - { - if ( find->second.id > SPELL_NONE && find->second.id < NUM_SPELLS ) - { - ItemType type = WOODEN_SHIELD; - if ( find->second.id == SPELL_FIREBALL ) - { - type = POTION_FIRESTORM; - } - else if ( find->second.id == SPELL_COLD ) - { - type = POTION_ICESTORM; - } - else if ( find->second.id == SPELL_LIGHTNING ) - { - type = POTION_THUNDERSTORM; - } - if ( type != WOODEN_SHIELD ) - { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_THROWN_DMG_TOTAL, type, damageTaken); - } - } - } + chance += 8; } - } - else - { - // normal spellcasts - auto find = ItemTooltips.spellItems.find(spellID); - if ( find != ItemTooltips.spellItems.end() ) + chance = std::max(2, chance - baseSpellChance); + if ( eventType & spell_t::SPELL_LEVEL_EVENT_ALWAYS ) { - if ( find->second.id > SPELL_NONE && find->second.id < NUM_SPELLS ) + chance = 1; + } + + real_t percentChance = 100.0 / chance; + if ( players[player]->mechanics.rollRngProc(Player::PlayerMechanics_t::RngRollTypes::RNG_ROLL_SPELL_LEVELS, + std::max(1, std::min(100, (int)percentChance)), spellID) ) + { + if ( allowedLevelup ) { - if ( damageTaken > 0 ) + int mpSpent = players[player]->mechanics.baseSpellMPSpent(spell->skillID); + int threshold = 5 + 5 * (stats[player]->getProficiency(spell->skillID) / 20); + + if ( skillTooHigh && (procsToLevel < highSkillProcsToLevel) ) { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, SPELL_ITEM, damageTaken, false, find->second.id); + ++procsToLevel; + players[player]->mechanics.baseSpellIncrementMP(5 + (spell->difficulty / 20), spell->skillID); } - else if ( damage == 0 && oldHP == 0 ) + else if ( (mpSpent >= threshold) || (eventType & spell_t::SPELL_LEVEL_EVENT_ALWAYS) ) { - if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) - { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, SPELL_ITEM, 1, false, find->second.id); - } + players[player]->mechanics.baseSpellClearMP(spell->skillID); + parent->increaseSkill(spell->skillID); + skillIncreased = true; + procsToLevel = 0; + } + else + { + players[player]->mechanics.baseSpellIncrementMP(5 + (spell->difficulty / 20), spell->skillID); + } + } + else + { + if ( skillTooHigh ) + { + nothingElseToLearnMsg = true; + players[player]->mechanics.baseSpellIncrementMP(5 + (spell->difficulty / 20), spell->skillID); } } } } - } - else if ( particle->actmagicCastByTinkerTrap == 1 ) - { - if ( damageTaken > 0 ) + + /*if ( nothingElseToLearnMsg ) { - if ( spellID == SPELL_FIREBALL ) + if ( local_rng.rand() % 20 == 0 ) { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_BOMB_DMG, TOOL_BOMB, damageTaken); + messagePlayer(player, MESSAGE_HINT, Language::get(2591)); } - else if ( spellID == SPELL_COLD ) + }*/ + + if ( spellbook ) + { + if ( skillIncreased ) { - Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_BOMB_DMG, TOOL_FREEZE_BOMB, damageTaken); + if ( stats[player] && stats[player]->playerRace == RACE_INSECTOID && stats[player]->stat_appearance == 0 ) + { + steamStatisticUpdateClient(player, STEAM_STAT_BOOKWORM, STEAM_STAT_INT, 1); + } } } } } + + return skillIncreased; } -void magicTrapOnHit(Entity* parent, Entity* hitentity, Stat* hitstats, Sint32 oldHP, int spellID) +void magicOnEntityHit(Entity* parent, Entity* particle, Entity* hitentity, Stat* hitstats, Sint32 preResistanceDamage, Sint32 damage, Sint32 oldHP, int spellID, int selfCastUsingItem) { - if ( !parent || !hitentity || !hitstats ) { return; } - if ( spellID == SPELL_NONE ) { return; } - if ( parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling ) + if ( !hitentity ) { return; } + + if ( hitstats ) { - const char* category = parent->behavior == &actMagicTrap ? "magic trap" : "ceiling trap"; - if ( oldHP == 0 ) + Sint32 damageTaken = oldHP - hitstats->HP; + if ( damageTaken > 0 ) { - if ( hitentity->behavior == &actPlayer ) + if ( hitstats->getEffectActive(EFF_DEFY_FLESH) && spellID != SPELL_DEFY_FLESH ) { - Compendium_t::Events_t::eventUpdateWorld(hitentity->skill[2], Compendium_t::CPDM_TRAP_MAGIC_STATUSED, category, 1); + hitentity->defyFleshProc(parent); + } + hitentity->pinpointDamageProc(parent, damageTaken); + if ( hitstats->getEffectActive(EFF_SPORES) ) + { + if ( hitentity->behavior == &actPlayer + && hitstats->type == MYCONID && hitstats->getEffectActive(EFF_GROWTH) >= 4 ) + { + floorMagicCreateSpores(hitentity, hitentity->x, hitentity->y, hitentity, 0, SPELL_SPORES); + } } } - else if ( oldHP > hitstats->HP ) + } + + if ( hitentity->behavior == &actPlayer && spellID > SPELL_NONE ) + { + Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_RES_SPELLS_HIT, "res", 1); + + if ( hitstats ) { - if ( hitentity->behavior == &actPlayer ) + Sint32 damageTaken = oldHP - hitstats->HP; + if ( damageTaken > 0 ) { - Compendium_t::Events_t::eventUpdateWorld(hitentity->skill[2], Compendium_t::CPDM_TRAP_DAMAGE, category, oldHP - hitstats->HP); - if ( hitstats->HP <= 0 ) + Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_RES_DMG_TAKEN, "res", damageTaken); + Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_HP_MOST_DMG_LOST_ONE_HIT, "hp", damageTaken); + if ( preResistanceDamage > damage ) { - Compendium_t::Events_t::eventUpdateWorld(hitentity->skill[2], Compendium_t::CPDM_TRAP_KILLED_BY, category, 1); + Sint32 noResistDmgTaken = oldHP - std::max(0, oldHP - preResistanceDamage); + if ( noResistDmgTaken > damageTaken ) + { + Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_RES_DMG_RESISTED, "res", noResistDmgTaken - damageTaken); + Compendium_t::Events_t::eventUpdateCodex(hitentity->skill[2], Compendium_t::CPDM_RES_DMG_RESISTED_RUN, "res", noResistDmgTaken - damageTaken); + } } } - else if ( hitentity->behavior == &actMonster ) + + if ( damage > 0 ) { - if ( auto leader = hitentity->monsterAllyGetPlayerLeader() ) + if ( hitstats->getEffectActive(EFF_GUARD_SPIRIT) ) { - Compendium_t::Events_t::eventUpdateWorld(hitentity->monsterAllyIndex, Compendium_t::CPDM_TRAP_FOLLOWERS_KILLED, category, 1); + thaumSpellArmorProc(hitentity, *hitstats, false, nullptr, EFF_GUARD_SPIRIT); + } + if ( hitstats->getEffectActive(EFF_DIVINE_GUARD) ) + { + thaumSpellArmorProc(hitentity, *hitstats, false, nullptr, EFF_DIVINE_GUARD); } } } } -} -void actMagicMissile(Entity* my) //TODO: Verify this function. -{ - if (!my || !my->children.first || !my->children.first->element) - { - return; - } - spell_t* spell = (spell_t*)my->children.first->element; - if (!spell) + if ( parent && parent->behavior == &actMonster ) { - return; + int summonSpellID = getSpellFromSummonedEntityForSpellEvent(parent); + if ( summonSpellID != SPELL_NONE ) + { + if ( Entity* leader = parent->monsterAllyGetPlayerLeader() ) + { + magicOnSpellCastEvent(leader, nullptr, hitentity, summonSpellID, spell_t::SPELL_LEVEL_EVENT_ASSIST, 1); + } + } } - //node_t *node = NULL; - spellElement_t* element = NULL; - node_t* node = NULL; - int i = 0; - int c = 0; - Entity* entity = NULL; - double tangent; - Entity* parent = uidToEntity(my->parent); - - if (magic_init) + if ( parent && parent->behavior == &actMonster && spellID > SPELL_NONE ) { - my->removeLightField(); - - if ( multiplayer != CLIENT ) + if ( hitstats ) { - //Handle the missile's life. - MAGIC_LIFE++; - - if (MAGIC_LIFE >= MAGIC_MAXLIFE) + Sint32 damageTaken = oldHP - hitstats->HP; + if ( damageTaken > 0 ) { - my->removeLightField(); - list_RemoveNode(my->mynode); - return; + if ( Stat* stats = parent->getStats() ) + { + if ( stats->type == SPELLBOT ) + { + if ( Entity* leader = parent->monsterAllyGetPlayerLeader() ) + { + Compendium_t::Events_t::eventUpdate(leader->skill[2], + Compendium_t::CPDM_SENTRY_DEPLOY_DMG, TOOL_SPELLBOT, damageTaken); + } + return; + } + } } + } + } + else if ( parent && parent->behavior == &actPlayer && spellID > SPELL_NONE ) + { + Sint32 damageTaken = 0; + if ( hitstats ) + { + damageTaken = oldHP - hitstats->HP; + } - if ( spell->ID == SPELL_CHARM_MONSTER || spell->ID == SPELL_ACID_SPRAY ) + Uint32 additionalFlags = 0; + if ( hitstats && hitstats->HP > 0 ) + { + if ( spellID == SPELL_FIRE_WALL + || spellID == SPELL_DISRUPT_EARTH + || spellID == SPELL_EARTH_SPINES + || spellID == SPELL_ICE_WAVE + || spellID == SPELL_HOLY_FIRE + || spellID == SPELL_SHADOW_TAG + || spellID == SPELL_COMMAND ) { - Entity* caster = uidToEntity(spell->caster); - if ( !caster ) - { - my->removeLightField(); - list_RemoveNode(my->mynode); - return; - } + additionalFlags |= spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE; } - else if ( spell->ID == SPELL_NONE ) + else if ( spellID == SPELL_METEOR || spellID == SPELL_METEOR_SHOWER ) { - my->removeLightField(); - list_RemoveNode(my->mynode); - return; + if ( particle && particle->flags[SPRITE] ) // weak hits + { + additionalFlags |= spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE; + } } + } + if ( spellID == SPELL_FLAMES || spellID == SPELL_BREATHE_FIRE ) + { + additionalFlags |= spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE; + } + if ( spellID == SPELL_DEFY_FLESH && damageTaken > 0 ) + { + additionalFlags |= spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE; + } - node = spell->elements.first; - //element = (spellElement_t *) spell->elements->first->element; - element = (spellElement_t*)node->element; - Sint32 entityHealth = 0; - double dist = 0.f; - bool hitFromAbove = false; - ParticleEmitterHit_t* particleEmitterHitProps = nullptr; - if ( my->actmagicSpray == 1 ) + if ( particle && particle->behavior == &actPlayer ) + { + if ( selfCastUsingItem > 0 && selfCastUsingItem < NUMITEMS ) { - my->vel_z += my->actmagicSprayGravity; - my->z += my->vel_z; - my->roll += 0.1; - dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); //normal flat projectiles - - if ( my->z < 8.0 ) + if ( items[selfCastUsingItem].category == MAGICSTAFF ) { - // if we didn't hit the floor, process normal horizontal movement collision if we aren't too high - if ( dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y) ) + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) { - hitFromAbove = true; - - if ( particleEmitterHitProps = getParticleEmitterHitProps(my->actmagicEmitter, hit.entity) ) + if ( damageTaken > 0 ) { - particleEmitterHitProps->hits++; - particleEmitterHitProps->tick = ticks; + if ( find->second.magicstaffId >= 0 && find->second.magicstaffId < NUMITEMS && items[find->second.magicstaffId].category == MAGICSTAFF ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, (ItemType)find->second.magicstaffId, damageTaken); + } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_DMG | spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF, damageTaken); + } + else if ( damage == 0 && oldHP == 0 ) + { + if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, + (ItemType)find->second.magicstaffId, 1); + } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_EFFECT | spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF, 1); } } } - else + else if ( items[selfCastUsingItem].category == SPELLBOOK ) { - my->removeLightField(); - list_RemoveNode(my->mynode); - return; + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) + { + if ( find->second.spellbookId >= 0 && find->second.spellbookId < NUMITEMS && items[find->second.spellbookId].category == SPELLBOOK ) + { + if ( damageTaken > 0 ) + { + /*if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_HEALING) != find->second.spellTags.end() ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_HEAL, (ItemType)find->second.spellbookId, damageTaken); + } + else*/ + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, (ItemType)find->second.spellbookId, damageTaken); + } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_DMG | spell_t::SPELL_LEVEL_EVENT_SPELLBOOK, damageTaken); + } + else if ( damage == 0 && oldHP == 0 ) + { + if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, + (ItemType)find->second.spellbookId, 1); + } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_EFFECT | spell_t::SPELL_LEVEL_EVENT_SPELLBOOK, 1); + } + } + } } } - else if ( (parent && parent->behavior == &actMagicTrapCeiling) || my->actmagicIsVertical == MAGIC_ISVERTICAL_Z ) + else { - // moving vertically. - my->z += my->vel_z; - hitFromAbove = my->magicFallingCollision(); - if ( !hitFromAbove ) + // normal spellcasts + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) { - // nothing hit yet, let's keep trying... + if ( find->second.id > SPELL_NONE && find->second.id < NUM_SPELLS ) + { + if ( damageTaken > 0 ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, SPELL_ITEM, damageTaken, false, find->second.id); + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_DMG, damageTaken); + } + else if ( damage == 0 && oldHP == 0 ) + { + if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, SPELL_ITEM, 1, false, find->second.id); + } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_EFFECT, 1); + } + } } } - else if ( my->actmagicIsOrbiting != 0 ) + } + if ( !particle + || (particle + && (particle->behavior != &actMagicMissile + && particle->behavior != &actParticleFloorMagic + && particle->behavior != &actParticleWave + && particle->behavior != &actParticleAestheticOrbit + && particle->behavior != &actParticleTimer))) { return; } + if ( particle->actmagicCastByMagicstaff == 1 ) + { + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) { - int turnRate = 4; - if ( parent && my->actmagicIsOrbiting == 1 ) - { - my->yaw += 0.1; - my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); - my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); - } - else if ( my->actmagicIsOrbiting == 2 ) - { - my->yaw += 0.2; - turnRate = 4; - my->x = my->actmagicOrbitStationaryX + my->actmagicOrbitStationaryCurrentDist * cos(my->yaw); - my->y = my->actmagicOrbitStationaryY + my->actmagicOrbitStationaryCurrentDist * sin(my->yaw); - my->actmagicOrbitStationaryCurrentDist = - std::min(my->actmagicOrbitStationaryCurrentDist + 0.5, static_cast(my->actmagicOrbitDist)); - } - hitFromAbove = my->magicOrbitingCollision(); - my->z += my->vel_z * my->actmagicOrbitVerticalDirection; - - if ( my->actmagicIsOrbiting == 2 ) - { - // we don't change direction, upwards we go! - // target speed is actmagicOrbitVerticalSpeed. - my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); - my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; - my->roll = std::max(my->roll, -PI / 4); - } - else if ( my->z > my->actmagicOrbitStartZ ) + if ( damageTaken > 0 ) { - if ( my->actmagicOrbitVerticalDirection == 1 ) - { - my->vel_z = std::max(0.01, my->vel_z * 0.95); - my->roll -= (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; - } - else + if ( find->second.magicstaffId >= 0 && find->second.magicstaffId < NUMITEMS && items[find->second.magicstaffId].category == MAGICSTAFF ) { - my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); - my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, (ItemType)find->second.magicstaffId, damageTaken); } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_DMG | spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF, damageTaken); } - else + else if ( damage == 0 && oldHP == 0 ) { - if ( my->actmagicOrbitVerticalDirection == 1 ) - { - my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); - my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; - } - else + if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) { - my->vel_z = std::max(0.01, my->vel_z * 0.95); - my->roll -= (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, + (ItemType)find->second.magicstaffId, 1); } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_EFFECT | spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF, 1); } - - if ( my->actmagicIsOrbiting == 1 ) + } + } + else if ( particle->actmagicSpray == 2 && spellID != SPELL_BREATHE_FIRE ) + { + // foci items + auto find = ItemTooltips.spellItems.find(spellID); + if ( find->second.fociId >= 0 && find->second.fociId < NUMITEMS && itemTypeIsFoci((ItemType)find->second.fociId) ) + { + if ( damageTaken > 0 ) { - if ( (my->z > my->actmagicOrbitStartZ + 4) && my->actmagicOrbitVerticalDirection == 1 ) - { - my->actmagicOrbitVerticalDirection = -1; - } - else if ( (my->z < my->actmagicOrbitStartZ - 4) && my->actmagicOrbitVerticalDirection != 1 ) - { - my->actmagicOrbitVerticalDirection = 1; - } + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, (ItemType)find->second.fociId, damageTaken); + magicOnSpellCastEvent(parent, particle, hitentity, spellID, + additionalFlags + | spell_t::SPELL_LEVEL_EVENT_DMG + | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE + | spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF, 1); } } - else + } + else if ( particle->actmagicFromSpellbook != 0 ) + { + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) { - if ( my->actmagicIsVertical == MAGIC_ISVERTICAL_XYZ ) + if ( find->second.spellbookId >= 0 && find->second.spellbookId < NUMITEMS && items[find->second.spellbookId].category == SPELLBOOK ) { - // moving vertically and horizontally, check if we hit the floor - my->z += my->vel_z; - hitFromAbove = my->magicFallingCollision(); - dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); - if ( !hitFromAbove && my->z > -5 ) - { - // if we didn't hit the floor, process normal horizontal movement collision if we aren't too high - if ( dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y) ) - { - hitFromAbove = true; - } - } - if ( my->actmagicProjectileArc > 0 ) + if ( damageTaken > 0 && spellID != SPELL_GREASE_SPRAY ) { - real_t z = -1 - my->z; - if ( z > 0 ) - { - my->pitch = -atan(z * 0.1 / sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y)); - } - else + /*if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_HEALING) != find->second.spellTags.end() ) { - my->pitch = -atan(z * 0.15 / sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y)); + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_HEAL, (ItemType)find->second.spellbookId, damageTaken); } - if ( my->actmagicProjectileArc == 1 ) + else*/ { - //messagePlayer(0, "z: %f vel: %f", my->z, my->vel_z); - my->vel_z = my->vel_z * 0.9; - if ( my->vel_z > -0.1 ) - { - //messagePlayer(0, "arc down"); - my->actmagicProjectileArc = 2; - my->vel_z = 0.01; - } + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, (ItemType)find->second.spellbookId, damageTaken); } - else if ( my->actmagicProjectileArc == 2 ) + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_DMG | spell_t::SPELL_LEVEL_EVENT_SPELLBOOK, damageTaken); + } + else if ( (damage == 0 && oldHP == 0) || spellID == SPELL_GREASE_SPRAY ) + { + if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) { - //messagePlayer(0, "z: %f vel: %f", my->z, my->vel_z); - my->vel_z = std::min(0.8, my->vel_z * 1.2); + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, + (ItemType)find->second.spellbookId, 1); } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_EFFECT | spell_t::SPELL_LEVEL_EVENT_SPELLBOOK, 1); } } - else - { - dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); //normal flat projectiles - } } - - if ( hitFromAbove - || (my->actmagicIsVertical != MAGIC_ISVERTICAL_XYZ && my->actmagicSpray != 1 && dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y)) ) - { - node = element->elements.first; - //element = (spellElement_t *) element->elements->first->element; - element = (spellElement_t*)node->element; - //if (hit.entity != NULL) { - bool mimic = hit.entity && hit.entity->isInertMimic(); - Stat* hitstats = nullptr; - int player = -1; - - // count reflection - int reflection = 0; - if ( hit.entity ) - { - hitstats = hit.entity->getStats(); - if ( hit.entity->behavior == &actPlayer ) - { - player = hit.entity->skill[2]; - } - } - if ( hitstats ) + } + else if ( particle->actmagicCastByTinkerTrap == 0 ) + { + if ( particle->actmagicIsOrbiting == 2 && particle->actmagicOrbitCastFromSpell == 0 ) + { + // cast by firestorm potion etc + if ( damageTaken > 0 && parent != hitentity ) { - if ( parent && - ((hit.entity->getRace() == LICH_ICE && parent->getRace() == LICH_FIRE) - || ((hit.entity->getRace() == LICH_FIRE || hitstats->leader_uid == parent->getUID()) && parent->getRace() == LICH_ICE) - || (parent->getRace() == LICH_ICE) && !strncmp(hitstats->name, "corrupted automaton", 19) - ) - ) - { - reflection = 3; - } - if ( !reflection ) - { - reflection = hit.entity->getReflection(); - } - if ( my->actmagicCastByTinkerTrap == 1 ) - { - reflection = 0; - } - if ( reflection == 3 && hitstats->shield && hitstats->shield->type == MIRROR_SHIELD && hitstats->defending ) + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) { - if ( my->actmagicIsVertical == MAGIC_ISVERTICAL_Z ) - { - reflection = 0; - } - // calculate facing angle to projectile, need to be facing projectile to reflect. - else if ( player >= 0 && players[player] && players[player]->entity ) + if ( find->second.id > SPELL_NONE && find->second.id < NUM_SPELLS ) { - real_t yawDiff = my->yawDifferenceFromEntity(players[player]->entity); - if ( yawDiff < (6 * PI / 5) ) + ItemType type = WOODEN_SHIELD; + if ( find->second.id == SPELL_FIREBALL ) { - reflection = 0; + type = POTION_FIRESTORM; } - else + else if ( find->second.id == SPELL_COLD ) { - reflection = 3; - if ( parent && (parent->behavior == &actMonster || parent->behavior == &actPlayer) ) - { - my->actmagicMirrorReflected = 1; - my->actmagicMirrorReflectedCaster = parent->getUID(); - } - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SHIELD_REFLECT, hitstats->shield->type, 1); + type = POTION_ICESTORM; } - } - } - } - - bool yourSpellHitsTheMonster = false; - bool youAreHitByASpell = false; - if ( hit.entity ) - { - if ( hit.entity->behavior == &actPlayer ) - { - bool skipMessage = false; - if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) && my->actmagicTinkerTrapFriendlyFire == 0 ) - { - if ( parent && (parent->behavior == &actMonster || parent->behavior == &actPlayer) && parent->checkFriend(hit.entity) ) + else if ( find->second.id == SPELL_LIGHTNING ) { - skipMessage = true; + type = POTION_THUNDERSTORM; } - } - - if ( my->actmagicCastByTinkerTrap == 1 ) - { - skipMessage = true; - } - if ( !skipMessage ) - { - Uint32 color = makeColorRGB(255, 0, 0); - if ( reflection == 0 ) + if ( type != WOODEN_SHIELD ) { - if ( particleEmitterHitProps ) - { - if ( particleEmitterHitProps->hits == 1 ) - { - if ( my->actmagicSpray == 1 ) - { - //messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(6238)); - //youAreHitByASpell = true; - } - else - { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(376)); - youAreHitByASpell = true; - } - } - } - else - { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(376)); - youAreHitByASpell = true; - } + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_THROWN_DMG_TOTAL, type, damageTaken); } } - if ( hitstats ) - { - entityHealth = hitstats->HP; - } } - if ( parent && hitstats ) + } + } + else + { + // normal spellcasts + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) + { + if ( find->second.id > SPELL_NONE && find->second.id < NUM_SPELLS ) { - if ( parent->behavior == &actPlayer ) + if ( damageTaken > 0 && spellID != SPELL_GREASE_SPRAY ) { - Uint32 color = makeColorRGB(0, 255, 0); - if ( strcmp(element->element_internal_name, spellElement_charmMonster.element_internal_name) ) + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_DMG, SPELL_ITEM, damageTaken, false, find->second.id); + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_DMG, damageTaken); + } + else if ( (damage == 0 && oldHP == 0) || spellID == SPELL_GREASE_SPRAY ) + { + if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_TRACK_HITS) != find->second.spellTags.end() ) { - if ( my->actmagicCastByTinkerTrap == 1 ) - { - //messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(3498), Language::get(3499), MSG_COMBAT); - } - else - { - if ( reflection == 0 ) - { - yourSpellHitsTheMonster = true; - if ( !hit.entity->isInertMimic() ) - { - if ( ItemTooltips.bSpellHasBasicHitMessage(spell->ID) ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(378), Language::get(377), MSG_COMBAT_BASIC); - } - else - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(378), Language::get(377), MSG_COMBAT); - } - } - } - } + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_TARGETS, SPELL_ITEM, 1, false, find->second.id); } + magicOnSpellCastEvent(parent, particle, hitentity, spellID, additionalFlags | spell_t::SPELL_LEVEL_EVENT_EFFECT, 1); } } } - - // Only degrade the equipment if Friendly Fire is ON or if it is (OFF && target is an enemy) - bool bShouldEquipmentDegrade = false; - if ( parent && parent->behavior == &actDeathGhost ) + } + } + else if ( particle->actmagicCastByTinkerTrap == 1 ) + { + if ( damageTaken > 0 ) + { + if ( spellID == SPELL_FIREBALL ) { - bShouldEquipmentDegrade = false; + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_BOMB_DMG, TOOL_BOMB, damageTaken); } - else if ( (svFlags & SV_FLAG_FRIENDLYFIRE) ) + else if ( spellID == SPELL_COLD ) { - // Friendly Fire is ON, equipment should always degrade, as hit will register - bShouldEquipmentDegrade = true; + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_BOMB_DMG, TOOL_FREEZE_BOMB, damageTaken); } - else + } + } + } +} + +void magicTrapOnHit(Entity* parent, Entity* hitentity, Stat* hitstats, Sint32 oldHP, int spellID) +{ + if ( !parent || !hitentity || !hitstats ) { return; } + if ( spellID == SPELL_NONE ) { return; } + if ( parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling ) + { + const char* category = parent->behavior == &actMagicTrap ? "magic trap" : "ceiling trap"; + if ( oldHP == 0 ) + { + if ( hitentity->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateWorld(hitentity->skill[2], Compendium_t::CPDM_TRAP_MAGIC_STATUSED, category, 1); + } + } + else if ( oldHP > hitstats->HP ) + { + if ( hitentity->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateWorld(hitentity->skill[2], Compendium_t::CPDM_TRAP_DAMAGE, category, oldHP - hitstats->HP); + if ( hitstats->HP <= 0 ) { - // Friendly Fire is OFF, is the target an enemy? - if ( parent != nullptr && (parent->checkFriend(hit.entity)) == false ) + Compendium_t::Events_t::eventUpdateWorld(hitentity->skill[2], Compendium_t::CPDM_TRAP_KILLED_BY, category, 1); + } + } + else if ( hitentity->behavior == &actMonster ) + { + if ( auto leader = hitentity->monsterAllyGetPlayerLeader() ) + { + if ( hitstats->HP <= 0 ) { - // Target is an enemy, equipment should degrade - bShouldEquipmentDegrade = true; + Compendium_t::Events_t::eventUpdateWorld(hitentity->monsterAllyIndex, Compendium_t::CPDM_TRAP_FOLLOWERS_KILLED, category, 1); } } + } + } + } +} - // Handling reflecting the missile - if ( reflection ) +bool absorbMagicEvent(Entity* entity, Entity* parent, Entity& damageSourceProjectile, int spellID, real_t* result, real_t& damageMultiplier, DamageGib& dmgGib) +{ + if ( !entity || !parent ) { return false; } + if ( Stat* parentStats = parent->getStats() ) + { + if ( parentStats->getEffectActive(EFF_ABSORB_MAGIC) > 1 && spellID > SPELL_NONE ) + { + if ( entity->behavior == &actMonster && parent && !entity->monsterAllyGetPlayerLeader() ) + { + if ( auto spell = getSpellFromID(spellID) ) { - spell_t* spellIsReflectingMagic = nullptr; - if ( hit.entity ) - { - spellIsReflectingMagic = hit.entity->getActiveMagicEffect(SPELL_REFLECT_MAGIC); - playSoundEntity(hit.entity, 166, 128); - if ( hit.entity->behavior == &actPlayer ) - { - if ( youAreHitByASpell ) - { - if ( !spellIsReflectingMagic ) - { - messagePlayer(player, MESSAGE_COMBAT, Language::get(379)); // but it bounces off! - } - else - { - messagePlayer(player, MESSAGE_COMBAT, Language::get(2475)); // but it bounces off! - } - } - else - { - messagePlayer(player, MESSAGE_COMBAT, Language::get(4325)); // you reflected a spell! - } - } - } - if ( parent && hitstats ) + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) { - if ( parent->behavior == &actPlayer ) + if ( find->second.spellTags.find(ItemTooltips_t::SPELL_TAG_DAMAGE) != find->second.spellTags.end() ) { - if ( yourSpellHitsTheMonster ) + real_t bonus = (parentStats->getEffectActive(EFF_ABSORB_MAGIC) - 1) / 100.0; + bonus = std::min(getSpellDamageSecondaryFromID(SPELL_ABSORB_MAGIC, parent, nullptr, parent) / 100.0, bonus); + if ( result ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(379)); // but it bounces off! + *result = bonus; } - else + damageMultiplier += bonus; + dmgGib = DMG_STRONGEST; + parent->setEffect(EFF_ABSORB_MAGIC, false, 0, false); + if ( parent->behavior == &actPlayer ) { - messagePlayerMonsterEvent(parent->skill[2], makeColorRGB(255, 255, 255), - *hitstats, Language::get(4322), Language::get(4323), MSG_COMBAT); // your spell bounces off the monster! + messagePlayerColor(parent->skill[2], MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6858)); + magicOnSpellCastEvent(parent, &damageSourceProjectile, entity, SPELL_ABSORB_MAGIC, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); } + return true; } } - if ( hit.side == HORIZONTAL ) - { - my->vel_x *= -1; - my->yaw = atan2(my->vel_y, my->vel_x); - } - else if ( hit.side == VERTICAL ) - { - my->vel_y *= -1; - my->yaw = atan2(my->vel_y, my->vel_x); - } - else if ( hit.side == 0 ) - { - my->vel_x *= -1; - my->vel_y *= -1; - my->yaw = atan2(my->vel_y, my->vel_x); - } - if ( hit.entity ) - { - if ( (parent && parent->behavior == &actMagicTrapCeiling) || my->actmagicIsVertical == MAGIC_ISVERTICAL_Z ) - { - // this missile came from the ceiling, let's redirect it.. - my->x = hit.entity->x + cos(hit.entity->yaw); - my->y = hit.entity->y + sin(hit.entity->yaw); - my->yaw = hit.entity->yaw; - my->z = -1; - my->vel_x = 4 * cos(hit.entity->yaw); - my->vel_y = 4 * sin(hit.entity->yaw); - my->vel_z = 0; - my->pitch = 0; - } - my->parent = hit.entity->getUID(); - ++my->actmagicReflectionCount; - } + } + } + } + } + return false; +} - if ( bShouldEquipmentDegrade ) +Sint32 convertResistancePointsToMagicValue(Sint32 value, int resistance) +{ + if ( resistance > 0 ) + { + real_t mult = 1.0; + for ( int i = 0; i < resistance; ++i, mult *= (1 - Entity::magicResistancePerPoint) ) {} + + return std::max(1.0, value * (mult)); + } + else + { + return value; + } +} + +void magicSetResistance(Entity* entity, Entity* parent, int& resistance, real_t& damageMultiplier, DamageGib& dmgGib, int& trapResist, int spellID) +{ + if ( entity ) + { + resistance = Entity::getMagicResistance(entity->getStats()); + if ( (entity->behavior == &actMonster || entity->behavior == &actPlayer) && entity->getStats() ) + { + damageMultiplier = Entity::getDamageTableMultiplier(entity, *entity->getStats(), DAMAGE_TABLE_MAGIC, &resistance); + + if ( damageMultiplier <= 0.75 ) + { + dmgGib = DMG_WEAKEST; + } + else if ( damageMultiplier <= 0.85 ) + { + dmgGib = DMG_WEAKER; + } + else if ( damageMultiplier >= 1.25 ) + { + dmgGib = resistance == 0 ? DMG_STRONGEST : DMG_WEAKER; + } + else if ( damageMultiplier >= 1.15 ) + { + dmgGib = resistance == 0 ? DMG_STRONGER : DMG_WEAKER; + } + else if ( resistance > 0 ) + { + dmgGib = DMG_WEAKEST; + } + } + + if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + { + trapResist = entity ? entity->getEntityBonusTrapResist() : 0; + if ( trapResist != 0 ) + { + damageMultiplier += -(trapResist / 100.0); + damageMultiplier = std::max(0.0, damageMultiplier); + } + } + } +} + +std::map lastMagicSoundPlayed; + +void actMagicMissile(Entity* my) //TODO: Verify this function. +{ + if (!my || !my->children.first || !my->children.first->element) + { + return; + } + spell_t* spell = (spell_t*)my->children.first->element; + if (!spell) + { + return; + } + //node_t *node = NULL; + spellElement_t* element = NULL; + node_t* node = NULL; + double tangent; + + Entity* parent = uidToEntity(my->parent); + + if ( !magic_init ) + { + //Any init stuff that needs to happen goes here. + magic_init = 1; + my->skill[2] = -7; // ordinarily the client won't do anything with this entity + if ( my->actmagicIsOrbiting == 1 || my->actmagicIsOrbiting == 2 ) + { + MAGIC_MAXLIFE = my->actmagicOrbitLifetime; + } + else if ( my->actmagicIsVertical != MAGIC_ISVERTICAL_NONE ) + { + MAGIC_MAXLIFE = 512; + } + } + + if (magic_init) + { + my->removeLightField(); + + if ( multiplayer != CLIENT ) + { + //Handle the missile's life. + if ( my->actmagicDelayMove > 0 ) + { + --my->actmagicDelayMove; + my->flags[INVISIBLE] = true; + if ( my->actmagicDelayMove == 0 ) + { + my->flags[INVISIBLE] = false; + my->flags[UPDATENEEDED] = true; + my->vel_x = my->actmagicVelXStore; + my->vel_y = my->actmagicVelYStore; + my->vel_z = my->actmagicVelZStore; + + if ( spell->ID == SPELL_METEOR || spell->ID == SPELL_METEOR_SHOWER ) { - // Reflection of 3 does not degrade equipment - bool chance = false; - if ( my->actmagicSpray == 1 ) - { - chance = local_rng.rand() % 10 == 0; - } - else - { - chance = local_rng.rand() % 2 == 0; - } - if ( chance && hitstats && reflection < 3 ) + if ( node_t* node = spell->elements.first ) { - // set armornum to the relevant equipment slot to send to clients - int armornum = 5 + reflection; - if ( (player >= 0 && players[player]->isLocalPlayer()) || player < 0 ) - { - if ( reflection == 1 ) - { - if ( hitstats->cloak ) - { - if ( hitstats->cloak->count > 1 ) - { - newItem(hitstats->cloak->type, hitstats->cloak->status, hitstats->cloak->beatitude, hitstats->cloak->count - 1, hitstats->cloak->appearance, hitstats->cloak->identified, &hitstats->inventory); - } - } - } - else if ( reflection == 2 ) - { - if ( hitstats->amulet ) - { - if ( hitstats->amulet->count > 1 ) - { - newItem(hitstats->amulet->type, hitstats->amulet->status, hitstats->amulet->beatitude, hitstats->amulet->count - 1, hitstats->amulet->appearance, hitstats->amulet->identified, &hitstats->inventory); - } - } - } - else if ( reflection == -1 ) - { - if ( hitstats->shield ) - { - if ( hitstats->shield->count > 1 ) - { - newItem(hitstats->shield->type, hitstats->shield->status, hitstats->shield->beatitude, hitstats->shield->count - 1, hitstats->shield->appearance, hitstats->shield->identified, &hitstats->inventory); - } - } - } - } - if ( reflection == 1 ) - { - if ( hitstats->cloak ) - { - hitstats->cloak->count = 1; - hitstats->cloak->status = static_cast(std::max(static_cast(BROKEN), hitstats->cloak->status - 1)); - if ( hitstats->cloak->status != BROKEN ) - { - messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(380)); - } - else - { - messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(381)); - playSoundEntity(hit.entity, 76, 64); - if ( player >= 0 ) - { - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, hitstats->cloak->type, 1); - } - } - } - } - else if ( reflection == 2 ) - { - if ( hitstats->amulet ) - { - hitstats->amulet->count = 1; - hitstats->amulet->status = static_cast(std::max(static_cast(BROKEN), hitstats->amulet->status - 1)); - if ( hitstats->amulet->status != BROKEN ) - { - messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(382)); - } - else - { - messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(383)); - playSoundEntity(hit.entity, 76, 64); - if ( player >= 0 ) - { - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, hitstats->amulet->type, 1); - } - } - } - } - else if ( reflection == -1 ) + spellElement_t* element = (spellElement_t*)node->element; + if ( node_t* node2 = element->elements.first ) { - if ( hitstats->shield ) + if ( element = (spellElement_t*)node2->element ) { - hitstats->shield->count = 1; - hitstats->shield->status = static_cast(std::max(static_cast(BROKEN), hitstats->shield->status - 1)); - if ( hitstats->shield->status != BROKEN ) + if ( !strcmp(element->element_internal_name, "spell_element_flames") ) { - messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(384)); + playSoundEntity(my, 814, 128); } - else + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR_SHOWER].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR].element_internal_name) ) { - messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(385)); - playSoundEntity(hit.entity, 76, 64); - if ( player >= 0 ) - { - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, hitstats->shield->type, 1); - } + playSoundEntity(my, 164, 128); } } } - if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) - { - strcpy((char*)net_packet->data, "ARMR"); - net_packet->data[4] = armornum; - if ( reflection == 1 ) - { - net_packet->data[5] = hitstats->cloak->status; - } - else if ( reflection == 2 ) - { - net_packet->data[5] = hitstats->amulet->status; - } - else - { - net_packet->data[5] = hitstats->shield->status; - } - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); - } } } + } + } + + if ( my->actmagicDelayMove == 0 ) + { + MAGIC_LIFE++; + } - if ( spellIsReflectingMagic ) - { - int spellCost = getCostOfSpell(spell); - bool unsustain = false; - if ( spellCost >= hit.entity->getMP() ) //Unsustain the spell if expended all mana. - { - unsustain = true; - } - - hit.entity->drainMP(spellCost); - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z / 2, 174); - playSoundEntity(hit.entity, 166, 128); //TODO: Custom sound effect? + if (MAGIC_LIFE >= MAGIC_MAXLIFE) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } - if ( unsustain ) - { - spellIsReflectingMagic->sustain = false; - if ( hitstats ) - { - hit.entity->setEffect(EFF_MAGICREFLECT, false, 0, true); - messagePlayer(player, MESSAGE_STATUS, Language::get(2476)); - } - } - } + if ( spell->ID == SPELL_CHARM_MONSTER || spell->ID == SPELL_ACID_SPRAY ) + { + Entity* caster = uidToEntity(spell->caster); + if ( !caster ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + else if ( spell->ID == SPELL_NONE ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } - if ( my->actmagicReflectionCount >= 3 ) + node = spell->elements.first; + //element = (spellElement_t *) spell->elements->first->element; + element = (spellElement_t*)node->element; + Sint32 entityHealth = 0; + double dist = 0.f; + bool hitFromAbove = false; + bool hitFromSide = false; + ParticleEmitterHit_t* particleEmitterHitProps = nullptr; + if ( my->actmagicDelayMove > 0 ) + { + // no movement or collision + } + else if ( my->actmagicSpray == 2 ) + { + // foci auto-hit particle + if ( Entity* autohitTarget = uidToEntity(my->actmagicOrbitHitTargetUID4) ) + { + hit.entity = autohitTarget; + hitFromSide = true; + } + } + else if ( my->actmagicSpray == 1 ) + { + my->vel_z += my->actmagicSprayGravity; + my->z += my->vel_z; + my->roll += 0.1; + dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); //normal flat projectiles + + if ( my->z < 8.0 ) + { + // if we didn't hit the floor, process normal horizontal movement collision if we aren't too high + if ( dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y) ) { - my->removeLightField(); - list_RemoveNode(my->mynode); - return; + hitFromAbove = true; + + if ( particleEmitterHitProps = getParticleEmitterHitProps(my->actmagicEmitter, hit.entity) ) + { + particleEmitterHitProps->hits++; + particleEmitterHitProps->tick = ticks; + } } + } + else + { + my->removeLightField(); + list_RemoveNode(my->mynode); return; } + } + else if ( (parent && parent->behavior == &actMagicTrapCeiling) || my->actmagicIsVertical == MAGIC_ISVERTICAL_Z ) + { + // moving vertically. + my->z += my->vel_z; + hitFromAbove = my->magicFallingCollision(); + if ( !hitFromAbove ) + { + // nothing hit yet, let's keep trying... + } + } + else if ( my->actmagicIsOrbiting != 0 ) + { + int turnRate = 4; + if ( parent && my->actmagicIsOrbiting == 1 ) + { + my->yaw += 0.1; + my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); + my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); + } + else if ( my->actmagicIsOrbiting == 2 ) + { + my->yaw += 0.2; + turnRate = 4; + my->x = my->actmagicOrbitStationaryX + my->actmagicOrbitStationaryCurrentDist * cos(my->yaw); + my->y = my->actmagicOrbitStationaryY + my->actmagicOrbitStationaryCurrentDist * sin(my->yaw); + my->actmagicOrbitStationaryCurrentDist = + std::min(my->actmagicOrbitStationaryCurrentDist + 0.5, static_cast(my->actmagicOrbitDist)); + } + hitFromAbove = my->magicOrbitingCollision(); + my->z += my->vel_z * my->actmagicOrbitVerticalDirection; - // Test for Friendly Fire, if Friendly Fire is OFF, delete the missile - if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + if ( my->actmagicIsOrbiting == 2 ) { - if ( !strcmp(element->element_internal_name, spellElement_telePull.element_internal_name) - || !strcmp(element->element_internal_name, spellElement_shadowTag.element_internal_name) - || my->actmagicTinkerTrapFriendlyFire == 1 ) + // we don't change direction, upwards we go! + // target speed is actmagicOrbitVerticalSpeed. + my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); + my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; + my->roll = std::max(my->roll, -PI / 4); + } + else if ( my->z > my->actmagicOrbitStartZ ) + { + if ( my->actmagicOrbitVerticalDirection == 1 ) { - // these spells can hit allies no penalty. + my->vel_z = std::max(0.01, my->vel_z * 0.95); + my->roll -= (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; } - else if ( parent && parent->checkFriend(hit.entity) ) + else { - my->removeLightField(); - list_RemoveNode(my->mynode); - return; + my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); + my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; } } - - int trapResist = 0; - if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + else { - trapResist = hit.entity ? hit.entity->getEntityBonusTrapResist() : 0; + if ( my->actmagicOrbitVerticalDirection == 1 ) + { + my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); + my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; + } + else + { + my->vel_z = std::max(0.01, my->vel_z * 0.95); + my->roll -= (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; + } } - - bool alertTarget = false; - bool alertAllies = false; - - // Alerting the hit Entity - if ( hit.entity ) + + if ( my->actmagicIsOrbiting == 1 ) { - // alert the hit entity if it was a monster - if ( hit.entity->behavior == &actMonster && parent != nullptr && parent->behavior != &actDeathGhost ) + if ( (my->z > my->actmagicOrbitStartZ + 4) && my->actmagicOrbitVerticalDirection == 1 ) { - if ( parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling ) + my->actmagicOrbitVerticalDirection = -1; + } + else if ( (my->z < my->actmagicOrbitStartZ - 4) && my->actmagicOrbitVerticalDirection != 1 ) + { + my->actmagicOrbitVerticalDirection = 1; + } + } + } + else + { + if ( my->actmagicIsVertical == MAGIC_ISVERTICAL_XYZ ) + { + // moving vertically and horizontally, check if we hit the floor + my->z += my->vel_z; + hitFromAbove = my->magicFallingCollision(); + my->processEntityWind(); + + bool halfSpeedCheck = false; + static ConsoleVariable cvar_magic_clip("/magic_clip_test", true); + real_t speed = sqrt(pow(my->vel_x, 2) + pow(my->vel_y, 2)); + if ( speed > 4.0 ) // can clip through thin gates + { + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( auto it : entLists ) { - bool ignoreMoveAside = false; - if ( trapResist == 100 ) + if ( !*cvar_magic_clip && (svFlags & SV_FLAG_CHEATS) ) { - ignoreMoveAside = true; + break; } - - if ( parent->behavior == &actMagicTrap && !ignoreMoveAside ) + for ( node_t* node = it->first; node != nullptr; node = node->next ) { - if ( static_cast(parent->y / 16) == static_cast(hit.entity->y / 16) ) - { - // avoid y axis. - int direction = 1; - if ( local_rng.rand() % 2 == 0 ) - { - direction = -1; - } - if ( hit.entity->monsterSetPathToLocation(hit.entity->x / 16, (hit.entity->y / 16) + 1 * direction, 0, - GeneratePathTypes::GENERATE_PATH_MOVEASIDE) ) - { - hit.entity->monsterState = MONSTER_STATE_HUNT; - serverUpdateEntitySkill(hit.entity, 0); - } - else if ( hit.entity->monsterSetPathToLocation(hit.entity->x / 16, (hit.entity->y / 16) - 1 * direction, 0, - GeneratePathTypes::GENERATE_PATH_MOVEASIDE) ) - { - hit.entity->monsterState = MONSTER_STATE_HUNT; - serverUpdateEntitySkill(hit.entity, 0); - } - else - { - monsterMoveAside(hit.entity, hit.entity); - } - } - else if ( static_cast(parent->x / 16) == static_cast(hit.entity->x / 16) ) + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actGate || entity->behavior == &actDoor || entity->behavior == &actIronDoor ) { - int direction = 1; - if ( local_rng.rand() % 2 == 0 ) - { - direction = -1; - } - // avoid x axis. - if ( hit.entity->monsterSetPathToLocation((hit.entity->x / 16) + 1 * direction, hit.entity->y / 16, 0, - GeneratePathTypes::GENERATE_PATH_MOVEASIDE) ) - { - hit.entity->monsterState = MONSTER_STATE_HUNT; - serverUpdateEntitySkill(hit.entity, 0); - } - else if ( hit.entity->monsterSetPathToLocation((hit.entity->x / 16) - 1 * direction, hit.entity->y / 16, 0, - GeneratePathTypes::GENERATE_PATH_MOVEASIDE) ) - { - hit.entity->monsterState = MONSTER_STATE_HUNT; - serverUpdateEntitySkill(hit.entity, 0); - } - else + if ( entityDist(my, entity) <= speed ) { - monsterMoveAside(hit.entity, hit.entity); + halfSpeedCheck = true; + break; } } - else - { - monsterMoveAside(hit.entity, hit.entity); - } } - else if ( !ignoreMoveAside ) + if ( halfSpeedCheck ) { - monsterMoveAside(hit.entity, hit.entity); + break; } } - else + } + + if ( !halfSpeedCheck ) + { + dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); + if ( !hitFromAbove && my->z > -5 ) { - alertTarget = true; - alertAllies = true; - if ( parent->behavior == &actMonster && parent->monsterAllyIndex != -1 ) - { - if ( hit.entity->behavior == &actMonster && hit.entity->monsterAllyIndex != -1 ) - { - // if a player ally + hit another ally, don't aggro back - alertTarget = false; - } - } - if ( !strcmp(element->element_internal_name, spellElement_telePull.element_internal_name) - || !strcmp(element->element_internal_name, spellElement_shadowTag.element_internal_name) ) + // if we didn't hit the floor, process normal horizontal movement collision if we aren't too high + if ( dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y) ) { - alertTarget = false; - alertAllies = false; + hitFromAbove = true; } - if ( hitstats->type == SHOPKEEPER && !strcmp(element->element_internal_name, spellElement_charmMonster.element_internal_name) ) + } + } + else + { + real_t vel_x = my->vel_x / 2.0; + real_t vel_y = my->vel_y / 2.0; + real_t dist = clipMove(&my->x, &my->y, vel_x, vel_y, my); + if ( !hitFromAbove && my->z > -5 ) + { + // if we didn't hit the floor, process normal horizontal movement collision if we aren't too high + if ( dist != sqrt(vel_x * vel_x + vel_y * vel_y) ) { - if ( parent->behavior == &actPlayer ) - { - alertTarget = false; - alertAllies = false; - } + hitFromAbove = true; } - /*if ( hitstats->type == SHOPKEEPER && parent->getMonsterTypeFromSprite() == SHOPKEEPER ) - { - alertTarget = false; - }*/ - if ( my->actmagicCastByTinkerTrap == 1 ) + } + if ( !hitFromAbove ) + { + dist = clipMove(&my->x, &my->y, vel_x, vel_y, my); + if ( !hitFromAbove && my->z > -5 ) { - if ( entityDist(hit.entity, parent) > TOUCHRANGE ) + // if we didn't hit the floor, process normal horizontal movement collision if we aren't too high + if ( dist != sqrt(vel_x * vel_x + vel_y * vel_y) ) { - // don't alert if bomb thrower far away. - alertTarget = false; - alertAllies = false; + hitFromAbove = true; } } + } + } - if ( spell->ID != SPELL_SLIME_TAR ) // processed manually later - { - if ( alertTarget && hit.entity->monsterState != MONSTER_STATE_ATTACK && (hitstats->type < LICH || hitstats->type >= SHOPKEEPER) ) - { - hit.entity->monsterAcquireAttackTarget(*parent, MONSTER_STATE_PATH, true); - } + if ( my->sprite == 2209 ) + { + my->roll += 0.25; + } - hit.entity->updateEntityOnHit(parent, alertTarget); - } - if ( parent->behavior == &actPlayer || parent->monsterAllyIndex != -1 ) - { - if ( hit.entity->behavior == &actPlayer || (hit.entity->behavior == &actMonster && hit.entity->monsterAllyIndex != -1) ) - { - // if a player ally + hit another ally or player, don't alert other allies. - alertAllies = false; - } - } - - // alert other monsters too - if ( alertAllies ) - { - hit.entity->alertAlliesOnBeingHit(parent); - } - } - } - } - - // check for magic resistance... - // resistance stacks diminishingly - int resistance = 0; - DamageGib dmgGib = DMG_DEFAULT; - real_t damageMultiplier = 1.0; - if ( hit.entity ) - { - resistance = Entity::getMagicResistance(hit.entity->getStats()); - if ( (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) && hitstats ) + if ( my->actmagicProjectileArc > 0 ) { - damageMultiplier = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_MAGIC); - if ( damageMultiplier <= 0.75 ) - { - dmgGib = DMG_WEAKEST; - } - else if ( damageMultiplier <= 0.85 ) + real_t z = -1 - my->z; + if ( z > 0 ) { - dmgGib = DMG_WEAKER; + my->pitch = -atan(z * 0.1 / sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y)); } - else if ( damageMultiplier >= 1.25 ) + else { - dmgGib = resistance == 0 ? DMG_STRONGEST : DMG_WEAKER; + my->pitch = -atan(z * 0.15 / sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y)); } - else if ( damageMultiplier >= 1.15 ) + if ( my->actmagicProjectileArc == 1 ) { - dmgGib = resistance == 0 ? DMG_STRONGER : DMG_WEAKER; + //messagePlayer(0, "z: %f vel: %f", my->z, my->vel_z); + my->vel_z = my->vel_z * 0.9; + if ( my->vel_z > -0.1 ) + { + //messagePlayer(0, "arc down"); + my->actmagicProjectileArc = 2; + my->vel_z = 0.01; + } } - else if ( resistance > 0 ) + else if ( my->actmagicProjectileArc == 2 ) { - dmgGib = DMG_WEAKEST; + //messagePlayer(0, "z: %f vel: %f", my->z, my->vel_z); + my->vel_z = std::min(0.8, my->vel_z * 1.2); } } } - - if ( hit.entity ) + else { - if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + my->processEntityWind(); + + bool halfSpeedCheck = false; + static ConsoleVariable cvar_magic_clip2("/magic_clip_test2", true); + real_t speed = sqrt(pow(my->vel_x, 2) + pow(my->vel_y, 2)); + if ( speed > 4.0 ) // can clip through thin gates { - if ( trapResist != 0 ) + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( auto it : entLists ) { - damageMultiplier += -(trapResist / 100.0); - damageMultiplier = std::max(0.0, damageMultiplier); + if ( !*cvar_magic_clip2 && (svFlags & SV_FLAG_CHEATS) ) + { + break; + } + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actGate || entity->behavior == &actDoor || entity->behavior == &actIronDoor ) + { + if ( entityDist(my, entity) <= speed ) + { + halfSpeedCheck = true; + break; + } + } + } + if ( halfSpeedCheck ) + { + break; + } } } - } - - real_t spellbookDamageBonus = (my->actmagicSpellbookBonus / 100.f); - if ( parent && parent->behavior == &actDeathGhost ) - { - // no extra bonus here - } - else - { - if ( my->actmagicCastByMagicstaff == 0 && my->actmagicCastByTinkerTrap == 0 ) + if ( !halfSpeedCheck ) { - spellbookDamageBonus += getBonusFromCasterOfSpellElement(parent, nullptr, element, spell ? spell->ID : SPELL_NONE); - if ( parent && parent->behavior == &actPlayer ) - { - Compendium_t::Events_t::eventUpdateCodex(parent->skill[2], Compendium_t::CPDM_CLASS_PWR_MAX_CASTED, "pwr", - 100 + (Sint32)(spellbookDamageBonus * 100.0)); - } + dist = clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); //normal flat projectiles + hitFromSide = dist != sqrt(my->vel_x * my->vel_x + my->vel_y * my->vel_y); } - if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + else { - if ( gameModeManager.currentSession.challengeRun.isActive(GameModeManager_t::CurrentSession_t::ChallengeRun_t::CHEVENT_STRONG_TRAPS) ) + real_t vel_x = my->vel_x / 2.0; + real_t vel_y = my->vel_y / 2.0; + dist = clipMove(&my->x, &my->y, vel_x, vel_y, my); + hitFromSide = dist != sqrt(vel_x * vel_x + vel_y * vel_y); + if ( !hitFromSide ) { - spellbookDamageBonus += 1.0; + dist = clipMove(&my->x, &my->y, vel_x, vel_y, my); + hitFromSide = dist != sqrt(vel_x * vel_x + vel_y * vel_y); } } } + } + + if ( my->actmagicDelayMove == 0 + && (hitFromAbove + || (my->actmagicIsVertical != MAGIC_ISVERTICAL_XYZ && my->actmagicSpray != 1 && hitFromSide)) ) + { + node = element->elements.first; + //element = (spellElement_t *) element->elements->first->element; + element = (spellElement_t*)node->element; + //if (hit.entity != NULL) { + bool mimic = hit.entity && hit.entity->isInertMimic(); + Stat* hitstats = nullptr; + int player = -1; - if (!strcmp(element->element_internal_name, spellElement_force.element_internal_name)) + if ( hit.entity ) { - if (hit.entity) + hitstats = hit.entity->getStats(); + if ( hit.entity->behavior == &actPlayer ) { - if ( mimic ) - { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->chestHandleDamageMagic(damage, *my, parent); - my->removeLightField(); - list_RemoveNode(my->mynode); - return; - } - else if (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) + player = hit.entity->skill[2]; + } + if ( hit.entity && hitstats ) + { + if ( hitstats->getEffectActive(EFF_MAGICIANS_ARMOR) + && !(!(svFlags & SV_FLAG_FRIENDLYFIRE) && parent + && parent->checkFriend(hit.entity) + && parent->friendlyFireProtection(hit.entity)) ) { - Entity* parent = uidToEntity(my->parent); - playSoundEntity(hit.entity, 28, 128); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - Sint32 preResistanceDamage = damage; - damage *= damageMultiplier; - damage /= (1 + (int)resistance); - Sint32 oldHP = hitstats->HP; - hit.entity->modHP(-damage); - magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); - magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); - for (i = 0; i < damage; i += 2) //Spawn a gib for every two points of damage. + Uint8 effectStrength = hitstats->getEffectActive(EFF_MAGICIANS_ARMOR); + int duration = hitstats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR]; + if ( effectStrength == 1 ) { - Entity* gib = spawnGib(hit.entity); - serverSpawnGibForClient(gib); + if ( hitstats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR] > 0 ) + { + hitstats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR] = 1; + } } - - if (parent) + else if ( effectStrength > 1 ) { - parent->killedByMonsterObituary(hit.entity); + --effectStrength; + hitstats->setEffectValueUnsafe(EFF_MAGICIANS_ARMOR, effectStrength); + hit.entity->setEffect(EFF_MAGICIANS_ARMOR, effectStrength, hitstats->EFFECTS_TIMERS[EFF_MAGICIANS_ARMOR], true); } - // update enemy bar for attacker - if ( !strcmp(hitstats->name, "") ) + magicOnSpellCastEvent(hit.entity, hit.entity, parent, SPELL_MAGICIANS_ARMOR, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + if ( hit.entity->behavior == &actPlayer ) { - updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, - false, dmgGib); + steamStatisticUpdateClient(hit.entity->skill[2], STEAM_STAT_DOESNT_COUNT, STEAM_STAT_INT, 1); + if ( parent && parent->behavior == &actMonster ) + { + int type = parent->getMonsterTypeFromSprite(); + if ( type == LICH || type == LICH_FIRE || type == LICH_ICE ) + { + serverUpdatePlayerGameplayStats(hit.entity->skill[2], STATISTICS_THATS_CHEATING, 1); + } + } } - else + + if ( (parent && parent->behavior == &actPlayer) || (parent && parent->behavior == &actMonster && parent->monsterAllyGetPlayerLeader()) + || hit.entity->behavior == &actPlayer || hit.entity->monsterAllyGetPlayerLeader() ) { - updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, - false, dmgGib); + spawnDamageGib(hit.entity, 0, DamageGib::DMG_GUARD, DamageGibDisplayType::DMG_GIB_GUARD, true); } - if ( hitstats->HP <= 0 && parent) + if ( hit.entity->behavior == &actPlayer ) { - parent->awardXP( hit.entity, true, true ); - spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6470)); } - } - else if (hit.entity->behavior == &actDoor) - { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->doorHandleDamageMagic(damage, *my, parent); + if ( parent && parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], makeColorRGB(255, 255, 255), + *hitstats, Language::get(6466), Language::get(6467), MSG_COMBAT); // %s guards the spell + } + + Entity* fx = createParticleAestheticOrbit(hit.entity, 1817, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE); + fx->x = hit.entity->x; + fx->y = hit.entity->y; + fx->z = hit.entity->z; + real_t tangent = atan2(my->y - hit.entity->y, my->x - hit.entity->x); + fx->x += 4.0 * cos(tangent); + fx->y += 4.0 * sin(tangent); + fx->yaw = tangent; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 0; + serverSpawnMiscParticlesAtLocation(fx->x, fx->y, fx->z, PARTICLE_EFFECT_NULL_PARTICLE, 1817, 0, fx->yaw * 256.0); + + //playSoundEntity(hit.entity, 166, 128); my->removeLightField(); list_RemoveNode(my->mynode); return; } - else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) + } + + if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + { + if ( hit.entity->onEntityTrapHitSacredPath(parent) ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->colliderHandleDamageMagic(damage, *my, parent); + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + Language::get(6470)); + } + playSoundEntity(hit.entity, 166, 128); my->removeLightField(); list_RemoveNode(my->mynode); return; } - else if ( hit.entity->behavior == &actChest ) + } + } + + // count reflection + int reflection = 0; + if ( hitstats ) + { + if ( parent && + ((hit.entity->getRace() == LICH_ICE && parent->getRace() == LICH_FIRE) + || ((hit.entity->getRace() == LICH_FIRE || hitstats->leader_uid == parent->getUID()) && parent->getRace() == LICH_ICE) + || (parent->getRace() == LICH_ICE) && !strncmp(hitstats->name, "corrupted automaton", 19) + ) + ) + { + reflection = 3; + } + if ( !reflection ) + { + reflection = hit.entity->getReflection(); + } + if ( my->actmagicCastByTinkerTrap == 1 ) + { + reflection = 0; + } + if ( reflection == 3 && hitstats->shield + && (hitstats->shield->type == MIRROR_SHIELD || hitstats->getEffectActive(EFF_REFLECTOR_SHIELD) > 0) && hitstats->defending ) + { + if ( my->actmagicIsVertical == MAGIC_ISVERTICAL_Z ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->chestHandleDamageMagic(damage, *my, parent); - my->removeLightField(); - list_RemoveNode(my->mynode); - return; + reflection = 0; } - else if (hit.entity->behavior == &actFurniture ) + // calculate facing angle to projectile, need to be facing projectile to reflect. + else if ( player >= 0 && players[player] && players[player]->entity ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - int oldHP = hit.entity->furnitureHealth; - hit.entity->furnitureHealth -= damage; - if ( parent ) + real_t yawDiff = my->yawDifferenceFromEntity(players[player]->entity); + if ( yawDiff < (6 * PI / 5) ) { - if ( parent->behavior == &actPlayer ) + reflection = 0; + } + else + { + reflection = 3; + if ( hitstats->shield->type == MIRROR_SHIELD ) { - bool destroyed = oldHP > 0 && hit.entity->furnitureHealth <= 0; - if ( destroyed ) + if ( parent && (parent->behavior == &actMonster || parent->behavior == &actPlayer) ) { - gameModeManager.currentSession.challengeRun.updateKillEvent(hit.entity); + my->actmagicMirrorReflected = 1; + my->actmagicMirrorReflectedCaster = parent->getUID(); } - switch ( hit.entity->furnitureType ) + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SHIELD_REFLECT, hitstats->shield->type, 1); + } + else + { + if ( hitstats->getEffectActive(EFF_REFLECTOR_SHIELD) ) { - case FURNITURE_CHAIR: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(388)); - } - updateEnemyBar(parent, hit.entity, Language::get(677), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_TABLE: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(389)); - } - updateEnemyBar(parent, hit.entity, Language::get(676), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2505)); - } - updateEnemyBar(parent, hit.entity, Language::get(2505), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BUNKBED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2506)); - } - updateEnemyBar(parent, hit.entity, Language::get(2506), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_PODIUM: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2507)); - } - updateEnemyBar(parent, hit.entity, Language::get(2507), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - default: - break; + magicOnSpellCastEvent(hit.entity, hit.entity, parent, SPELL_REFLECTOR, spell_t::SpellOnCastEventTypes::SPELL_LEVEL_EVENT_DEFAULT, 1); } } } - playSoundEntity(hit.entity, 28, 128); } } } - else if (!strcmp(element->element_internal_name, spellElement_magicmissile.element_internal_name)) + + bool yourSpellHitsTheMonster = false; + bool youAreHitByASpell = false; + if ( hit.entity ) { - spawnExplosion(my->x, my->y, my->z); - if (hit.entity) + if ( hit.entity->behavior == &actPlayer ) { - if ( mimic ) + bool skipMessage = false; + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) && my->actmagicTinkerTrapFriendlyFire == 0 && my->actmagicAllowFriendlyFireHit == 0 ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->chestHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) + if ( parent + && (parent->behavior == &actMonster || parent->behavior == &actPlayer) + && parent->checkFriend(hit.entity) + && parent->friendlyFireProtection(hit.entity) ) { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } - if ( !(my->actmagicIsOrbiting == 2) ) - { - my->removeLightField(); - list_RemoveNode(my->mynode); - } - else - { - spawnExplosion(my->x, my->y, my->z); + skipMessage = true; } - return; } - else if (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) + + if ( my->actmagicCastByTinkerTrap == 1 ) { - Entity* parent = uidToEntity(my->parent); - playSoundEntity(hit.entity, 28, 128); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - if ( my->actmagicIsOrbiting == 2 ) + skipMessage = true; + } + if ( !skipMessage ) + { + Uint32 color = makeColorRGB(255, 0, 0); + if ( reflection == 0 ) { - spawnExplosion(my->x, my->y, my->z); - if ( parent && my->actmagicOrbitCastFromSpell == 1 ) + if ( particleEmitterHitProps ) { - // cast through amplify magic effect - damage /= 2; - } - damage = damage - local_rng.rand() % ((damage / 8) + 1); - } - - Sint32 preResistanceDamage = damage; - damage *= damageMultiplier; - damage /= (1 + (int)resistance); - Sint32 oldHP = hitstats->HP; - hit.entity->modHP(-damage); - magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); - magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); - for (i = 0; i < damage; i += 2) //Spawn a gib for every two points of damage. - { - Entity* gib = spawnGib(hit.entity); - serverSpawnGibForClient(gib); + if ( particleEmitterHitProps->hits == 1 ) + { + if ( my->actmagicSpray == 1 || my->actmagicSpray == 2 ) + { + //messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(6238)); + //youAreHitByASpell = true; + } + else + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(376)); + youAreHitByASpell = true; + } + } + } + else + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(376)); + youAreHitByASpell = true; + } } - - // write the obituary - if ( parent ) + } + if ( hitstats ) + { + entityHealth = hitstats->HP; + } + } + if ( parent && hitstats ) + { + if ( parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + if ( strcmp(element->element_internal_name, spellElement_charmMonster.element_internal_name) ) { - parent->killedByMonsterObituary(hit.entity); + if ( my->actmagicCastByTinkerTrap == 1 || my->actmagicNoHitMessage != 0 ) + { + // no message + //messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(3498), Language::get(3499), MSG_COMBAT); + } + else + { + if ( reflection == 0 ) + { + yourSpellHitsTheMonster = true; + if ( !hit.entity->isInertMimic() ) + { + if ( ItemTooltips.bSpellHasBasicHitMessage(spell->ID) ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(378), Language::get(377), MSG_COMBAT_BASIC); + } + else + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(378), Language::get(377), MSG_COMBAT); + } + } + } + } } + } + } + } - // update enemy bar for attacker - if ( !strcmp(hitstats->name, "") ) + // Only degrade the equipment if Friendly Fire is ON or if it is (OFF && target is an enemy) + bool bShouldEquipmentDegrade = false; + if ( parent && parent->behavior == &actDeathGhost ) + { + bShouldEquipmentDegrade = false; + } + else if ( (svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + // Friendly Fire is ON, equipment should always degrade, as hit will register + bShouldEquipmentDegrade = true; + } + else + { + // Friendly Fire is OFF, is the target an enemy? + if ( parent != nullptr && (parent->checkFriend(hit.entity)) == false ) + { + // Target is an enemy, equipment should degrade + bShouldEquipmentDegrade = true; + } + } + + // Handling reflecting the missile + if ( reflection ) + { + spell_t* spellIsReflectingMagic = nullptr; + if ( hit.entity ) + { + spellIsReflectingMagic = hit.entity->getActiveMagicEffect(SPELL_REFLECT_MAGIC); + playSoundEntity(hit.entity, 166, 128); + if ( hit.entity->behavior == &actPlayer ) + { + if ( youAreHitByASpell ) { - updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, - false, dmgGib); + if ( !spellIsReflectingMagic ) + { + messagePlayer(player, MESSAGE_COMBAT, Language::get(379)); // but it bounces off! + } + else + { + messagePlayer(player, MESSAGE_COMBAT, Language::get(2475)); // but it bounces off! + } } else { - updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, - false, dmgGib); - } - - if ( hitstats->HP <= 0 && parent) - { - parent->awardXP( hit.entity, true, true ); - spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); + messagePlayer(player, MESSAGE_COMBAT, Language::get(4325)); // you reflected a spell! } } - else if ( hit.entity->behavior == &actDoor ) + } + if ( parent && hitstats ) + { + if ( parent->behavior == &actPlayer ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - damage /= (1 + (int)resistance); - hit.entity->doorHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) - { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } - if ( !(my->actmagicIsOrbiting == 2) ) + if ( yourSpellHitsTheMonster ) { - my->removeLightField(); - list_RemoveNode(my->mynode); + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(379)); // but it bounces off! } else { - spawnExplosion(my->x, my->y, my->z); + messagePlayerMonsterEvent(parent->skill[2], makeColorRGB(255, 255, 255), + *hitstats, Language::get(4322), Language::get(4323), MSG_COMBAT); // your spell bounces off the monster! } - return; } - else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) + } + + if ( hit.side == 0 || hit.entity ) + { + my->vel_x *= -1; + my->vel_y *= -1; + my->yaw = atan2(my->vel_y, my->vel_x); + } + else if ( hit.side == HORIZONTAL ) + { + my->vel_x *= -1; + my->yaw = atan2(my->vel_y, my->vel_x); + } + else if ( hit.side == VERTICAL ) + { + my->vel_y *= -1; + my->yaw = atan2(my->vel_y, my->vel_x); + } + if ( hit.entity ) + { + if ( (parent && parent->behavior == &actMagicTrapCeiling) || my->actmagicIsVertical == MAGIC_ISVERTICAL_Z ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); + // this missile came from the ceiling, let's redirect it.. + my->x = hit.entity->x + cos(hit.entity->yaw); + my->y = hit.entity->y + sin(hit.entity->yaw); + my->yaw = hit.entity->yaw; + my->z = -1; + my->vel_x = 4 * cos(hit.entity->yaw); + my->vel_y = 4 * sin(hit.entity->yaw); + my->vel_z = 0; + my->actmagicIsVertical = 0; + my->pitch = 0; + } + my->parent = hit.entity->getUID(); + ++my->actmagicReflectionCount; + } - hit.entity->colliderHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) - { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } - if ( !(my->actmagicIsOrbiting == 2) ) - { - my->removeLightField(); - list_RemoveNode(my->mynode); - } - else - { - spawnExplosion(my->x, my->y, my->z); - } - return; + if ( bShouldEquipmentDegrade ) + { + // Reflection of 3 does not degrade equipment + bool chance = false; + if ( my->actmagicSpray == 1 || my->actmagicSpray == 2 ) + { + chance = local_rng.rand() % 10 == 0; } - else if ( hit.entity->behavior == &actChest ) + else { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->chestHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) - { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } - if ( !(my->actmagicIsOrbiting == 2) ) - { - my->removeLightField(); - list_RemoveNode(my->mynode); - } - else - { - spawnExplosion(my->x, my->y, my->z); - } - return; + chance = local_rng.rand() % 2 == 0; } - else if (hit.entity->behavior == &actFurniture ) + if ( chance && hitstats && reflection < 3 ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - int oldHP = hit.entity->furnitureHealth; - hit.entity->furnitureHealth -= damage; - if ( parent ) + // set armornum to the relevant equipment slot to send to clients + int armornum = 5 + reflection; + if ( (player >= 0 && players[player]->isLocalPlayer()) || player < 0 ) { - if ( parent->behavior == &actPlayer ) + if ( reflection == 1 ) { - bool destroyed = oldHP > 0 && hit.entity->furnitureHealth <= 0; - if ( destroyed ) - { - gameModeManager.currentSession.challengeRun.updateKillEvent(hit.entity); - } - switch ( hit.entity->furnitureType ) + if ( hitstats->cloak ) { - case FURNITURE_CHAIR: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(388)); - } - updateEnemyBar(parent, hit.entity, Language::get(677), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_TABLE: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(389)); - } - updateEnemyBar(parent, hit.entity, Language::get(676), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BED: - if ( destroyed ) + if ( hitstats->cloak->count > 1 ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2505)); + newItem(hitstats->cloak->type, hitstats->cloak->status, hitstats->cloak->beatitude, hitstats->cloak->count - 1, hitstats->cloak->appearance, hitstats->cloak->identified, &hitstats->inventory); } - updateEnemyBar(parent, hit.entity, Language::get(2505), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BUNKBED: - if ( destroyed ) + } + } + else if ( reflection == 2 ) + { + if ( hitstats->amulet ) + { + if ( hitstats->amulet->count > 1 ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2506)); + newItem(hitstats->amulet->type, hitstats->amulet->status, hitstats->amulet->beatitude, hitstats->amulet->count - 1, hitstats->amulet->appearance, hitstats->amulet->identified, &hitstats->inventory); } - updateEnemyBar(parent, hit.entity, Language::get(2506), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_PODIUM: - if ( destroyed ) + } + } + else if ( reflection == -1 ) + { + if ( hitstats->shield ) + { + if ( hitstats->shield->count > 1 ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2507)); + newItem(hitstats->shield->type, hitstats->shield->status, hitstats->shield->beatitude, hitstats->shield->count - 1, hitstats->shield->appearance, hitstats->shield->identified, &hitstats->inventory); } - updateEnemyBar(parent, hit.entity, Language::get(2507), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - default: - break; } } } - playSoundEntity(hit.entity, 28, 128); - if ( my->actmagicProjectileArc > 0 ) + if ( reflection == 1 ) { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } - if ( !(my->actmagicIsOrbiting == 2) ) - { - my->removeLightField(); - list_RemoveNode(my->mynode); - } - else - { - spawnExplosion(my->x, my->y, my->z); - } - return; - } - } - } - else if (!strcmp(element->element_internal_name, spellElement_fire.element_internal_name)) - { - if ( !(my->actmagicIsOrbiting == 2) ) - { - spawnExplosion(my->x, my->y, my->z); - } - if (hit.entity) - { - // Attempt to set the Entity on fire - hit.entity->SetEntityOnFire(); - - if ( mimic ) - { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->chestHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) - { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } - if ( !(my->actmagicIsOrbiting == 2) ) - { - my->removeLightField(); - list_RemoveNode(my->mynode); - } - else - { - spawnExplosion(my->x, my->y, my->z); - } - return; - } - else if (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) - { - real_t fireMultiplier = 1.0; - //if ( hitstats->helmet && hitstats->helmet->type == HAT_WARM ) - //{ - // if ( !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) - // { - // if ( hitstats->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(hitstats) ) - // { - // fireMultiplier += 0.5; - // } - // else - // { - // fireMultiplier += 0.5 + 0.5 * abs(hitstats->helmet->beatitude); // cursed, extra fire damage - // } - // } - //} - - //playSoundEntity(my, 153, 64); - playSoundEntity(hit.entity, 28, 128); - //TODO: Apply fire resistances/weaknesses. - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - if ( my->actmagicIsOrbiting == 2 ) - { - spawnExplosion(my->x, my->y, my->z); - if ( parent && my->actmagicOrbitCastFromSpell == 0 ) + if ( hitstats->cloak + && !hit.entity->spellEffectPreserveItem(hitstats->cloak) ) { - if ( parent->behavior == &actParticleDot ) + hitstats->cloak->count = 1; + hitstats->cloak->status = static_cast(std::max(static_cast(BROKEN), hitstats->cloak->status - 1)); + if ( hitstats->cloak->status != BROKEN ) { - damage = parent->skill[1]; + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(380)); } - else if ( parent->behavior == &actPlayer ) + else { - Stat* playerStats = parent->getStats(); - if ( playerStats ) + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(381)); + playSoundEntity(hit.entity, 76, 64); + if ( player >= 0 ) { - int skillLVL = playerStats->getModifiedProficiency(PRO_ALCHEMY) / 20; - damage = (14 + skillLVL * 1.5); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, hitstats->cloak->type, 1); } } - else - { - damage = 14; - } - } - else if ( parent && my->actmagicOrbitCastFromSpell == 1 ) - { - // cast through amplify magic effect - damage /= 2; } - else - { - damage = 14; - } - damage = damage - local_rng.rand() % ((damage / 8) + 1); } - Sint32 preResistanceDamage = damage; - damage *= damageMultiplier; - if ( parent ) + else if ( reflection == 2 ) { - Stat* casterStats = parent->getStats(); - if ( casterStats && casterStats->type == LICH_FIRE && parent->monsterLichAllyStatus == LICH_ALLY_DEAD ) + if ( hitstats->amulet + && !hit.entity->spellEffectPreserveItem(hitstats->amulet) ) { - damage *= 2; + hitstats->amulet->count = 1; + hitstats->amulet->status = static_cast(std::max(static_cast(BROKEN), hitstats->amulet->status - 1)); + if ( hitstats->amulet->status != BROKEN ) + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(382)); + } + else + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(383)); + playSoundEntity(hit.entity, 76, 64); + if ( player >= 0 ) + { + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, hitstats->amulet->type, 1); + } + } } } - int oldHP = hitstats->HP; - damage *= fireMultiplier; - damage /= (1 + (int)resistance); - hit.entity->modHP(-damage); - magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); - magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); - //for (i = 0; i < damage; i += 2) { //Spawn a gib for every two points of damage. - Entity* gib = spawnGib(hit.entity); - serverSpawnGibForClient(gib); - //} - - // write the obituary - if ( parent ) + else if ( reflection == -1 ) { - if ( my->actmagicIsOrbiting == 2 - && parent->behavior == &actParticleDot - && parent->skill[1] > 0 ) + if ( hitstats->shield + && !hit.entity->spellEffectPreserveItem(hitstats->shield) ) { - if ( hitstats && !strcmp(hitstats->obituary, Language::get(3898)) ) + hitstats->shield->count = 1; + hitstats->shield->status = static_cast(std::max(static_cast(BROKEN), hitstats->shield->status - 1)); + if ( hitstats->shield->status != BROKEN ) { - // was caused by a flaming boulder. - hit.entity->setObituary(Language::get(3898)); - hitstats->killer = KilledBy::BOULDER; + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(384)); } else { - // blew the brew (alchemy) - hit.entity->setObituary(Language::get(3350)); - hitstats->killer = KilledBy::FAILED_ALCHEMY; + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(385)); + playSoundEntity(hit.entity, 76, 64); + if ( player >= 0 ) + { + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_BROKEN, hitstats->shield->type, 1); + } } } + } + if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "ARMR"); + net_packet->data[4] = armornum; + if ( reflection == 1 ) + { + net_packet->data[5] = hitstats->cloak->status; + SDLNet_Write16(hitstats->cloak->type, &net_packet->data[6]); + } + else if ( reflection == 2 ) + { + net_packet->data[5] = hitstats->amulet->status; + SDLNet_Write16(hitstats->amulet->type, &net_packet->data[6]); + } else { - parent->killedByMonsterObituary(hit.entity); + net_packet->data[5] = hitstats->shield->status; + SDLNet_Write16(hitstats->shield->type, &net_packet->data[6]); } + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); } + } + } + + if ( spellIsReflectingMagic ) + { + int spellCost = getCostOfSpell(spell) + 5 + local_rng.rand() % 6; + bool unsustain = false; + if ( spellCost >= hit.entity->getMP() ) //Unsustain the spell if expended all mana. + { + unsustain = true; + } + + hit.entity->drainMP(spellCost); + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z / 2, 174); + playSoundEntity(hit.entity, 166, 128); //TODO: Custom sound effect? + + if ( unsustain ) + { + spellIsReflectingMagic->sustain = false; if ( hitstats ) { - hitstats->burningInflictedBy = static_cast(my->parent); + hit.entity->setEffect(EFF_MAGICREFLECT, false, 0, true); + messagePlayer(player, MESSAGE_STATUS, Language::get(2476)); } + } + } - // update enemy bar for attacker - if ( !strcmp(hitstats->name, "") ) + if ( my->actmagicReflectionCount >= 3 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + + if ( my->actmagicSpray == 2 ) + { + if ( Entity* gib = spawnFociGib(my->x, my->y, 1.0, my->yaw, 0.0, hit.entity->getUID(), my->sprite, local_rng.rand()) ) + { + gib->actmagicReflectionCount = my->actmagicReflectionCount; + + if ( spell ) { - updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, - false, dmgGib); + node_t* node = list_AddNodeFirst(&gib->children); + node->element = copySpell(spell); + ((spell_t*)node->element)->caster = hit.entity->getUID(); + node->deconstructor = &spellDeconstructor; + node->size = sizeof(spell_t); } - else + } + + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + return; + } + + // Test for Friendly Fire, if Friendly Fire is OFF, delete the missile + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( my->actmagicAllowFriendlyFireHit == 1 + || my->actmagicTinkerTrapFriendlyFire == 1 ) + { + // these spells can hit allies no penalty. + } + else if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + + // check for magic resistance... + // resistance stacks diminishingly + int trapResist = 0; + int resistance = 0; + DamageGib dmgGib = DMG_DEFAULT; + real_t damageMultiplier = 1.0; + magicSetResistance(hit.entity, parent, resistance, damageMultiplier, dmgGib, trapResist, spell->ID); + + bool alertTarget = false; + bool alertAllies = false; + + // Alerting the hit Entity + if ( hit.entity ) + { + // alert the hit entity if it was a monster + if ( hit.entity->behavior == &actMonster && parent != nullptr && parent->behavior != &actDeathGhost ) + { + if ( parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling ) + { + bool ignoreMoveAside = false; + if ( trapResist == 100 ) { - updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, - false, dmgGib); + ignoreMoveAside = true; } - if ( oldHP > 0 && hitstats->HP <= 0 ) + + if ( parent->behavior == &actMagicTrap && !ignoreMoveAside ) { - if ( parent ) + if ( static_cast(parent->y / 16) == static_cast(hit.entity->y / 16) ) { - if ( my->actmagicIsOrbiting == 2 && my->actmagicOrbitCastFromSpell == 0 && parent->behavior == &actPlayer ) + // avoid y axis. + int direction = 1; + if ( local_rng.rand() % 2 == 0 ) { - if ( hitstats->type == LICH || hitstats->type == LICH_ICE || hitstats->type == LICH_FIRE ) - { - if ( true/*client_classes[parent->skill[2]] == CLASS_BREWER*/ ) - { - steamAchievementClient(parent->skill[2], "BARONY_ACH_SECRET_WEAPON"); - } - } - steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_BOMBARDIER, STEAM_STAT_INT, 1); + direction = -1; } - if ( my->actmagicCastByTinkerTrap == 1 && parent->behavior == &actPlayer && hitstats->type == MINOTAUR ) + if ( hit.entity->monsterSetPathToLocation(hit.entity->x / 16, (hit.entity->y / 16) + 1 * direction, 0, + GeneratePathTypes::GENERATE_PATH_MOVEASIDE) ) { - steamAchievementClient(parent->skill[2], "BARONY_ACH_TIME_TO_PLAN"); + hit.entity->monsterState = MONSTER_STATE_HUNT; + serverUpdateEntitySkill(hit.entity, 0); } - parent->awardXP( hit.entity, true, true ); - spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); - } - else - { - if ( achievementObserver.checkUidIsFromPlayer(my->parent) >= 0 ) + else if ( hit.entity->monsterSetPathToLocation(hit.entity->x / 16, (hit.entity->y / 16) - 1 * direction, 0, + GeneratePathTypes::GENERATE_PATH_MOVEASIDE) ) { - steamAchievementClient(achievementObserver.checkUidIsFromPlayer(my->parent), "BARONY_ACH_TAKING_WITH"); + hit.entity->monsterState = MONSTER_STATE_HUNT; + serverUpdateEntitySkill(hit.entity, 0); } + else + { + monsterMoveAside(hit.entity, hit.entity); + } + } + else if ( static_cast(parent->x / 16) == static_cast(hit.entity->x / 16) ) + { + int direction = 1; + if ( local_rng.rand() % 2 == 0 ) + { + direction = -1; + } + // avoid x axis. + if ( hit.entity->monsterSetPathToLocation((hit.entity->x / 16) + 1 * direction, hit.entity->y / 16, 0, + GeneratePathTypes::GENERATE_PATH_MOVEASIDE) ) + { + hit.entity->monsterState = MONSTER_STATE_HUNT; + serverUpdateEntitySkill(hit.entity, 0); + } + else if ( hit.entity->monsterSetPathToLocation((hit.entity->x / 16) - 1 * direction, hit.entity->y / 16, 0, + GeneratePathTypes::GENERATE_PATH_MOVEASIDE) ) + { + hit.entity->monsterState = MONSTER_STATE_HUNT; + serverUpdateEntitySkill(hit.entity, 0); + } + else + { + monsterMoveAside(hit.entity, hit.entity); + } + } + else + { + monsterMoveAside(hit.entity, hit.entity); } } + else if ( !ignoreMoveAside ) + { + monsterMoveAside(hit.entity, hit.entity); + } } - else if (hit.entity->behavior == &actDoor) + else { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - - hit.entity->doorHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) + alertTarget = hit.entity->monsterAlertBeforeHit(parent); + alertAllies = true; + if ( !strcmp(element->element_internal_name, spellElement_telePull.element_internal_name) + || !strcmp(element->element_internal_name, spellElement_shadowTag.element_internal_name) ) { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + alertTarget = false; + alertAllies = false; } - if ( !(my->actmagicIsOrbiting == 2) ) + + if ( spell->ID == SPELL_HOLY_BEAM && parent && parent->checkFriend(hit.entity) ) { - my->removeLightField(); - list_RemoveNode(my->mynode); + alertTarget = false; + alertAllies = false; } - else + if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_NUMBING_BOLT].element_internal_name) ) { - spawnExplosion(my->x, my->y, my->z); + alertTarget = false; + alertAllies = false; } - return; - } - else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) - { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - - hit.entity->colliderHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) + if ( hitstats->type == SHOPKEEPER && !strcmp(element->element_internal_name, spellElement_charmMonster.element_internal_name) ) { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + if ( parent->behavior == &actPlayer ) + { + alertTarget = false; + alertAllies = false; + } } - if ( !(my->actmagicIsOrbiting == 2) ) + /*if ( hitstats->type == SHOPKEEPER && parent->getMonsterTypeFromSprite() == SHOPKEEPER ) { - my->removeLightField(); - list_RemoveNode(my->mynode); - } - else + alertTarget = false; + }*/ + if ( my->actmagicCastByTinkerTrap == 1 ) { - spawnExplosion(my->x, my->y, my->z); + if ( entityDist(hit.entity, parent) > TOUCHRANGE ) + { + // don't alert if bomb thrower far away. + alertTarget = false; + alertAllies = false; + } } - return; - } - else if (hit.entity->behavior == &actChest) - { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->chestHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) + + if ( spell->ID != SPELL_SLIME_TAR ) // processed manually later { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + if ( alertTarget && hit.entity->monsterState != MONSTER_STATE_ATTACK && (hitstats->type < LICH || hitstats->type >= SHOPKEEPER) ) + { + hit.entity->monsterAcquireAttackTarget(*parent, MONSTER_STATE_PATH, true); + } + + hit.entity->updateEntityOnHit(parent, alertTarget); } - if ( !(my->actmagicIsOrbiting == 2) ) + if ( parent->behavior == &actPlayer || parent->monsterAllyIndex != -1 ) { - my->removeLightField(); - list_RemoveNode(my->mynode); + if ( hit.entity->behavior == &actPlayer || (hit.entity->behavior == &actMonster && hit.entity->monsterAllyIndex != -1) ) + { + // if a player ally + hit another ally or player, don't alert other allies. + alertAllies = false; + } } - else + + // alert other monsters too + if ( alertAllies ) { - spawnExplosion(my->x, my->y, my->z); + hit.entity->alertAlliesOnBeingHit(parent); } - return; } - else if (hit.entity->behavior == &actFurniture ) + } + } + + real_t spellbookDamageBonus = (my->actmagicSpellbookBonus / 100.f); + if ( parent && parent->behavior == &actDeathGhost ) + { + // no extra bonus here + } + else + { + if ( my->actmagicCastByMagicstaff == 1 && spell && spell->ID == SPELL_SCEPTER_BLAST ) + { + spellbookDamageBonus += getBonusFromCasterOfSpellElement(parent, nullptr, element, spell ? spell->ID : SPELL_NONE, spell->skillID); + } + else if ( my->actmagicCastByMagicstaff != 1 && my->actmagicCastByTinkerTrap == 0 ) + { + spellbookDamageBonus += getBonusFromCasterOfSpellElement(parent, nullptr, element, spell ? spell->ID : SPELL_NONE, spell->skillID); + if ( parent && parent->behavior == &actPlayer ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - int oldHP = hit.entity->furnitureHealth; - hit.entity->furnitureHealth -= damage; - if ( parent ) + Compendium_t::Events_t::eventUpdateCodex(parent->skill[2], Compendium_t::CPDM_CLASS_PWR_MAX_CASTED, "pwr", + 100 + (Sint32)(spellbookDamageBonus * 100.0)); + } + absorbMagicEvent(hit.entity, parent, *my, spell->ID, nullptr, damageMultiplier, dmgGib); + } + else if ( parent && parent->behavior == &actMonster && my->actmagicCastByTinkerTrap == 0 ) + { + spellbookDamageBonus += getBonusFromCasterOfSpellElement(parent, nullptr, element, spell ? spell->ID : SPELL_NONE, spell->skillID); + } + + if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + { + if ( currentlevel >= 26 ) + { + spellbookDamageBonus += 1.0; + } + if ( gameModeManager.currentSession.challengeRun.isActive(GameModeManager_t::CurrentSession_t::ChallengeRun_t::CHEVENT_STRONG_TRAPS) ) + { + spellbookDamageBonus += 1.0; + } + } + } + + int damageTmp = 0; + Sint32 preResistanceDamageTmp = 0; + { + int magicDmg = element->getDamage(); + if ( my->actmagicAdditionalDamage > 0 ) + { + magicDmg += my->actmagicAdditionalDamage; + } + magicDmg += (spellbookDamageBonus * magicDmg * (abs(element->getDamageMult()) > 0.01 ? element->getDamageMult() : 1.0)); + if ( element->getDamageMult() > 0.01 && element->getDamage() > 0 ) + { + // range checking for PWR penalties, if we should do _some_ damage, then do at least 1 + magicDmg = std::max(1, magicDmg); + } + if ( hit.entity && !mimic && (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) ) + { + if ( my->actmagicIsOrbiting == 2 ) + { + updateEntityOldHPBeforeMagicHit(*hit.entity, *my); + + if ( !strcmp(element->element_internal_name, spellElement_magicmissile.element_internal_name) ) { - if ( parent->behavior == &actPlayer ) + if ( parent && my->actmagicOrbitCastFromSpell == 1 ) { - bool destroyed = oldHP > 0 && hit.entity->furnitureHealth <= 0; - if ( destroyed ) - { - gameModeManager.currentSession.challengeRun.updateKillEvent(hit.entity); - } - switch ( hit.entity->furnitureType ) + // cast through amplify magic effect + real_t mult = getSpellDamageFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0; + mult = std::min(mult, getSpellDamageSecondaryFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0); + magicDmg *= mult; + } + magicDmg = magicDmg - local_rng.rand() % ((magicDmg / 8) + 1); + } + else if ( !strcmp(element->element_internal_name, spellElement_fire.element_internal_name) ) + { + if ( my->actmagicIsOrbiting == 2 ) + { + if ( parent && my->actmagicOrbitCastFromSpell == 0 ) { - case FURNITURE_CHAIR: - if ( destroyed ) + if ( parent->behavior == &actParticleDot ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(388)); + magicDmg = parent->skill[1]; } - updateEnemyBar(parent, hit.entity, Language::get(677), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_TABLE: - if ( destroyed ) + else if ( parent->behavior == &actPlayer ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(389)); + Stat* playerStats = parent->getStats(); + if ( playerStats ) + { + int skillLVL = playerStats->getModifiedProficiency(PRO_ALCHEMY) / 20; + magicDmg = (14 + skillLVL * 1.5); + } } - updateEnemyBar(parent, hit.entity, Language::get(676), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BED: - if ( destroyed ) + else { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2505)); + magicDmg = 14; } - updateEnemyBar(parent, hit.entity, Language::get(2505), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BUNKBED: - if ( destroyed ) + } + else if ( parent && my->actmagicOrbitCastFromSpell == 1 ) + { + // cast through amplify magic effect + real_t mult = getSpellDamageFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0; + mult = std::min(mult, getSpellDamageSecondaryFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0); + magicDmg *= mult; + } + else + { + magicDmg = 14; + } + magicDmg = magicDmg - local_rng.rand() % ((magicDmg / 8) + 1); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_cold.element_internal_name) ) + { + if ( my->actmagicIsOrbiting == 2 ) + { + if ( parent && my->actmagicOrbitCastFromSpell == 0 ) + { + if ( parent->behavior == &actParticleDot ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2506)); + magicDmg = parent->skill[1]; } - updateEnemyBar(parent, hit.entity, Language::get(2506), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_PODIUM: - if ( destroyed ) + else if ( parent->behavior == &actPlayer ) + { + Stat* playerStats = parent->getStats(); + if ( playerStats ) + { + int skillLVL = playerStats->getModifiedProficiency(PRO_ALCHEMY) / 20; + magicDmg = (18 + skillLVL * 1.5); + } + } + else { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2507)); + magicDmg = 18; } - updateEnemyBar(parent, hit.entity, Language::get(2507), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - default: - break; } + else if ( parent && my->actmagicOrbitCastFromSpell == 1 ) + { + // cast through amplify magic effect + real_t mult = getSpellDamageFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0; + mult = std::min(mult, getSpellDamageSecondaryFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0); + magicDmg *= mult; + } + else + { + magicDmg = 18; + } + magicDmg = magicDmg - local_rng.rand() % ((magicDmg / 8) + 1); } } - playSoundEntity(hit.entity, 28, 128); - if ( my->actmagicProjectileArc > 0 ) - { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } - if ( !(my->actmagicIsOrbiting == 2) ) + else if ( !strcmp(element->element_internal_name, spellElement_lightning.element_internal_name) ) { - my->removeLightField(); - list_RemoveNode(my->mynode); + if ( my->actmagicIsOrbiting == 2 ) + { + if ( parent && my->actmagicOrbitCastFromSpell == 0 ) + { + if ( parent->behavior == &actParticleDot ) + { + magicDmg = parent->skill[1]; + } + else if ( parent->behavior == &actPlayer ) + { + Stat* playerStats = parent->getStats(); + if ( playerStats ) + { + int skillLVL = playerStats->getModifiedProficiency(PRO_ALCHEMY) / 20; + magicDmg = (12 + skillLVL * 1.5); + } + } + else + { + magicDmg = 12; + } + } + else if ( parent && my->actmagicOrbitCastFromSpell == 1 ) + { + // cast through amplify magic effect + real_t mult = getSpellDamageFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0; + mult = std::min(mult, getSpellDamageSecondaryFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0); + magicDmg *= mult; + } + else + { + magicDmg = 12; + } + magicDmg = magicDmg - local_rng.rand() % ((magicDmg / 8) + 1); + } } - else + } + + if ( Stat* hitstats = hit.entity->getStats() ) + { + if ( spell->ID == SPELL_SLIME_WATER ) { - spawnExplosion(my->x, my->y, my->z); + if ( hitstats->type == VAMPIRE ) + { + magicDmg *= 2; + } } - return; } } - } - else if (!strcmp(element->element_internal_name, spellElement_confuse.element_internal_name)) - { - if (hit.entity) + + Sint32 preResistanceDamage = magicDmg; + if ( hit.entity && (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) ) { - if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer) + if ( mimic ) { - int duration = (element->duration * (((element->mana) / static_cast(element->base_mana)) * element->overload_multiplier)); - duration /= (1 + (int)resistance); - - if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + //magicDmg = convertResistancePointsToMagicValue(magicDmg, resistance); + } + else + { + if ( spell->ID == SPELL_LIGHTNING || spell->ID == SPELL_LIGHTNING_BOLT || spell->ID == SPELL_LIGHTNING_NEXUS + || spell->ID == SPELL_FOCI_ARCS ) { - if ( trapResist > 0 ) + if ( hitstats && hitstats->getEffectActive(EFF_STATIC) ) { - if ( local_rng.rand() % 100 < trapResist ) + int extraDamage = getSpellDamageSecondaryFromID(spell->ID, parent, nullptr, my, (my->actmagicSpellbookBonus / 100.f)); + if ( extraDamage > 0 ) { - duration = 0; - } - } - } + extraDamage *= getSpellDamageFromStatic(spell->ID, hitstats); + if ( my->actmagicIsOrbiting == 2 ) + { + if ( parent && my->actmagicOrbitCastFromSpell == 1 ) + { + // cast through amplify magic effect + real_t mult = getSpellDamageFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0; + mult = std::min(mult, getSpellDamageSecondaryFromID(SPELL_AMPLIFY_MAGIC, parent, nullptr, my) / 100.0); + extraDamage *= mult; + } + } + magicDmg += std::max(1, extraDamage); + } + } + } + + Entity::modifyDamageMultipliersFromEffects(hit.entity, parent, damageMultiplier, DAMAGE_TABLE_MAGIC, my, spell->ID); - if ( duration > 0 && hit.entity->setEffect(EFF_CONFUSED, true, duration, false) ) + magicDmg *= damageMultiplier; + + if ( parent ) { - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); - playSoundEntity(hit.entity, 174, 64); - if ( hit.entity->behavior == &actMonster ) - { - hit.entity->monsterTarget = 0; // monsters forget what they're doing - } - if ( parent ) + if ( Stat* casterStats = parent->getStats() ) { - Uint32 color = makeColorRGB(0, 255, 0); - if ( parent->behavior == &actPlayer ) + if ( casterStats && casterStats->type == LICH_FIRE && parent->monsterLichAllyStatus == LICH_ALLY_DEAD ) { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(391), Language::get(390), MSG_COMBAT); + if ( !strcmp(element->element_internal_name, spellElement_bleed.element_internal_name) + || !strcmp(element->element_internal_name, spellElement_fire.element_internal_name) ) + { + magicDmg *= 2; + } } } - Uint32 color = makeColorRGB(255, 0, 0); - if ( player >= 0 ) + } + + if ( spell->ID == SPELL_HOLY_BEAM ) + { + if ( hit.entity->isSmiteWeakMonster() ) { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(392)); + magicDmg *= 2; } } - else + if ( !strcmp(element->element_internal_name, spellElement_cold.element_internal_name) + || spell->ID == SPELL_FOCI_SNOW ) { - if ( parent ) + real_t coldMultiplier = 1.0; + if ( hitstats && hitstats->helmet && hitstats->helmet->type == HAT_WARM ) { - Uint32 color = makeColorRGB(255, 0, 0); - if ( parent->behavior == &actPlayer ) + if ( !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2905), Language::get(2906), MSG_COMBAT); + if ( hitstats->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(hitstats) ) + { + coldMultiplier = std::max(0.0, 0.5 - 0.25 * (abs(hitstats->helmet->beatitude))); + } + else + { + coldMultiplier = 0.50; + } } } + magicDmg *= coldMultiplier; } - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + if ( !strcmp(element->element_internal_name, spellElement_fire.element_internal_name) + || !strcmp(element->element_internal_name, "spell_element_flames") + || spell->ID == SPELL_FOCI_FIRE + || spell->ID == SPELL_BREATHE_FIRE + || !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR_SHOWER].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR].element_internal_name) ) + { + real_t fireMultiplier = 1.0; + //if ( hitstats->helmet && hitstats->helmet->type == HAT_WARM ) + //{ + // if ( !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) + // { + // if ( hitstats->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(hitstats) ) + // { + // fireMultiplier += 0.5; + // } + // else + // { + // fireMultiplier += 0.5 + 0.5 * abs(hitstats->helmet->beatitude); // cursed, extra fire damage + // } + // } + //} + if ( hitstats->type == DRYAD ) + { + fireMultiplier += 0.2; + if ( !hitstats->helmet && hitstats->getEffectActive(EFF_GROWTH) > 1 ) + { + int bonus = std::min(3, hitstats->getEffectActive(EFF_GROWTH) - 1); + fireMultiplier += 0.05; + } + } + if ( hitstats->type == SALAMANDER ) + { + if ( hitstats->getEffectActive(EFF_SALAMANDER_HEART) == 1 + || hitstats->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + fireMultiplier *= 0.25; + } + else + { + fireMultiplier *= 0.5; + } + } + magicDmg *= fireMultiplier; + } + } + } + + if ( spell->ID >= SPELL_SLIME_ACID && spell->ID <= SPELL_SLIME_METAL || spell->ID == SPELL_GREASE_SPRAY ) + { + preResistanceDamage = std::max(2, preResistanceDamage); + magicDmg = std::max(2, magicDmg); + } + else if ( spell->ID == SPELL_FOCI_FIRE || spell->ID == SPELL_FOCI_ARCS || spell->ID == SPELL_FOCI_SNOW + || spell->ID == SPELL_FOCI_NEEDLES || spell->ID == SPELL_FOCI_SANDBLAST + || spell->ID == SPELL_BREATHE_FIRE ) + { + preResistanceDamage = std::max(1, preResistanceDamage); + magicDmg = std::max(1, magicDmg); + } + else if ( !strcmp(element->element_internal_name, "spell_element_flames") ) + { + preResistanceDamage = std::max(2, preResistanceDamage); + magicDmg = std::max(element->getDamage(), magicDmg); + } + damageTmp = magicDmg; + preResistanceDamageTmp = preResistanceDamage; + } + const Sint32 preResistanceDamage = preResistanceDamageTmp; + const int damage = damageTmp; + + if ( hit.entity && hitstats && hitstats->getEffectActive(EFF_ABSORB_MAGIC) ) + { + Uint8 effectStrength = hitstats->getEffectActive(EFF_ABSORB_MAGIC); + if ( effectStrength >= 101 ) + { + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_STATUS, makeColorRGB(255, 0, 0), Language::get(6856)); } } + else + { + int increase = std::max(damage, getSpellDamageFromID(SPELL_ABSORB_MAGIC, hit.entity, nullptr, hit.entity)); + effectStrength = std::min(101, (int)(effectStrength + increase)); + hit.entity->setEffect(EFF_ABSORB_MAGIC, effectStrength, hitstats->EFFECTS_TIMERS[EFF_ABSORB_MAGIC], false, true, true); + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6857)); + } + + Entity* fx = createParticleAestheticOrbit(hit.entity, 1817, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE); + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + real_t tangent = atan2(my->y - hit.entity->y, my->x - hit.entity->x); + fx->yaw = tangent; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 0; + serverSpawnMiscParticlesAtLocation(fx->x, fx->y, fx->z, PARTICLE_EFFECT_NULL_PARTICLE, 1817, 0, fx->yaw * 256.0); + + magicOnSpellCastEvent(hit.entity, nullptr, parent, SPELL_ABSORB_MAGIC, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } } - else if (!strcmp(element->element_internal_name, spellElement_cold.element_internal_name)) + + if (!strcmp(element->element_internal_name, spellElement_force.element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_MERCURY_BOLT].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_LEAD_BOLT].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_SPORE_BOMB].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_MYCELIUM_BOMB].element_internal_name) + || spell->ID == SPELL_FOCI_ARCS + || spell->ID == SPELL_FOCI_SANDBLAST + || spell->ID == SPELL_FOCI_NEEDLES + || spell->ID == SPELL_FOCI_SNOW ) { - playSoundEntity(my, 197, 128); if (hit.entity) { - if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer) + bool doSound = true; + int customSoundVolume = 0; + if ( spell->ID == SPELL_FOCI_ARCS + || spell->ID == SPELL_FOCI_SANDBLAST + || spell->ID == SPELL_FOCI_NEEDLES + || spell->ID == SPELL_FOCI_SNOW ) { - real_t coldMultiplier = 1.0; - bool warmHat = false; - if ( hitstats->helmet && hitstats->helmet->type == HAT_WARM ) + if ( ticks - lastMagicSoundPlayed[spell->ID] < 10 ) { - if ( !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) + doSound = false; + } + else if ( ticks - lastMagicSoundPlayed[spell->ID] < 25 ) + { + doSound = false; + customSoundVolume = 32; + } + else + { + lastMagicSoundPlayed[spell->ID] = ticks; + } + } + + if ( mimic ) + { + hit.entity->chestHandleDamageMagic(damage, *my, parent, doSound); + if ( !doSound && customSoundVolume > 0 ) + { + playSoundEntity(hit.entity, 28, customSoundVolume); + } + if ( spell->ID == SPELL_SPORE_BOMB || spell->ID == SPELL_MYCELIUM_BOMB ) + { + floorMagicCreateSpores(hit.entity, my->x, my->y, parent, damage, spell->ID); + } + + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else if (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) + { + Entity* parent = uidToEntity(my->parent); + if ( doSound ) + { + playSoundEntity(hit.entity, 28, 128); + } + else if ( !doSound && customSoundVolume > 0 ) + { + playSoundEntity(hit.entity, 28, customSoundVolume); + } + + Sint32 oldHP = hitstats->HP; + hit.entity->modHP(-damage); + magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); + + int numGibs = damage / 2; + numGibs = std::min(5, numGibs); + if ( my->actmagicSpray == 2 ) + { + numGibs = std::min(1, numGibs); + } + for ( int i = 0; i < numGibs; ++i) //Spawn a gib for every two points of damage. + { + Entity* gib = spawnGib(hit.entity); + serverSpawnGibForClient(gib); + } + + if (parent) + { + parent->killedByMonsterObituary(hit.entity, true); + } + + + if ( spell->ID == SPELL_FOCI_SANDBLAST ) + { + if ( !hitstats->getEffectActive(EFF_KNOCKBACK) && hit.entity->setEffect(EFF_KNOCKBACK, true, element->duration, false) ) { - if ( hitstats->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(hitstats) ) + real_t pushbackMultiplier = 0.3; + if ( parent ) { - coldMultiplier = std::max(0.0, 0.5 - 0.25 * (abs(hitstats->helmet->beatitude))); + real_t dist = entityDist(parent, hit.entity); + if ( dist < TOUCHRANGE ) + { + pushbackMultiplier += 0.5; + } } - else + if ( hit.entity->behavior == &actMonster ) { - coldMultiplier = 0.50; + real_t tangent = atan2(hit.entity->y - my->y, hit.entity->x - my->x); + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackTangentDir = tangent; + hit.entity->monsterKnockbackUID = parent ? parent->getUID() : 0; } - warmHat = true; + else if ( hit.entity->behavior == &actPlayer ) + { + if ( !players[hit.entity->skill[2]]->isLocalPlayer() ) + { + hit.entity->monsterKnockbackVelocity = pushbackMultiplier; + hit.entity->monsterKnockbackTangentDir = my->yaw; + serverUpdateEntityFSkill(hit.entity, 11); + serverUpdateEntityFSkill(hit.entity, 9); + } + else + { + hit.entity->monsterKnockbackVelocity = pushbackMultiplier; + hit.entity->monsterKnockbackTangentDir = my->yaw; + } + } + } + } + else if ( spell->ID == SPELL_FOCI_SNOW ) + { + Sint32 duration = element->duration; + duration = convertResistancePointsToMagicValue(duration, resistance); + int prevDuration = hitstats->getEffectActive(EFF_SLOW) ? hitstats->EFFECTS_TIMERS[EFF_SLOW] : 0; + if ( hit.entity->setEffect(EFF_SLOW, true, + std::min(element->getDurationSecondary(), prevDuration + duration), false) ) + { + /*if ( prevDuration ) + { + playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + }*/ } } + else if ( spell->ID == SPELL_FOCI_ARCS ) + { + Sint32 duration = element->duration; + duration = convertResistancePointsToMagicValue(duration, resistance); - playSoundEntity(hit.entity, 28, 128); + int prevDuration = hitstats->getEffectActive(EFF_STATIC) ? hitstats->EFFECTS_TIMERS[EFF_STATIC] : 0; + int maxDuration = element->getDurationSecondary(); + bool updateClient = !hitstats->getEffectActive(EFF_STATIC); - if ( !warmHat ) - { - hitstats->EFFECTS[EFF_SLOW] = true; - hitstats->EFFECTS_TIMERS[EFF_SLOW] = (element->duration * (((element->mana) / static_cast(element->base_mana)) * element->overload_multiplier)); - hitstats->EFFECTS_TIMERS[EFF_SLOW] /= (1 + (int)resistance); + duration = std::min(prevDuration + duration, maxDuration); + Uint8 effectStrength = 1; + if ( duration >= 8 * element->duration ) + { + effectStrength = 3; + } + else if ( duration >= 4 * element->duration ) + { + effectStrength = 2; + } - // If the Entity hit is a Player, update their status to be Slowed - if ( hit.entity->behavior == &actPlayer ) + if ( hit.entity->setEffect(EFF_STATIC, effectStrength, duration, updateClient) ) { - serverUpdateEffects(hit.entity->skill[2]); + } } - - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //messagePlayer(0, "damage: %d", damage); - if ( my->actmagicIsOrbiting == 2 ) + else if ( spell->ID == SPELL_FOCI_NEEDLES ) { - if ( parent && my->actmagicOrbitCastFromSpell == 0 ) + Sint32 duration = element->duration; + duration = convertResistancePointsToMagicValue(duration, resistance); + int prevDuration = hitstats->getEffectActive(EFF_BLEEDING) ? hitstats->EFFECTS_TIMERS[EFF_BLEEDING] : 0; + if ( hit.entity->setEffect(EFF_BLEEDING, true, + std::min(element->getDurationSecondary(), prevDuration + duration), false) ) { - if ( parent->behavior == &actParticleDot ) + //playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + if ( parent ) { - damage = parent->skill[1]; + hitstats->bleedInflictedBy = parent->getUID(); } - else if ( parent->behavior == &actPlayer ) + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_MERCURY_BOLT].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_LEAD_BOLT].element_internal_name) ) + { + if ( hit.entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + real_t pushbackMultiplier = 0.6; + if ( parent ) { - Stat* playerStats = parent->getStats(); - if ( playerStats ) + real_t dist = entityDist(parent, hit.entity); + if ( dist < TOUCHRANGE ) { - int skillLVL = playerStats->getModifiedProficiency(PRO_ALCHEMY) / 20; - damage = (18 + skillLVL * 1.5); + pushbackMultiplier += 0.5; } } - else + if ( hit.entity->behavior == &actMonster ) { - damage = 18; + real_t tangent = atan2(hit.entity->y - my->y, hit.entity->x - my->x); + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackTangentDir = tangent; + hit.entity->monsterKnockbackUID = parent ? parent->getUID() : 0; + } + else if ( hit.entity->behavior == &actPlayer ) + { + if ( !players[hit.entity->skill[2]]->isLocalPlayer() ) + { + hit.entity->monsterKnockbackVelocity = pushbackMultiplier; + hit.entity->monsterKnockbackTangentDir = my->yaw; + serverUpdateEntityFSkill(hit.entity, 11); + serverUpdateEntityFSkill(hit.entity, 9); + } + else + { + hit.entity->monsterKnockbackVelocity = pushbackMultiplier; + hit.entity->monsterKnockbackTangentDir = my->yaw; + } } } - else if ( parent && my->actmagicOrbitCastFromSpell == 1 ) - { - // cast through amplify magic effect - damage /= 2; - } - else + + if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_MERCURY_BOLT].element_internal_name) ) { - damage = 18; + Sint32 duration = element->duration; + duration = convertResistancePointsToMagicValue(duration, resistance); + if ( hit.entity->setEffect(EFF_SLOW, true, duration, false) ) + { + playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + } + + duration = element->duration; + duration = convertResistancePointsToMagicValue(duration, resistance); + if ( hit.entity->setEffect(EFF_POISONED, true, duration, false) ) + { + hitstats->poisonKiller = my->parent; + } } - damage = damage - local_rng.rand() % ((damage / 8) + 1); + } - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - int oldHP = hitstats->HP; - Sint32 preResistanceDamage = damage; - damage *= damageMultiplier; - damage *= coldMultiplier; - damage /= (1 + (int)resistance); - if ( damage > 0 ) + if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_SPORE_BOMB].element_internal_name) ) { - hit.entity->modHP(-damage); - magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); - Entity* gib = spawnGib(hit.entity); - serverSpawnGibForClient(gib); - magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); - } + Sint32 duration = 6 * TICKS_PER_SECOND + 10; + duration = convertResistancePointsToMagicValue(duration, resistance); + if ( hit.entity->setEffect(EFF_SLOW, true, duration, false, true, false, false) ) + { + playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + } - // write the obituary - if ( parent ) + duration = 6 * TICKS_PER_SECOND + 10; + duration = convertResistancePointsToMagicValue(duration, resistance); + if ( hit.entity->setEffect(EFF_POISONED, true, duration, false, true, false, false) ) + { + hitstats->poisonKiller = my->parent; + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_MYCELIUM_BOMB].element_internal_name) ) { - parent->killedByMonsterObituary(hit.entity); + Sint32 duration = 6 * TICKS_PER_SECOND + 10; + duration = convertResistancePointsToMagicValue(duration, resistance); + if ( hit.entity->setEffect(EFF_SLOW, true, duration, false, true, false, false) ) + { + playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + } } // update enemy bar for attacker @@ -2629,1439 +3669,1080 @@ void actMagicMissile(Entity* my) //TODO: Verify this function. updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, false, dmgGib); } - if ( parent ) + + if ( hitstats->HP <= 0 && parent) { - Uint32 color = makeColorRGB(0, 255, 0); - if ( parent->behavior == &actPlayer ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(394), Language::get(393), MSG_COMBAT); - } + parent->awardXP( hit.entity, true, true ); + spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); } - Uint32 color = makeColorRGB(255, 0, 0); - if ( player >= 0 ) + } + else if (hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) + { + hit.entity->doorHandleDamageMagic(damage, *my, parent, true, doSound); + if ( !doSound && customSoundVolume > 0 ) { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(395)); + playSoundEntity(hit.entity, 28, customSoundVolume); } - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); - if ( oldHP > 0 && hitstats->HP <= 0 ) + if ( spell->ID == SPELL_SPORE_BOMB || spell->ID == SPELL_MYCELIUM_BOMB ) { - if ( parent ) - { - parent->awardXP(hit.entity, true, true); - if ( my->actmagicIsOrbiting == 2 && my->actmagicOrbitCastFromSpell == 0 && parent->behavior == &actPlayer ) - { - if ( hitstats->type == LICH || hitstats->type == LICH_ICE || hitstats->type == LICH_FIRE ) - { - if ( true/*client_classes[parent->skill[2]] == CLASS_BREWER*/ ) - { - steamAchievementClient(parent->skill[2], "BARONY_ACH_SECRET_WEAPON"); - } - } - steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_BOMBARDIER, STEAM_STAT_INT, 1); - } - if ( my->actmagicCastByTinkerTrap == 1 && parent->behavior == &actPlayer && hitstats->type == MINOTAUR ) - { - steamAchievementClient(parent->skill[2], "BARONY_ACH_TIME_TO_PLAN"); - } - spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); - } - else - { - if ( achievementObserver.checkUidIsFromPlayer(my->parent) >= 0 ) - { - steamAchievementClient(achievementObserver.checkUidIsFromPlayer(my->parent), "BARONY_ACH_TAKING_WITH"); - } - } + floorMagicCreateSpores(hit.entity, my->x, my->y, parent, damage, spell->ID); } + + my->removeLightField(); + list_RemoveNode(my->mynode); + return; } - } - } - else if (!strcmp(element->element_internal_name, spellElement_slow.element_internal_name)) - { - if (hit.entity) - { - if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer) + else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) { - playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); - hitstats->EFFECTS[EFF_SLOW] = true; - hitstats->EFFECTS_TIMERS[EFF_SLOW] = (element->duration * (((element->mana) / static_cast(element->base_mana)) * element->overload_multiplier)); - hitstats->EFFECTS_TIMERS[EFF_SLOW] /= (1 + (int)resistance); - - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); - - // If the Entity hit is a Player, update their status to be Slowed - if ( hit.entity->behavior == &actPlayer ) + hit.entity->colliderHandleDamageMagic(damage, *my, parent, true, doSound); + if ( !doSound && customSoundVolume > 0 ) { - serverUpdateEffects(hit.entity->skill[2]); + playSoundEntity(hit.entity, hit.entity->getColliderSfxOnHit() > 0 ? + hit.entity->getColliderSfxOnHit() : 28, customSoundVolume); + } + if ( spell->ID == SPELL_SPORE_BOMB || spell->ID == SPELL_MYCELIUM_BOMB ) + { + floorMagicCreateSpores(hit.entity, my->x, my->y, parent, damage, spell->ID); } - // update enemy bar for attacker - if ( parent ) + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else if ( hit.entity->behavior == &actChest ) + { + hit.entity->chestHandleDamageMagic(damage, *my, parent, doSound); + if ( !doSound && customSoundVolume > 0 ) { - Uint32 color = makeColorRGB(0, 255, 0); - if ( parent->behavior == &actPlayer ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(394), Language::get(393), MSG_COMBAT); - } + playSoundEntity(hit.entity, 28, customSoundVolume); } - Uint32 color = makeColorRGB(255, 0, 0); - if ( player >= 0 ) + if ( spell->ID == SPELL_SPORE_BOMB || spell->ID == SPELL_MYCELIUM_BOMB ) { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(395)); + floorMagicCreateSpores(hit.entity, my->x, my->y, parent, damage, spell->ID); + } + + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else if (hit.entity->behavior == &actFurniture ) + { + hit.entity->furnitureHandleDamageMagic(damage, *my, parent, true, doSound); + if ( !doSound && customSoundVolume > 0 ) + { + playSoundEntity(hit.entity, 28, customSoundVolume); } - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); } } } - else if (!strcmp(element->element_internal_name, spellElement_sleep.element_internal_name)) + else if (!strcmp(element->element_internal_name, spellElement_magicmissile.element_internal_name) + || spell->ID == SPELL_SCEPTER_BLAST + || spell->ID == SPELL_BLOOD_WAVES + || spell->ID == SPELL_HOLY_BEAM ) { - if (hit.entity) + if ( spell->ID == SPELL_HOLY_BEAM ) { - if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer) + bool doSound = true; + if ( ticks - lastMagicSoundPlayed[spell->ID] < 10 ) { - int effectDuration = 0; - if ( parent && parent->behavior == &actMagicTrapCeiling ) + doSound = false; + } + else + { + lastMagicSoundPlayed[spell->ID] = ticks; + } + if ( hit.entity && !(parent && parent->checkFriend(hit.entity)) ) + { + createParticleFociLight(hit.entity, SPELL_HOLY_BEAM, true); + if ( doSound ) { - effectDuration = 200 + local_rng.rand() % 150; // 4 seconds + 0 to 3 seconds. + playSoundEntity(hit.entity, 849, 128); } - else + } + /*else + { + createParticleFociLight(my, SPELL_HOLY_BEAM, true); + if ( doSound ) { - effectDuration = 600 + local_rng.rand() % 300; // 12 seconds + 0 to 6 seconds. - if ( hitstats ) - { - effectDuration = std::max(0, effectDuration - ((hitstats->CON % 10) * 50)); // reduce 1 sec every 10 CON. - } + playSoundEntity(my, 849, 128); } - effectDuration /= (1 + (int)resistance); - - bool magicTrapReapplySleep = true; - - if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + }*/ + } + else if ( spell->ID == SPELL_BLOOD_WAVES ) + { + playSoundEntity(my, 173, 128); + for ( int i = 0; i < 4; ++i ) + { + if ( Entity* gib = spawnGib(my, 5) ) { - if ( hitstats && hitstats->EFFECTS[EFF_ASLEEP] ) - { - // check to see if we're reapplying the sleep effect. - int preventSleepRoll = (local_rng.rand() % 4) - resistance; - if ( hit.entity->behavior == &actPlayer || (preventSleepRoll <= 0) ) - { - magicTrapReapplySleep = false; - //messagePlayer(0, "Target already asleep!"); - } - } - - int trapResist = hit.entity->getEntityBonusTrapResist(); - if ( trapResist > 0 ) - { - if ( local_rng.rand() % 100 < trapResist ) - { - magicTrapReapplySleep = false; - } - } + gib->sprite = 5; } - if ( magicTrapReapplySleep ) - { - if ( hit.entity->setEffect(EFF_ASLEEP, true, effectDuration, false) ) - { - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); - playSoundEntity(hit.entity, 174, 64); - hitstats->OLDHP = hitstats->HP; - if ( hit.entity->behavior == &actPlayer ) - { - serverUpdateEffects(hit.entity->skill[2]); - Uint32 color = makeColorRGB(255, 0, 0); - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(396)); - } - if ( parent ) - { - Uint32 color = makeColorRGB(0, 255, 0); - if ( parent->behavior == &actPlayer ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(398), Language::get(397), MSG_COMBAT); - } - } - } - else - { - if ( parent ) - { - Uint32 color = makeColorRGB(255, 0, 0); - if ( parent->behavior == &actPlayer ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2905), Language::get(2906), MSG_COMBAT); - } - } - } - } + Entity* fx = createParticleAestheticOrbit(nullptr, 283, 1.5 * TICKS_PER_SECOND + i * 10, PARTICLE_EFFECT_BLOOD_BUBBLE); + real_t dir = (local_rng.rand() % 360) * PI / 180.f; + fx->x = (hit.entity ? hit.entity->x : my->x) + 4.0 * cos(dir); + fx->y = (hit.entity ? hit.entity->y : my->y) + 4.0 * sin(dir); + fx->z = my->z - (local_rng.rand() % 5); + fx->flags[SPRITE] = true; + + fx->fskill[2] = 2 * PI * (local_rng.rand() % 10) / 10.0; + fx->fskill[3] = 0.025; // speed osc + fx->scalex = 0.0125; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 2; + fx->actmagicOrbitStationaryX = my->x; + fx->actmagicOrbitStationaryY = my->y; + } + if ( hit.entity ) + { spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + serverSpawnMiscParticlesAtLocation(hit.entity->x, hit.entity->y, my->z, PARTICLE_EFFECT_BLOOD_BUBBLE, 283); + } + else + { + spawnMagicEffectParticles(my->x, my->y, my->z, my->sprite); + serverSpawnMiscParticlesAtLocation(my->x, my->y, my->z, PARTICLE_EFFECT_BLOOD_BUBBLE, 283); } } - } - else if (!strcmp(element->element_internal_name, spellElement_lightning.element_internal_name)) - { - playSoundEntity(my, 173, 128); + else if ( spell->ID == SPELL_SCEPTER_BLAST ) + { + spawnExplosionFromSprite(135, my->x, my->y, my->z); + } + else + { + spawnExplosion(my->x, my->y, my->z); + } if (hit.entity) { if ( mimic ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); hit.entity->chestHandleDamageMagic(damage, *my, parent); if ( my->actmagicProjectileArc > 0 ) { Entity* caster = uidToEntity(spell->caster); spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } - if ( !(my->actmagicIsOrbiting == 2) ) + + if ( spell->ID == SPELL_SCEPTER_BLAST || spell->ID == SPELL_BLOOD_WAVES || spell->ID == SPELL_HOLY_BEAM ) + { + if ( hit.entity->getStats() && hit.entity->getStats()->HP > 0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + else + { + my->collisionIgnoreTargets.insert(hit.entity->getUID()); + } + } + else if ( !(my->actmagicIsOrbiting == 2) ) { my->removeLightField(); list_RemoveNode(my->mynode); } + else + { + spawnExplosion(my->x, my->y, my->z); + } return; } else if (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) { Entity* parent = uidToEntity(my->parent); - playSoundEntity(my, 173, 64); - playSoundEntity(hit.entity, 28, 128); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - if ( my->actmagicIsOrbiting == 2 ) + + if ( spell->ID == SPELL_SCEPTER_BLAST || spell->ID == SPELL_BLOOD_WAVES || spell->ID == SPELL_HOLY_BEAM ) { - if ( parent && my->actmagicOrbitCastFromSpell == 0 ) + my->collisionIgnoreTargets.insert(hit.entity->getUID()); + particleTimerEmitterHitEntities[my->getUID()][my->getUID()].hits++; + if ( particleTimerEmitterHitEntities[my->getUID()][my->getUID()].hits == 2 ) { - if ( parent->behavior == &actParticleDot ) - { - damage = parent->skill[1]; - } - else if ( parent->behavior == &actPlayer ) + if ( spell->ID == SPELL_SCEPTER_BLAST ) { - Stat* playerStats = parent->getStats(); - if ( playerStats ) + if ( parent && parent->behavior == &actPlayer ) { - int skillLVL = playerStats->getModifiedProficiency(PRO_ALCHEMY) / 20; - damage = (22 + skillLVL * 1.5); + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_MULTI_HITS, MAGICSTAFF_SCEPTER, 1); } } - else - { - damage = 22; - } - } - else if ( parent && my->actmagicOrbitCastFromSpell == 1 ) - { - // cast through amplify magic effect - damage /= 2; - } - else - { - damage = 22; } - damage = damage - local_rng.rand() % ((damage / 8) + 1); } - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - int oldHP = hitstats->HP; - Sint32 preResistanceDamage = damage; - damage *= damageMultiplier; - damage /= (1 + (int)resistance); - hit.entity->modHP(-damage); - magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); - magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); - // write the obituary - if (parent) + if ( my->actmagicIsOrbiting == 2 ) { - parent->killedByMonsterObituary(hit.entity); + spawnExplosion(my->x, my->y, my->z); } - // update enemy bar for attacker - if ( !strcmp(hitstats->name, "") ) - { - updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, - false, dmgGib); - } - else - { - updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, - false, dmgGib); - } - if ( oldHP > 0 && hitstats->HP <= 0 && parent) + if ( spell->ID == SPELL_HOLY_BEAM && parent && parent->checkFriend(hit.entity) ) { - parent->awardXP( hit.entity, true, true ); - if ( my->actmagicIsOrbiting == 2 && my->actmagicOrbitCastFromSpell == 0 && parent->behavior == &actPlayer ) + Sint32 oldHP = hitstats->HP; + + int holyHeal = getSpellDamageSecondaryFromID(SPELL_HOLY_BEAM, parent, parent ? parent->getStats() : nullptr, my, (my->actmagicSpellbookBonus / 100.f)); + spell_changeHealth(hit.entity, holyHeal); + int heal = std::max(hitstats->HP - oldHP, 0); + if ( heal >= 0 ) { - if ( hitstats->type == LICH || hitstats->type == LICH_ICE || hitstats->type == LICH_FIRE ) + if ( heal > 0 ) { - if ( true/*client_classes[parent->skill[2]] == CLASS_BREWER*/ ) + spawnDamageGib(hit.entity, -heal, DamageGib::DMG_HEAL, DamageGibDisplayType::DMG_GIB_NUMBER, true); + } + playSoundEntity(hit.entity, 168, 128); + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 169); + + if ( parent && parent->behavior == &actPlayer && heal > 0 ) + { + serverUpdatePlayerGameplayStats(parent->skill[2], STATISTICS_HEAL_BOT, heal); + players[parent->skill[2]]->mechanics.updateSustainedSpellEvent(spell->ID, heal, 1.0, nullptr); + + if ( my->actmagicFromSpellbook ) { - steamAchievementClient(parent->skill[2], "BARONY_ACH_SECRET_WEAPON"); + auto find = ItemTooltips.spellItems.find(spell->ID); + if ( find != ItemTooltips.spellItems.end() ) + { + if ( find->second.spellbookId >= 0 && find->second.spellbookId < NUMITEMS && items[find->second.spellbookId].category == SPELLBOOK ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_HEAL, (ItemType)find->second.spellbookId, heal); + } + } + } + else + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_SPELL_HEAL, SPELL_ITEM, heal, false, spell->ID); } } - steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_BOMBARDIER, STEAM_STAT_INT, 1); } - spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); + } + else + { + playSoundEntity(hit.entity, 28, 128); + + Sint32 oldHP = hitstats->HP; + hit.entity->modHP(-damage); + magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); + magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); + int numGibs = damage / 2; + numGibs = std::min(5, numGibs); + for ( int i = 0; i < numGibs; ++i ) //Spawn a gib for every two points of damage. + { + Entity* gib = spawnGib(hit.entity); + serverSpawnGibForClient(gib); + } + + // write the obituary + if ( parent ) + { + parent->killedByMonsterObituary(hit.entity, true); + } + + // update enemy bar for attacker + if ( !strcmp(hitstats->name, "") ) + { + updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + else + { + updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + + if ( oldHP > 0 && hitstats->HP <= 0 && parent) + { + parent->awardXP( hit.entity, true, true ); + spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); + } } } - else if ( hit.entity->behavior == &actDoor ) + else if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - damage /= (1 + (int)resistance); - hit.entity->doorHandleDamageMagic(damage, *my, parent); if ( my->actmagicProjectileArc > 0 ) { Entity* caster = uidToEntity(spell->caster); spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } - if ( !(my->actmagicIsOrbiting == 2) ) + + if ( spell->ID == SPELL_SCEPTER_BLAST || spell->ID == SPELL_BLOOD_WAVES || spell->ID == SPELL_HOLY_BEAM ) { - my->removeLightField(); - list_RemoveNode(my->mynode); - } - return; + if ( hit.entity->doorHealth > 0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + else + { + my->collisionIgnoreTargets.insert(hit.entity->getUID()); + } + } + else if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + else + { + spawnExplosion(my->x, my->y, my->z); + } + return; } else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - damage /= (1 + (int)resistance); - hit.entity->colliderHandleDamageMagic(damage, *my, parent); if ( my->actmagicProjectileArc > 0 ) { Entity* caster = uidToEntity(spell->caster); spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } - if ( !(my->actmagicIsOrbiting == 2) ) + + if ( spell->ID == SPELL_SCEPTER_BLAST || spell->ID == SPELL_BLOOD_WAVES || spell->ID == SPELL_HOLY_BEAM ) + { + if ( hit.entity->colliderCurrentHP > 0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + else + { + my->collisionIgnoreTargets.insert(hit.entity->getUID()); + } + } + else if ( !(my->actmagicIsOrbiting == 2) ) { my->removeLightField(); list_RemoveNode(my->mynode); } + else + { + spawnExplosion(my->x, my->y, my->z); + } return; } else if ( hit.entity->behavior == &actChest ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); hit.entity->chestHandleDamageMagic(damage, *my, parent); if ( my->actmagicProjectileArc > 0 ) { Entity* caster = uidToEntity(spell->caster); spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } - if ( !(my->actmagicIsOrbiting == 2) ) + + if ( spell->ID == SPELL_SCEPTER_BLAST || spell->ID == SPELL_BLOOD_WAVES || spell->ID == SPELL_HOLY_BEAM ) + { + if ( hit.entity->chestHealth > 0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + else + { + my->collisionIgnoreTargets.insert(hit.entity->getUID()); + } + } + else if ( !(my->actmagicIsOrbiting == 2) ) { my->removeLightField(); list_RemoveNode(my->mynode); } + else + { + spawnExplosion(my->x, my->y, my->z); + } return; } else if (hit.entity->behavior == &actFurniture ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - int oldHP = hit.entity->furnitureHealth; - hit.entity->furnitureHealth -= damage; - if ( parent ) + hit.entity->furnitureHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) { - if ( parent->behavior == &actPlayer ) + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + + if ( spell->ID == SPELL_SCEPTER_BLAST || spell->ID == SPELL_BLOOD_WAVES || spell->ID == SPELL_HOLY_BEAM ) + { + if ( hit.entity->furnitureHealth > 0 ) { - bool destroyed = oldHP > 0 && hit.entity->furnitureHealth <= 0; - if ( destroyed ) - { - gameModeManager.currentSession.challengeRun.updateKillEvent(hit.entity); - } - switch ( hit.entity->furnitureType ) - { - case FURNITURE_CHAIR: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(388)); - } - updateEnemyBar(parent, hit.entity, Language::get(677), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_TABLE: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(389)); - } - updateEnemyBar(parent, hit.entity, Language::get(676), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2505)); - } - updateEnemyBar(parent, hit.entity, Language::get(2505), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BUNKBED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2506)); - } - updateEnemyBar(parent, hit.entity, Language::get(2506), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_PODIUM: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2507)); - } - updateEnemyBar(parent, hit.entity, Language::get(2507), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - default: - break; - } + my->removeLightField(); + list_RemoveNode(my->mynode); + } + else + { + my->collisionIgnoreTargets.insert(hit.entity->getUID()); } } - playSoundEntity(hit.entity, 28, 128); - if ( my->actmagicProjectileArc > 0 ) + else if ( !(my->actmagicIsOrbiting == 2) ) { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + my->removeLightField(); + list_RemoveNode(my->mynode); + } + else + { + spawnExplosion(my->x, my->y, my->z); } + return; } } } - else if ( spell->ID >= SPELL_SLIME_ACID && spell->ID <= SPELL_SLIME_METAL ) + else if (!strcmp(element->element_internal_name, spellElement_fire.element_internal_name) + || !strcmp(element->element_internal_name, "spell_element_flames") + || !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR_SHOWER].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR].element_internal_name) + || spell->ID == SPELL_FOCI_FIRE + || spell->ID == SPELL_BREATHE_FIRE ) { - if ( hit.entity ) + bool explode = !strcmp(element->element_internal_name, spellElement_fire.element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR_SHOWER].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR].element_internal_name); + if ( !(my->actmagicIsOrbiting == 2) ) { - int volume = 128; - static ConsoleVariable cvar_slimehit_sfx("/slimehit_sfx", 173); - int hitsfx = *cvar_slimehit_sfx; - int hitvolume = (particleEmitterHitProps && particleEmitterHitProps->hits == 1) ? 128 : 64; - if ( spell->ID == SPELL_SLIME_WATER || spell->ID == SPELL_SLIME_TAR ) + if ( explode ) { - hitsfx = 665; + spawnExplosion(my->x, my->y, my->z); } - if ( particleEmitterHitProps && particleEmitterHitProps->hits > 1 ) + } + if (hit.entity) + { + bool doSound = true; + int customSoundVolume = 0; + if ( spell->ID == SPELL_FOCI_FIRE + || spell->ID == SPELL_BREATHE_FIRE ) { - volume = 0; - if ( spell->ID == SPELL_SLIME_WATER || spell->ID == SPELL_SLIME_TAR ) + if ( ticks - lastMagicSoundPlayed[spell->ID] < 10 ) { - hitvolume = 32; + doSound = false; + } + else if ( ticks - lastMagicSoundPlayed[spell->ID] < 25 ) + { + doSound = false; + customSoundVolume = 32; + } + else + { + lastMagicSoundPlayed[spell->ID] = ticks; } } - bool hasgoggles = false; - if ( hitstats && hitstats->mask && hitstats->mask->type == MASK_HAZARD_GOGGLES ) + // Attempt to set the Entity on fire + int prevBurningCounter = hit.entity->flags[BURNING] ? hit.entity->char_fire : 0; + hit.entity->SetEntityOnFire((parent && parent->getStats()) ? parent : nullptr); + if ( hit.entity->flags[BURNING] + && (prevBurningCounter == 0 || (spell->ID == SPELL_FOCI_FIRE || spell->ID == SPELL_BREATHE_FIRE)) ) { - if ( !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) + if ( (parent && parent->behavior == &actPlayer) || spell->ID == SPELL_FLAMES ) { - hasgoggles = true; + if ( spell->ID == SPELL_FOCI_FIRE + || spell->ID == SPELL_BREATHE_FIRE ) + { + hit.entity->char_fire = std::min(element->getDurationSecondary(), prevBurningCounter + element->duration); + } + else + { + hit.entity->char_fire = std::min(hit.entity->char_fire, element->duration); + } + } + } + + if ( !strcmp(element->element_internal_name, "spell_element_flames") ) + { + if ( Entity* fx = createParticleAestheticOrbit(hit.entity, 233, TICKS_PER_SECOND / 2, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->x = hit.entity->x; + fx->y = hit.entity->y; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->vel_z = -0.05; + fx->actmagicOrbitDist = 2; + fx->fskill[2] = hit.entity->yaw + (local_rng.rand() % 8) * PI / 4.0; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + + serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_FLAMES, 233, 0, fx->skill[0]); } } if ( mimic ) { - playSoundEntity(hit.entity, hitsfx, hitvolume); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - damage = std::max(2, damage); - hit.entity->chestHandleDamageMagic(damage, *my, parent); + hit.entity->chestHandleDamageMagic(damage, *my, parent, doSound); + if ( !doSound && customSoundVolume > 0 ) + { + playSoundEntity(hit.entity, 28, customSoundVolume); + } if ( my->actmagicProjectileArc > 0 ) { Entity* caster = uidToEntity(spell->caster); spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } + if ( (spell->ID == SPELL_METEOR || spell->ID == SPELL_METEOR_SHOWER) + && explode ) + { + Entity* caster = uidToEntity(spell->caster); + if ( Entity* aoe = createSpellExplosionArea(spell->ID, caster, my->x, my->y, my->z, 32.0, preResistanceDamage, hit.entity) ) + { + aoe->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + aoe->actmagicFromSpellbook = my->actmagicFromSpellbook; + } + } if ( !(my->actmagicIsOrbiting == 2) ) { my->removeLightField(); list_RemoveNode(my->mynode); } - if ( spell->ID == SPELL_SLIME_FIRE ) + else { - hit.entity->SetEntityOnFire(); + if ( explode ) + { + spawnExplosion(my->x, my->y, my->z); + } } return; } - else if ( hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer ) + else if (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) { - playSoundEntity(hit.entity, hitsfx, hitvolume); - Entity* parent = uidToEntity(my->parent); - playSoundEntity(hit.entity, 28, volume); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - if ( spell->ID == SPELL_SLIME_WATER && hitstats->type == VAMPIRE ) + //playSoundEntity(my, 153, 64); + if ( doSound ) { - damage *= 2; + playSoundEntity(hit.entity, 28, 128); + } + else if ( !doSound && customSoundVolume > 0 ) + { + playSoundEntity(hit.entity, 28, customSoundVolume); + } + //TODO: Apply fire resistances/weaknesses. + if ( my->actmagicIsOrbiting == 2 ) + { + if ( explode ) + { + spawnExplosion(my->x, my->y, my->z); + } } - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - int oldHP = hitstats->HP; - Sint32 preResistanceDamage = damage; - damage *= damageMultiplier; - damage /= (1 + (int)resistance); - - if ( spell->ID == SPELL_SLIME_FIRE ) + if ( spell->ID == SPELL_BREATHE_FIRE ) { - if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + Stat* parentStats = parent ? parent->getStats() : nullptr; + Uint8 effectStrength = (hitstats->getEffectActive(EFF_DIVINE_FIRE) & 0xF); + int maxStrength = 10; + if ( parentStats ) { - if ( parent && parent->behavior == &actPlayer ) + int minStrength = 2; + if ( parentStats->type == SALAMANDER + && parentStats->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) { - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6239), Language::get(6238), MSG_COMBAT); + minStrength += 3; } - if ( hit.entity->behavior == &actPlayer ) + maxStrength = std::min(maxStrength, minStrength + std::max(0, statGetCHR(parent->getStats(), parent)) / 5); + } + if ( effectStrength < maxStrength ) + { + effectStrength += 1; + } + int duration = std::min(element->getDurationSecondary(), hitstats->EFFECTS_TIMERS[EFF_DIVINE_FIRE] + + element->duration); + if ( parent ) + { + if ( parent->behavior == &actPlayer ) { - Uint32 color = makeColorRGB(255, 0, 0); - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6237)); + effectStrength |= (1 + parent->skill[2]) << 4; } - if ( hasgoggles ) + else if ( parent->behavior == &actMonster && parent->monsterAllyGetPlayerLeader() ) { - if ( hit.entity->behavior == &actPlayer ) - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); - } + effectStrength |= (1 + parent->monsterAllyGetPlayerLeader()->skill[2]) << 4; } } - - if ( !hasgoggles ) + if ( hit.entity->setEffect(EFF_DIVINE_FIRE, effectStrength, duration, false, true, true) ) { - hit.entity->SetEntityOnFire(); + if ( parentStats && parentStats->type == SALAMANDER ) + { + int mpAmount = parent->modMP(1 + (effectStrength & 0xF) / 4); + parent->playerInsectoidIncrementHungerToMP(mpAmount); + } } } - if ( spell->ID == SPELL_SLIME_TAR ) + + int oldHP = hitstats->HP; + hit.entity->modHP(-damage); + magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); + //for (i = 0; i < damage; i += 2) { //Spawn a gib for every two points of damage. + Entity* gib = spawnGib(hit.entity); + serverSpawnGibForClient(gib); + //} + + // write the obituary + if ( parent ) { - if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + if ( my->actmagicIsOrbiting == 2 + && parent->behavior == &actParticleDot + && parent->skill[1] > 0 ) { - if ( parent && parent->behavior == &actPlayer ) + if ( hitstats && !strcmp(hitstats->obituary, Language::get(3898)) ) { - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6244), Language::get(6243), MSG_COMBAT); + // was caused by a flaming boulder. + hit.entity->setObituary(Language::get(3898)); + hitstats->killer = KilledBy::BOULDER; } - if ( hit.entity->behavior == &actPlayer ) + else { - Uint32 color = makeColorRGB(255, 0, 0); - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6236)); + // blew the brew (alchemy) + hit.entity->setObituary(Language::get(3350)); + hitstats->killer = KilledBy::FAILED_ALCHEMY; } } + else + { + parent->killedByMonsterObituary(hit.entity, true); + } + } + if ( hitstats ) + { + hitstats->burningInflictedBy = static_cast(my->parent); + } - if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + // update enemy bar for attacker + if ( !strcmp(hitstats->name, "") ) + { + updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + else + { + updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + if ( oldHP > 0 && hitstats->HP <= 0 ) + { + if ( parent ) { - if ( local_rng.rand() % 2 == 0 ) + if ( my->actmagicIsOrbiting == 2 && my->actmagicOrbitCastFromSpell == 0 && parent->behavior == &actPlayer ) { - int duration = 6 * TICKS_PER_SECOND; - duration /= (1 + (int)resistance); - - int status = hit.entity->behavior == &actPlayer ? EFF_MESSY : EFF_BLIND; - - if ( hasgoggles ) - { - if ( hit.entity->behavior == &actPlayer ) - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); - } - } - else if ( hit.entity->setEffect(status, true, duration / 2, false) ) + if ( hitstats->type == LICH || hitstats->type == LICH_ICE || hitstats->type == LICH_FIRE ) { - if ( hit.entity->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(255, 0, 0); - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(765)); - } - if ( parent && parent->behavior == &actPlayer ) + if ( true/*client_classes[parent->skill[2]] == CLASS_BREWER*/ ) { - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(3878), Language::get(3879), MSG_COMBAT); + steamAchievementClient(parent->skill[2], "BARONY_ACH_SECRET_WEAPON"); } } + steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_BOMBARDIER, STEAM_STAT_INT, 1); } - else + if ( my->actmagicCastByTinkerTrap == 1 && parent->behavior == &actPlayer && hitstats->type == MINOTAUR ) { - int duration = 10 * TICKS_PER_SECOND; - duration /= (1 + (int)resistance); + steamAchievementClient(parent->skill[2], "BARONY_ACH_TIME_TO_PLAN"); + } - if ( hasgoggles ) + if ( spell->ID == SPELL_BREATHE_FIRE && hitstats ) + { + if ( parent && parent->behavior == &actPlayer ) { - if ( hit.entity->behavior == &actPlayer ) + if ( (hitstats->type == SLIME && hitstats->getAttribute("slime_type") == "slime red") + || hitstats->type == LICH_FIRE + || hitstats->type == DEVIL + || hitstats->type == DEMON + || hitstats->type == CREATURE_IMP ) { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); - } - } - else if ( hit.entity->setEffect(EFF_GREASY, true, duration, false) ) - { - Uint32 color = makeColorRGB(255, 0, 0); - if ( hit.entity->behavior == &actPlayer ) - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6245)); + if ( Stat* parentStats = parent->getStats() ) + { + if ( parentStats->playerRace == RACE_SALAMANDER && parentStats->stat_appearance == 0 ) + { + steamAchievementClient(parent->skill[2], "BARONY_ACH_FIGHT_FIRE_WITH"); + } + } } } } - } - if ( !hit.entity->isBlind() ) - { - if ( alertTarget && hit.entity->monsterState != MONSTER_STATE_ATTACK && (hitstats->type < LICH || hitstats->type >= SHOPKEEPER) ) - { - hit.entity->monsterAcquireAttackTarget(*parent, MONSTER_STATE_PATH, true); - } + parent->awardXP( hit.entity, true, true ); + spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); } else { - alertTarget = false; - hit.entity->monsterReleaseAttackTarget(); - } - hit.entity->updateEntityOnHit(parent, alertTarget); - } - if ( spell->ID == SPELL_SLIME_ACID || spell->ID == SPELL_SLIME_METAL ) - { - bool hasamulet = false; - if ( (hitstats->amulet && hitstats->amulet->type == AMULET_POISONRESISTANCE) || hitstats->type == INSECTOID ) - { - resistance += 2; - hasamulet = true; - } - if ( hasgoggles ) - { - resistance += 2; - } - - int duration = (spell->ID == SPELL_SLIME_METAL ? 10 : 6) * TICKS_PER_SECOND; - duration /= (1 + (int)resistance); - if ( spell->ID == SPELL_SLIME_ACID ) - { - if ( !hasamulet && !hasgoggles ) - { - if ( hit.entity->setEffect(EFF_POISONED, true, duration, false) ) - { - hitstats->poisonKiller = my->parent; - } - } - } - - if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) - { - if ( parent && parent->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(0, 255, 0); - if ( spell->ID == SPELL_SLIME_METAL ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6241), Language::get(6240), MSG_COMBAT); - } - else - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2431), Language::get(2430), MSG_COMBAT); - } - } - if ( hit.entity->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(255, 0, 0); - if ( spell->ID == SPELL_SLIME_METAL ) - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6242)); - } - else - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(2432)); - } - if ( hasgoggles ) - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); - } - } - } - - if ( spell->ID == SPELL_SLIME_METAL ) - { - if ( !hasgoggles && hit.entity->setEffect(EFF_SLOW, true, duration, false) ) - { - if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) - { - playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); - if ( parent && parent->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(394), Language::get(393), MSG_COMBAT); - } - if ( hit.entity->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(255, 0, 0); - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(395)); - } - } - } - } - - if ( hitstats->HP > 0 && !hasgoggles ) - { - // damage armor - Item* armor = nullptr; - int armornum = -1; - int acidChance = spell->ID == SPELL_SLIME_METAL ? 2 : 4; - if ( hitstats->defending && (local_rng.rand() % ((acidChance * 2) + resistance) == 0) ) // 1 in 8 to corrode shield - { - armornum = hitstats->pickRandomEquippedItem(&armor, true, false, true, true); - } - else if ( !hitstats->defending && (local_rng.rand() % (acidChance + resistance) == 0) ) // 1 in 4 to corrode armor - { - armornum = hitstats->pickRandomEquippedItem(&armor, true, false, false, false); - } - if ( armornum != -1 && armor != nullptr ) + if ( achievementObserver.checkUidIsFromPlayer(my->parent) >= 0 ) { - hit.entity->degradeArmor(*hitstats, *armor, armornum); + steamAchievementClient(achievementObserver.checkUidIsFromPlayer(my->parent), "BARONY_ACH_TAKING_WITH"); } } } - else if ( spell->ID == SPELL_SLIME_WATER ) + } + else if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) + { + hit.entity->doorHandleDamageMagic(damage, *my, parent, true, doSound); + if ( !doSound && customSoundVolume > 0 ) { - if ( !hasgoggles && hit.entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) - { - real_t pushbackMultiplier = 0.6; - if ( hit.entity->behavior == &actMonster ) - { - if ( !hit.entity->isMobile() ) - { - pushbackMultiplier += 0.3; - } - if ( parent ) - { - real_t tangent = atan2(hit.entity->y - parent->y, hit.entity->x - parent->x); - hit.entity->vel_x = cos(tangent) * pushbackMultiplier; - hit.entity->vel_y = sin(tangent) * pushbackMultiplier; - hit.entity->monsterKnockbackVelocity = 0.01; - hit.entity->monsterKnockbackUID = my->parent; - hit.entity->monsterKnockbackTangentDir = tangent; - } - else - { - real_t tangent = atan2(hit.entity->y - my->y, hit.entity->x - my->x); - hit.entity->vel_x = cos(tangent) * pushbackMultiplier; - hit.entity->vel_y = sin(tangent) * pushbackMultiplier; - hit.entity->monsterKnockbackVelocity = 0.01; - hit.entity->monsterKnockbackTangentDir = tangent; - } - } - else if ( hit.entity->behavior == &actPlayer ) - { - /*if ( parent ) - { - real_t dist = entityDist(parent, hit.entity); - if ( dist < TOUCHRANGE ) - { - pushbackMultiplier += 0.5; - } - }*/ - if ( !players[hit.entity->skill[2]]->isLocalPlayer() ) - { - hit.entity->monsterKnockbackVelocity = pushbackMultiplier; - hit.entity->monsterKnockbackTangentDir = my->yaw; - serverUpdateEntityFSkill(hit.entity, 11); - serverUpdateEntityFSkill(hit.entity, 9); - } - else - { - hit.entity->monsterKnockbackVelocity = pushbackMultiplier; - hit.entity->monsterKnockbackTangentDir = my->yaw; - } - } - } - - if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) - { - if ( parent && parent->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(3215), Language::get(3214), MSG_COMBAT); - } - if ( hit.entity->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(255, 0, 0); - if ( !hasgoggles ) - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6235)); - } - else - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6309)); - } - - if ( hitstats->type == VAMPIRE ) - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(644)); - } - else if ( hasgoggles ) - { - messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); - } - } - } + playSoundEntity(hit.entity, 28, customSoundVolume); } - - damage = std::max(2, damage); - hit.entity->modHP(-damage); - magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); - magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); - - // write the obituary - if ( parent ) + if ( my->actmagicProjectileArc > 0 ) { - parent->killedByMonsterObituary(hit.entity); + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } - - // update enemy bar for attacker - if ( !strcmp(hitstats->name, "") ) + if ( (spell->ID == SPELL_METEOR || spell->ID == SPELL_METEOR_SHOWER) + && explode ) { - updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, - false, dmgGib); + Entity* caster = uidToEntity(spell->caster); + if ( Entity* aoe = createSpellExplosionArea(spell->ID, caster, my->x, my->y, my->z, 32.0, preResistanceDamage, hit.entity) ) + { + aoe->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + aoe->actmagicFromSpellbook = my->actmagicFromSpellbook; + } } - else + if ( !(my->actmagicIsOrbiting == 2) ) { - updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, - false, dmgGib); + my->removeLightField(); + list_RemoveNode(my->mynode); } - if ( oldHP > 0 && hitstats->HP <= 0 && parent ) + else { - parent->awardXP(hit.entity, true, true); - spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); + if ( explode ) + { + spawnExplosion(my->x, my->y, my->z); + } } - } - else if ( hit.entity->behavior == &actDoor ) + return; + } + else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) { - if ( spell->ID == SPELL_SLIME_FIRE ) + hit.entity->colliderHandleDamageMagic(damage, *my, parent, true, doSound); + if ( !doSound && customSoundVolume > 0 ) { - hit.entity->SetEntityOnFire(); + playSoundEntity(hit.entity, hit.entity->getColliderSfxOnHit() > 0 ? + hit.entity->getColliderSfxOnHit() : 28, customSoundVolume); } - playSoundEntity(hit.entity, hitsfx, hitvolume); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - damage /= (1 + (int)resistance); - - hit.entity->doorHandleDamageMagic(damage, *my, parent); if ( my->actmagicProjectileArc > 0 ) { Entity* caster = uidToEntity(spell->caster); spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } + if ( (spell->ID == SPELL_METEOR || spell->ID == SPELL_METEOR_SHOWER) + && explode ) + { + Entity* caster = uidToEntity(spell->caster); + if ( Entity* aoe = createSpellExplosionArea(spell->ID, caster, my->x, my->y, my->z, 32.0, preResistanceDamage, hit.entity) ) + { + aoe->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + aoe->actmagicFromSpellbook = my->actmagicFromSpellbook; + } + } if ( !(my->actmagicIsOrbiting == 2) ) { my->removeLightField(); list_RemoveNode(my->mynode); } + else + { + if ( explode ) + { + spawnExplosion(my->x, my->y, my->z); + } + } return; } - else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) + else if (hit.entity->behavior == &actChest) { - if ( spell->ID == SPELL_SLIME_FIRE ) + hit.entity->chestHandleDamageMagic(damage, *my, parent, doSound); + if ( !doSound && customSoundVolume > 0 ) { - hit.entity->SetEntityOnFire(); + playSoundEntity(hit.entity, 28, customSoundVolume); } - playSoundEntity(hit.entity, hitsfx, hitvolume); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - damage /= (1 + (int)resistance); - - hit.entity->colliderHandleDamageMagic(damage, *my, parent); if ( my->actmagicProjectileArc > 0 ) { Entity* caster = uidToEntity(spell->caster); spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } + if ( (spell->ID == SPELL_METEOR || spell->ID == SPELL_METEOR_SHOWER) + && explode ) + { + Entity* caster = uidToEntity(spell->caster); + if ( Entity* aoe = createSpellExplosionArea(spell->ID, caster, my->x, my->y, my->z, 32.0, preResistanceDamage, hit.entity) ) + { + aoe->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + aoe->actmagicFromSpellbook = my->actmagicFromSpellbook; + } + } if ( !(my->actmagicIsOrbiting == 2) ) { my->removeLightField(); list_RemoveNode(my->mynode); } + else + { + if ( explode ) + { + spawnExplosion(my->x, my->y, my->z); + } + } return; } - else if ( hit.entity->behavior == &actChest ) + else if (hit.entity->behavior == &actFurniture ) { - if ( spell->ID == SPELL_SLIME_FIRE ) + hit.entity->furnitureHandleDamageMagic(damage, *my, parent, true, doSound); + if ( !doSound && customSoundVolume > 0 ) { - hit.entity->SetEntityOnFire(); + playSoundEntity(hit.entity, 28, customSoundVolume); } - playSoundEntity(hit.entity, hitsfx, hitvolume); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->chestHandleDamageMagic(damage, *my, parent); if ( my->actmagicProjectileArc > 0 ) { Entity* caster = uidToEntity(spell->caster); spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } + if ( (spell->ID == SPELL_METEOR || spell->ID == SPELL_METEOR_SHOWER) + && explode ) + { + Entity* caster = uidToEntity(spell->caster); + if ( Entity* aoe = createSpellExplosionArea(spell->ID, caster, my->x, my->y, my->z, 32.0, preResistanceDamage, hit.entity) ) + { + aoe->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + aoe->actmagicFromSpellbook = my->actmagicFromSpellbook; + } + } if ( !(my->actmagicIsOrbiting == 2) ) { my->removeLightField(); list_RemoveNode(my->mynode); } - return; - } - else if ( hit.entity->behavior == &actFurniture ) - { - if ( spell->ID == SPELL_SLIME_FIRE ) - { - hit.entity->SetEntityOnFire(); - } - playSoundEntity(hit.entity, hitsfx, hitvolume); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - int oldHP = hit.entity->furnitureHealth; - hit.entity->furnitureHealth -= damage; - if ( parent ) + else { - if ( parent->behavior == &actPlayer ) + if ( explode ) { - bool destroyed = oldHP > 0 && hit.entity->furnitureHealth <= 0; - if ( destroyed ) - { - gameModeManager.currentSession.challengeRun.updateKillEvent(hit.entity); - } - switch ( hit.entity->furnitureType ) - { - case FURNITURE_CHAIR: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(388)); - } - updateEnemyBar(parent, hit.entity, Language::get(677), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_TABLE: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(389)); - } - updateEnemyBar(parent, hit.entity, Language::get(676), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2505)); - } - updateEnemyBar(parent, hit.entity, Language::get(2505), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BUNKBED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2506)); - } - updateEnemyBar(parent, hit.entity, Language::get(2506), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_PODIUM: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2507)); - } - updateEnemyBar(parent, hit.entity, Language::get(2507), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - default: - break; - } + spawnExplosion(my->x, my->y, my->z); } } - playSoundEntity(hit.entity, 28, volume); - if ( my->actmagicProjectileArc > 0 ) - { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } + return; } } } - else if ( !strcmp(element->element_internal_name, spellElement_ghostBolt.element_internal_name) ) + else if (!strcmp(element->element_internal_name, spellElement_confuse.element_internal_name)) { - if ( hit.entity ) + if (hit.entity) { - if ( (!mimic && hit.entity->behavior == &actMonster) ) + if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer) { - Entity* parent = uidToEntity(my->parent); - real_t pushbackMultiplier = 0.6;// +(0.2 * spellbookDamageBonus); - if ( !hit.entity->isMobile() ) - { - pushbackMultiplier += 0.3; - } - - bool doSlow = true; - const int duration = TICKS_PER_SECOND * 2; - if ( hitstats ) - { - if ( hitstats->EFFECTS[EFF_SLOW] || hitstats->EFFECTS_TIMERS[EFF_SLOW] > duration ) - { - doSlow = false; - } - } + int duration = element->duration; + duration = convertResistancePointsToMagicValue(duration, resistance); - if ( doSlow ) + if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) { - if ( hit.entity->setEffect(EFF_SLOW, true, duration, false) ) + if ( trapResist > 0 ) { - //playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); - if ( parent ) + if ( local_rng.rand() % 100 < trapResist ) { - Uint32 color = makeColorRGB(0, 255, 0); - if ( parent->behavior == &actPlayer || parent->behavior == &actDeathGhost ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(394), Language::get(393), MSG_COMBAT); - } + duration = 0; } - /*Uint32 color = makeColorRGB(255, 0, 0); - if ( player >= 0 ) - { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(395)); - }*/ } } - if ( hit.entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + Uint8 effectStrength = MAXPLAYERS + 1; + if ( parent && parent->behavior == &actPlayer ) { - if ( parent ) - { - real_t tangent = atan2(hit.entity->y - parent->y, hit.entity->x - parent->x); - hit.entity->vel_x = cos(tangent) * pushbackMultiplier; - hit.entity->vel_y = sin(tangent) * pushbackMultiplier; - hit.entity->monsterKnockbackVelocity = 0.01; - hit.entity->monsterKnockbackUID = my->parent; - hit.entity->monsterKnockbackTangentDir = tangent; - //hit.entity->lookAtEntity(*parent); - } - else - { - real_t tangent = atan2(hit.entity->y - my->y, hit.entity->x - my->x); - hit.entity->vel_x = cos(tangent) * pushbackMultiplier; - hit.entity->vel_y = sin(tangent) * pushbackMultiplier; - hit.entity->monsterKnockbackVelocity = 0.01; - hit.entity->monsterKnockbackTangentDir = tangent; - hit.entity->monsterKnockbackUID = 0; - //hit.entity->lookAtEntity(*my); - } + effectStrength = parent->skill[2] + 1; } - /*if ( hit.entity->monsterAttack == 0 ) - { - hit.entity->monsterHitTime = std::max(HITRATE - 12, hit.entity->monsterHitTime); - }*/ - } - else - { - //if ( parent ) - //{ - // if ( parent->behavior == &actPlayer || parent->behavior == &actDeathGhost ) - // { - // messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(401)); // "No telling what it did..." - // } - //} - } - - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); - } - } - else if (!strcmp(element->element_internal_name, spellElement_locking.element_internal_name)) - { - if ( hit.entity ) - { - if (hit.entity->behavior == &actDoor) - { - if ( parent && parent->behavior == &actPlayer && MFLAG_DISABLEOPENING ) + else if ( parent && parent->monsterAllyGetPlayerLeader() ) { - Uint32 color = makeColorRGB(255, 0, 255); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3097)); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3101)); // disabled locking spell. + effectStrength = parent->monsterAllyGetPlayerLeader()->skill[2] + 1; } - else + if ( duration > 0 && hit.entity->setEffect(EFF_CONFUSED, effectStrength, duration, true, true, true) ) { - playSoundEntity(hit.entity, 92, 64); - hit.entity->skill[5] = 1; //Lock the door. + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); + playSoundEntity(hit.entity, 174, 64); + if ( hit.entity->behavior == &actMonster ) + { + hit.entity->monsterTarget = 0; // monsters forget what they're doing + } if ( parent ) { + Uint32 color = makeColorRGB(0, 255, 0); if ( parent->behavior == &actPlayer ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(399)); - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(391), Language::get(390), MSG_COMBAT); } } - } - } - else if (hit.entity->behavior == &actChest) - { - //Lock chest - playSoundEntity(hit.entity, 92, 64); - if ( !hit.entity->chestLocked ) - { - if ( parent && parent->behavior == &actPlayer && MFLAG_DISABLEOPENING ) - { - Uint32 color = makeColorRGB(255, 0, 255); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3099)); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3100)); // disabled locking spell. - } - else + Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) { - hit.entity->lockChest(); - if ( parent ) - { - if ( parent->behavior == &actPlayer ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(400)); - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - } - } + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(392)); } } - } - else if ( hit.entity->behavior == &actMonster && hit.entity->getMonsterTypeFromSprite() == MIMIC ) - { - //Lock chest - playSoundEntity(hit.entity, 92, 64); - - if ( hitstats ) + else { - //if ( MFLAG_DISABLEOPENING ) - //{ - // if ( parent && parent->behavior == &actPlayer ) - // { - // Uint32 color = makeColorRGB(255, 0, 255); - // messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3099)); - // messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3100)); // disabled locking spell. - // } - //} - //else + if ( parent ) { - if ( parent && parent->behavior == &actPlayer ) - { - if ( mimic ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(400)); - } - else - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(6083)); - } - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - } - if ( !hitstats->EFFECTS[EFF_MIMIC_LOCKED] ) + Uint32 color = makeColorRGB(255, 0, 0); + if ( parent->behavior == &actPlayer ) { - if ( hit.entity->setEffect(EFF_MIMIC_LOCKED, true, TICKS_PER_SECOND * 5, false) ) - { - hit.entity->monsterHitTime = HITRATE - 2; - hitstats->monsterMimicLockedBy = parent ? parent->getUID() : 0; - } + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2905), Language::get(2906), MSG_COMBAT); } } } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); } - else - { - if ( parent ) - { - if ( parent->behavior == &actPlayer ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(401)); - } - } - if ( player >= 0 ) - { - messagePlayer(player, MESSAGE_COMBAT, Language::get(401)); - } - } - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); } } - else if (!strcmp(element->element_internal_name, spellElement_opening.element_internal_name)) + else if (!strcmp(element->element_internal_name, spellElement_cold.element_internal_name)) { + playSoundEntity(my, 197, 128); if (hit.entity) { - if (hit.entity->behavior == &actDoor) + if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer) { - if ( MFLAG_DISABLEOPENING || hit.entity->doorDisableOpening == 1 ) + bool warmHat = false; + if ( hitstats->helmet && hitstats->helmet->type == HAT_WARM ) { - if ( parent && parent->behavior == &actPlayer ) + if ( !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) { - Uint32 color = makeColorRGB(255, 0, 255); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3097)); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3101)); // disabled opening spell. + warmHat = true; } } - else + + playSoundEntity(hit.entity, 28, 128); + + if ( !warmHat ) { - // Open the Door - playSoundEntity(hit.entity, 91, 64); // "UnlockDoor.ogg" - if ( hit.entity->doorLocked ) - { - if ( parent && parent->behavior == &actPlayer ) - { - Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_DOOR_UNLOCKED, "door", 1); - } - } - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - hit.entity->doorLocked = 0; // Unlocks the Door - hit.entity->doorPreventLockpickExploit = 1; + hitstats->setEffectActive(EFF_SLOW, 1); + hitstats->EFFECTS_TIMERS[EFF_SLOW] = convertResistancePointsToMagicValue(element->duration, resistance); - if ( !hit.entity->skill[0] && !hit.entity->skill[3] ) - { - hit.entity->skill[3] = 1 + (my->x > hit.entity->x); // Opens the Door - playSoundEntity(hit.entity, 21, 96); // "UnlockDoor.ogg" - } - else if ( hit.entity->skill[0] && !hit.entity->skill[3] ) - { - hit.entity->skill[3] = 1 + (my->x < hit.entity->x); // Opens the Door - playSoundEntity(hit.entity, 21, 96); // "UnlockDoor.ogg" - } - if ( parent ) + // If the Entity hit is a Player, update their status to be Slowed + if ( hit.entity->behavior == &actPlayer ) { - if ( parent->behavior == &actPlayer) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(402)); - } + serverUpdateEffects(hit.entity->skill[2]); } } - } - else if ( hit.entity->behavior == &actGate ) - { - if ( MFLAG_DISABLEOPENING || hit.entity->gateDisableOpening == 1 ) + + int oldHP = hitstats->HP; + if ( damage > 0 ) { - if ( parent && parent->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(255, 0, 255); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3098)); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3102)); // disabled opening spell. - } + hit.entity->modHP(-damage); + magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); + Entity* gib = spawnGib(hit.entity); + serverSpawnGibForClient(gib); + magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); + } + + // write the obituary + if ( parent ) + { + parent->killedByMonsterObituary(hit.entity, true); + } + + // update enemy bar for attacker + if ( !strcmp(hitstats->name, "") ) + { + updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, + false, dmgGib); } else { - // Open the Gate - if ( (hit.entity->skill[28] != 2 && hit.entity->gateInverted == 0) - || (hit.entity->skill[28] != 1 && hit.entity->gateInverted == 1) ) - { - if ( hit.entity->gateInverted == 1 ) - { - hit.entity->skill[28] = 1; // Depowers the Gate - } - else - { - hit.entity->skill[28] = 2; // Powers the Gate - } - if ( parent ) - { - if ( parent->behavior == &actPlayer ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(403)); // "The spell opens the gate!" - Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_GATE_OPENED_SPELL, "portcullis", 1); - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - } - } - } + updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, + false, dmgGib); } - } - else if ( hit.entity->behavior == &actChest ) - { - // Unlock the Chest - if ( hit.entity->chestLocked ) + if ( parent ) { - if ( MFLAG_DISABLEOPENING ) - { - if ( parent && parent->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(255, 0, 255); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3099)); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3100)); // disabled opening spell. - } - } - else + Uint32 color = makeColorRGB(0, 255, 0); + if ( parent->behavior == &actPlayer ) { - playSoundEntity(hit.entity, 91, 64); // "UnlockDoor.ogg" - hit.entity->unlockChest(); - if ( parent ) - { - if ( parent->behavior == &actPlayer) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(404)); // "The spell unlocks the chest!" - Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_CHESTS_UNLOCKED, "chest", 1); - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - } - } + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(394), Language::get(393), MSG_COMBAT); } } - } - else if ( hit.entity->behavior == &actPowerCrystalBase ) - { - Entity* childentity = nullptr; - if ( hit.entity->children.first ) + Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) { - childentity = static_cast((&hit.entity->children)->first->element); - if ( childentity != nullptr ) + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(395)); + } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + if ( oldHP > 0 && hitstats->HP <= 0 ) + { + if ( parent ) { - //Unlock crystal - if ( childentity->crystalSpellToActivate ) + parent->awardXP(hit.entity, true, true); + if ( my->actmagicIsOrbiting == 2 && my->actmagicOrbitCastFromSpell == 0 && parent->behavior == &actPlayer ) { - playSoundEntity(hit.entity, 151, 128); - childentity->crystalSpellToActivate = 0; - // send the clients the updated skill. - serverUpdateEntitySkill(childentity, 10); - if ( parent ) + if ( hitstats->type == LICH || hitstats->type == LICH_ICE || hitstats->type == LICH_FIRE ) { - if ( parent->behavior == &actPlayer ) + if ( true/*client_classes[parent->skill[2]] == CLASS_BREWER*/ ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2358)); - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + steamAchievementClient(parent->skill[2], "BARONY_ACH_SECRET_WEAPON"); } } + steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_BOMBARDIER, STEAM_STAT_INT, 1); } - } - } - } - else if ( hit.entity->behavior == &actMonster && hit.entity->getMonsterTypeFromSprite() == MIMIC ) - { - if ( hit.entity->isInertMimic() ) - { - if ( hitstats->EFFECTS[EFF_MIMIC_LOCKED] ) - { - hit.entity->setEffect(EFF_MIMIC_LOCKED, false, 0, false); - } - if ( hit.entity->disturbMimic(parent, false, true) ) - { - if ( parent ) + if ( my->actmagicCastByTinkerTrap == 1 && parent->behavior == &actPlayer && hitstats->type == MINOTAUR ) { - if ( parent->behavior == &actPlayer ) - { - messagePlayer(parent->skill[2], MESSAGE_INTERACTION, Language::get(6081)); - } + steamAchievementClient(parent->skill[2], "BARONY_ACH_TIME_TO_PLAN"); } + spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); } - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - } - else - { - if ( hitstats ) + else { - //if ( MFLAG_DISABLEOPENING ) - //{ - // if ( parent && parent->behavior == &actPlayer ) - // { - // Uint32 color = makeColorRGB(255, 0, 255); - // messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3099)); - // messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3100)); // disabled locking spell. - // } - //} - //else + if ( achievementObserver.checkUidIsFromPlayer(my->parent) >= 0 ) { - if ( hitstats->EFFECTS[EFF_MIMIC_LOCKED] ) - { - if ( hit.entity->setEffect(EFF_MIMIC_LOCKED, false, 0, false) ) - { - hit.entity->monsterHitTime = std::max(hit.entity->monsterHitTime, HITRATE / 2); - } - } - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + steamAchievementClient(achievementObserver.checkUidIsFromPlayer(my->parent), "BARONY_ACH_TAKING_WITH"); } } } } - else - { - if ( parent ) - { - if ( parent->behavior == &actPlayer ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(401)); // "No telling what it did..." - } - } - - if ( player >= 0 ) - { - messagePlayer(player, MESSAGE_COMBAT, Language::get(401)); // "No telling what it did..." - } - } - - spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); } } - else if (!strcmp(element->element_internal_name, spellElement_dig.element_internal_name)) + else if (!strcmp(element->element_internal_name, spellElement_slow.element_internal_name)) { - if ( !hit.entity ) - { - if ( hit.mapx >= 1 && hit.mapx < map.width - 1 && hit.mapy >= 1 && hit.mapy < map.height - 1 ) - { - magicDig(parent, my, 8, 4); - } - } - else + if (hit.entity) { - if ( hit.entity->behavior == &actBoulder ) + if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer) { - if ( hit.entity->sprite == 989 || hit.entity->sprite == 990 ) - { - magicDig(parent, my, 0, 1); - } - else + playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + hitstats->setEffectActive(EFF_SLOW, 1); + hitstats->EFFECTS_TIMERS[EFF_SLOW] = convertResistancePointsToMagicValue(element->duration, resistance); + + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); + + // If the Entity hit is a Player, update their status to be Slowed + if ( hit.entity->behavior == &actPlayer ) { - magicDig(parent, my, 8, 4); + serverUpdateEffects(hit.entity->skill[2]); } - } - else if ( hit.entity->behavior == &actColliderDecoration && hit.entity->colliderDiggable != 0 ) - { - magicDig(parent, my, 1, 0); - } - else - { + + // update enemy bar for attacker if ( parent ) { + Uint32 color = makeColorRGB(0, 255, 0); if ( parent->behavior == &actPlayer ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(401)); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(394), Language::get(393), MSG_COMBAT); } } + Uint32 color = makeColorRGB(255, 0, 0); if ( player >= 0 ) { - messagePlayer(player, MESSAGE_COMBAT, Language::get(401)); + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(395)); } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); } } } - else if ( !strcmp(element->element_internal_name, spellElement_stoneblood.element_internal_name) ) + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_NUMBING_BOLT].element_internal_name) ) { if ( hit.entity ) { if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer ) { - int effectDuration = (element->duration * (((element->mana) / static_cast(element->base_mana)) * element->overload_multiplier)); - effectDuration /= (1 + (int)resistance); - int oldDuration = !hitstats->EFFECTS[EFF_PARALYZED] ? 0 : hitstats->EFFECTS_TIMERS[EFF_PARALYZED]; - if ( hit.entity->setEffect(EFF_PARALYZED, true, effectDuration, false) ) + if ( hit.entity->setEffect(EFF_NUMBING_BOLT, true, element->duration, false) ) { + spawnFloatingSpriteMisc(134, hit.entity->x + (-4 + local_rng.rand() % 9) + cos(hit.entity->yaw) * 2, + hit.entity->y + (-4 + local_rng.rand() % 9) + sin(hit.entity->yaw) * 2, hit.entity->z + local_rng.rand() % 4); + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - if ( hit.entity->behavior == &actPlayer ) + magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); + + // update enemy bar for attacker + if ( parent ) { - serverUpdateEffects(hit.entity->skill[2]); + Uint32 color = makeColorRGB(0, 255, 0); + if ( parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6623), Language::get(6624), MSG_COMBAT); + } } - - // notify if effect wasn't active with identical duration, few ticks leeway - if ( abs(hitstats->EFFECTS_TIMERS[EFF_PARALYZED] - oldDuration) > 10 ) + Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) { - playSoundEntity(hit.entity, 172, 64); //TODO: Paralyze spell sound. - if ( parent ) - { - Uint32 color = makeColorRGB(0, 255, 0); - if ( parent->behavior == &actPlayer ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2421), Language::get(2420), MSG_COMBAT); - } - } - - Uint32 color = makeColorRGB(255, 0, 0); - if ( player >= 0 ) - { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(2422)); - } + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(6625)); } } else @@ -4079,2802 +4760,14086 @@ void actMagicMissile(Entity* my) //TODO: Verify this function. } } } - else if ( !strcmp(element->element_internal_name, spellElement_bleed.element_internal_name) ) + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_PSYCHIC_SPEAR].element_internal_name) ) { - playSoundEntity(my, 173, 128); if ( hit.entity ) { - if ( (hit.entity->behavior == &actMonster && !mimic) || hit.entity->behavior == &actPlayer ) + if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer ) { - Entity* parent = uidToEntity(my->parent); - playSoundEntity(my, 173, 64); - playSoundEntity(hit.entity, 28, 128); - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - Sint32 preResistanceDamage = damage; - damage *= damageMultiplier; - Stat* casterStats = nullptr; - if ( parent ) + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + + //for ( int i = 0; i < 3; ++i ) { - casterStats = parent->getStats(); - if ( casterStats && casterStats->type == LICH_FIRE && parent->monsterLichAllyStatus == LICH_ALLY_DEAD ) + Entity* fx = createParticleAestheticOrbit(hit.entity, 2362, 5 * TICKS_PER_SECOND, PARTICLE_EFFECT_PSYCHIC_SPEAR); + fx->yaw = my->yaw; + fx->skill[3] = spell->caster; + fx->pitch = 0;// PI / 4; + fx->fskill[0] = fx->yaw + PI / 2 + (local_rng.rand() % 6) * PI / 3; + fx->fskill[1] = PI / 4 + PI / 8;// +(i + 1) * 2 * PI / 3; + fx->x = hit.entity->x - 8.0 * cos(fx->yaw); + fx->y = hit.entity->y - 8.0 * sin(fx->yaw); + fx->z = hit.entity->z;// -8.0; + fx->scalex = 0.0; + fx->scaley = 0.0; + fx->scalez = 0.0; + fx->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + fx->actmagicFromSpellbook = my->actmagicFromSpellbook; + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_weakness.element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_INCOHERENCE].element_internal_name) ) + { + if ( hit.entity ) + { + if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer ) + { + int effectID = 0; + if ( spell ) + { + if ( spell->ID == SPELL_WEAKNESS ) + { + effectID = EFF_WEAKNESS; + } + else if ( spell->ID == SPELL_INCOHERENCE ) { - damage *= 2; + effectID = EFF_INCOHERENCE; } } - damage /= (1 + (int)resistance); - - Sint32 oldHP = hitstats->HP; - hit.entity->modHP(-damage); - magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); - magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); - // write the obituary - if ( parent ) + Uint8 effectStrength = 1; + if ( parent && parent->behavior == &actPlayer ) + { + effectStrength = std::min(getSpellDamageSecondaryFromID(spell->ID, parent, parent ? parent->getStats() : nullptr, my, my->actmagicSpellbookBonus / 100.f), + getSpellDamageFromID(spell->ID, parent, parent ? parent->getStats() : nullptr, my, my->actmagicSpellbookBonus / 100.f)); + } + else { - parent->killedByMonsterObituary(hit.entity); + effectStrength = 5; } - int bleedDuration = (element->duration * (((element->mana) / static_cast(element->base_mana)) * element->overload_multiplier)); - bleedDuration /= (1 + (int)resistance); - bool wasBleeding = hit.entity->getStats() ? hit.entity->getStats()->EFFECTS[EFF_BLEEDING] : false; - if ( hit.entity->setEffect(EFF_BLEEDING, true, bleedDuration, true) ) + if ( hit.entity->setEffect(effectID, effectStrength, element->duration, false, true, true) ) { - if ( parent ) - { - hitstats->bleedInflictedBy = static_cast(my->parent); - } - if ( !wasBleeding && parent && casterStats ) + if ( effectID == EFF_WEAKNESS ) { - // energize if wearing punisher hood! - if ( casterStats->helmet && casterStats->helmet->type == PUNISHER_HOOD ) + if ( parent ) { - parent->modMP(1 + local_rng.rand() % 2); Uint32 color = makeColorRGB(0, 255, 0); - parent->setEffect(EFF_MP_REGEN, true, 250, true); if ( parent->behavior == &actPlayer ) { - messagePlayerColor(parent->skill[2], MESSAGE_HINT, color, Language::get(3753)); - steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_ITS_A_LIVING, STEAM_STAT_INT, 1); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6527), Language::get(6526), MSG_COMBAT); } - playSoundEntity(parent, 168, 128); } + Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(6528)); + } + playSoundEntity(hit.entity, 824, 64); } - } - hitstats->EFFECTS[EFF_SLOW] = true; - hitstats->EFFECTS_TIMERS[EFF_SLOW] = (element->duration * (((element->mana) / static_cast(element->base_mana)) * element->overload_multiplier)); - hitstats->EFFECTS_TIMERS[EFF_SLOW] /= 4; - hitstats->EFFECTS_TIMERS[EFF_SLOW] /= (1 + (int)resistance); - if ( hit.entity->behavior == &actPlayer ) - { - serverUpdateEffects(hit.entity->skill[2]); - } - // update enemy bar for attacker - if ( parent ) - { - Uint32 color = makeColorRGB(0, 255, 0); - if ( parent->behavior == &actPlayer ) + else if ( effectID == EFF_INCOHERENCE ) { - messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2424), Language::get(2423), MSG_COMBAT); + if ( parent ) + { + Uint32 color = makeColorRGB(0, 255, 0); + if ( parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6910), Language::get(6909), MSG_COMBAT); + } + } + Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(6911)); + } + playSoundEntity(hit.entity, 825, 64); } - } - - // write the obituary - if ( parent ) - { - parent->killedByMonsterObituary(hit.entity); - } - // update enemy bar for attacker - if ( !strcmp(hitstats->name, "") ) - { - updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, - false, dmgGib); } else { - updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, - false, dmgGib); - } - - if ( hitstats->HP <= 0 && parent ) - { - parent->awardXP(hit.entity, true, true); - - if ( hit.entity->behavior == &actMonster ) + if ( parent ) { - bool tryBloodVial = false; - if ( gibtype[hitstats->type] == 1 || gibtype[hitstats->type] == 2 ) + Uint32 color = makeColorRGB(255, 0, 0); + if ( parent->behavior == &actPlayer ) { - for ( c = 0; c < MAXPLAYERS; ++c ) - { - if ( playerRequiresBloodToSustain(c) ) - { - tryBloodVial = true; - break; - } - } - if ( tryBloodVial ) - { - Item* blood = newItem(FOOD_BLOOD, EXCELLENT, 0, 1, gibtype[hitstats->type] - 1, true, &hitstats->inventory); - } + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2905), Language::get(2906), MSG_COMBAT); } } } - Uint32 color = makeColorRGB(255, 0, 0); - if ( player >= 0 ) - { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(2425)); - } + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); - for ( int gibs = 0; gibs < 10; ++gibs ) - { - Entity* gib = spawnGib(hit.entity); - serverSpawnGibForClient(gib); - } } - else + } + } + else if (!strcmp(element->element_internal_name, spellElement_sleep.element_internal_name)) + { + if (hit.entity) + { + if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer) { - Entity* caster = uidToEntity(spell->caster); - bool forceFurnitureDamage = false; - if ( caster && caster->behavior == &actMonster && caster->getMonsterTypeFromSprite() == SHOPKEEPER ) + int effectDuration = 0; + if ( parent && parent->behavior == &actMagicTrapCeiling ) { - forceFurnitureDamage = true; + effectDuration = 200 + local_rng.rand() % 150; // 4 seconds + 0 to 3 seconds. } - - if ( forceFurnitureDamage ) + else { - if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) + effectDuration = element->duration; + if ( hitstats ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - damage /= (1 + (int)resistance); + effectDuration = std::max(50, effectDuration - ((hitstats->CON / 3) * 50)); // reduce 1 sec every 3 CON. + } + } + effectDuration = convertResistancePointsToMagicValue(effectDuration, resistance); - hit.entity->colliderHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) + bool magicTrapReapplySleep = true; + + if ( parent && (parent->behavior == &actMagicTrap || parent->behavior == &actMagicTrapCeiling) ) + { + if ( hitstats && hitstats->getEffectActive(EFF_ASLEEP) ) + { + // check to see if we're reapplying the sleep effect. + int preventSleepRoll = (local_rng.rand() % 4) - resistance; + if ( hit.entity->behavior == &actPlayer || (preventSleepRoll <= 0) ) { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + magicTrapReapplySleep = false; + //messagePlayer(0, "Target already asleep!"); } - if ( !(my->actmagicIsOrbiting == 2) ) + } + + int trapResist = hit.entity->getEntityBonusTrapResist(); + if ( trapResist > 0 ) + { + if ( local_rng.rand() % 100 < trapResist ) { - my->removeLightField(); - list_RemoveNode(my->mynode); + magicTrapReapplySleep = false; } - return; } - else if ( hit.entity->behavior == &actChest || mimic ) + } + + if ( magicTrapReapplySleep ) + { + if ( hit.entity->setEffect(EFF_ASLEEP, true, effectDuration, false) ) { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - hit.entity->chestHandleDamageMagic(damage, *my, parent); - if ( my->actmagicProjectileArc > 0 ) + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); + playSoundEntity(hit.entity, 174, 64); + hitstats->OLDHP = hitstats->HP; + if ( hit.entity->behavior == &actPlayer ) { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + serverUpdateEffects(hit.entity->skill[2]); + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(396)); } - if ( !(my->actmagicIsOrbiting == 2) ) + if ( parent ) { - my->removeLightField(); - list_RemoveNode(my->mynode); + Uint32 color = makeColorRGB(0, 255, 0); + if ( parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(398), Language::get(397), MSG_COMBAT); + } } - return; } - else if ( hit.entity->behavior == &actFurniture ) + else { - int damage = element->damage; - damage += (spellbookDamageBonus * damage); - damage /= (1 + (int)resistance); - int oldHP = hit.entity->furnitureHealth; - hit.entity->furnitureHealth -= damage; if ( parent ) { + Uint32 color = makeColorRGB(255, 0, 0); if ( parent->behavior == &actPlayer ) { - bool destroyed = oldHP > 0 && hit.entity->furnitureHealth <= 0; - if ( destroyed ) - { - gameModeManager.currentSession.challengeRun.updateKillEvent(hit.entity); - } - switch ( hit.entity->furnitureType ) - { - case FURNITURE_CHAIR: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(388)); - } - updateEnemyBar(parent, hit.entity, Language::get(677), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_TABLE: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(389)); - } - updateEnemyBar(parent, hit.entity, Language::get(676), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2505)); - } - updateEnemyBar(parent, hit.entity, Language::get(2505), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BUNKBED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2506)); - } - updateEnemyBar(parent, hit.entity, Language::get(2506), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_PODIUM: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2507)); - } - updateEnemyBar(parent, hit.entity, Language::get(2507), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - default: - break; - } + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2905), Language::get(2906), MSG_COMBAT); } } - playSoundEntity(hit.entity, 28, 128); - if ( my->actmagicProjectileArc > 0 ) - { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } } } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); } } } - else if ( !strcmp(element->element_internal_name, spellElement_dominate.element_internal_name) ) + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_DEFY_FLESH].element_internal_name) ) { - Entity *caster = uidToEntity(spell->caster); - if ( caster ) + if ( hit.entity && hitstats ) { - if ( spellEffectDominate(*my, *element, *caster, parent) ) + if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer ) { - //Success - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - } - } - } - else if ( !strcmp(element->element_internal_name, spellElement_acidSpray.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - spellEffectAcid(*my, *element, parent, resistance); - } - } - else if ( !strcmp(element->element_internal_name, spellElement_poison.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - spellEffectPoison(*my, *element, parent, resistance); - } - } - else if ( !strcmp(element->element_internal_name, spellElement_sprayWeb.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - spellEffectSprayWeb(*my, *element, parent, resistance); - } - } - else if ( !strcmp(element->element_internal_name, spellElement_stealWeapon.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - spellEffectStealWeapon(*my, *element, parent, resistance); - } - } - else if ( !strcmp(element->element_internal_name, spellElement_drainSoul.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - spellEffectDrainSoul(*my, *element, parent, resistance); - } - } - else if ( !strcmp(element->element_internal_name, spellElement_charmMonster.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - spellEffectCharmMonster(*my, *element, parent, resistance, static_cast(my->actmagicCastByMagicstaff)); - } - } - else if ( !strcmp(element->element_internal_name, spellElement_telePull.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - if ( spellEffectTeleportPull(my, *element, parent, hit.entity, resistance) ) - { - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); - } - } - } - else if ( !strcmp(element->element_internal_name, spellElement_shadowTag.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - spellEffectShadowTag(*my, *element, parent, resistance); - } - } - else if ( !strcmp(element->element_internal_name, spellElement_demonIllusion.element_internal_name) ) - { - Entity* caster = uidToEntity(spell->caster); - if ( caster ) - { - if ( spellEffectDemonIllusion(*my, *element, parent, hit.entity, resistance) ) - { - magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + int charges = std::min(15, std::max(1, + std::min(getSpellDamageSecondaryFromID(SPELL_DEFY_FLESH, parent, nullptr, my, (my->actmagicSpellbookBonus / 100.f)), + hitstats->HP / std::max(1, element->getDurationSecondary())))); + Uint8 effectStrength = charges; + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + effectStrength |= ((parent->skill[2] + 1) << 4) & 0xF0; + } + } + + if ( hitstats->getEffectActive(EFF_DEFY_FLESH) ) + { + if ( parent ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(parent->isEntityPlayer(), MESSAGE_COMBAT, color, Language::get(6908)); + } + playSoundEntity(hit.entity, 163, 128); + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + } + else if ( hit.entity->setEffect(EFF_DEFY_FLESH, effectStrength, element->duration, true, true, true) ) + { + if ( Entity* fx = createParticleAestheticOrbit(hit.entity, 2363, element->duration, PARTICLE_EFFECT_DEFY_FLESH_ORBIT) ) + { + fx->skill[3] = spell->caster; + fx->flags[INVISIBLE] = true; + fx->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + fx->actmagicFromSpellbook = my->actmagicFromSpellbook; + } + + serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_DEFY_FLESH_ORBIT, 2363, 0, element->duration); + + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, 0, spell ? spell->ID : SPELL_NONE); + + // update enemy bar for attacker + if ( parent ) + { + Uint32 color = makeColorRGB(0, 255, 0); + if ( parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6905), Language::get(6906), MSG_COMBAT); + } + } + Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(6907)); + } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + } } } } - - if ( hitstats ) + else if (!strcmp(element->element_internal_name, spellElement_lightning.element_internal_name)) { - if ( player >= 0 ) + playSoundEntity(my, 173, 128); + if (hit.entity) { - entityHealth -= hitstats->HP; - if ( entityHealth > 0 ) + if ( mimic ) { - // entity took damage, shake screen. - if ( multiplayer == SERVER && player > 0 ) + hit.entity->chestHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) { - strcpy((char*)net_packet->data, "SHAK"); - net_packet->data[4] = 10; // turns into .1 - net_packet->data[5] = 10; - net_packet->address.host = net_clients[player - 1].host; - net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 6; - sendPacketSafe(net_sock, -1, net_packet, player - 1); + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } - else if (player == 0 || (splitscreen && player > 0) ) + if ( !(my->actmagicIsOrbiting == 2) ) { - cameravars[player].shakex += .1; - cameravars[player].shakey += 10; + my->removeLightField(); + list_RemoveNode(my->mynode); } + return; } - } - else - { - if ( parent && parent->behavior == &actPlayer ) + else if (hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer) { - if ( hitstats->HP <= 0 ) + Entity* parent = uidToEntity(my->parent); + playSoundEntity(my, 173, 64); + playSoundEntity(hit.entity, 28, 128); + + int oldHP = hitstats->HP; + hit.entity->modHP(-damage); + magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); + + // write the obituary + if (parent) { - if ( hitstats->type == SCARAB ) + parent->killedByMonsterObituary(hit.entity, true); + } + + if ( spell->ID == SPELL_LIGHTNING ) + { + Uint8 effectStrength = hitstats->getEffectActive(EFF_STATIC); + if ( effectStrength < getSpellEffectDurationSecondaryFromID(SPELL_LIGHTNING, parent, nullptr, my) ) { - // killed a scarab with magic. - steamAchievementEntity(parent, "BARONY_ACH_THICK_SKULL"); + effectStrength += 1; } - if ( my->actmagicMirrorReflected == 1 && static_cast(my->actmagicMirrorReflectedCaster) == hit.entity->getUID() ) + if ( hit.entity->setEffect(EFF_STATIC, effectStrength, + getSpellEffectDurationFromID(spell->ID, parent, nullptr, my), true, true, false, false) ) { - // killed a monster with it's own spell with mirror reflection. - steamAchievementEntity(parent, "BARONY_ACH_NARCISSIST"); + } - if ( stats[parent->skill[2]] && stats[parent->skill[2]]->playerRace == RACE_INSECTOID && stats[parent->skill[2]]->stat_appearance == 0 ) + } + + // update enemy bar for attacker + if ( !strcmp(hitstats->name, "") ) + { + updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + else + { + updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + if ( oldHP > 0 && hitstats->HP <= 0 && parent) + { + parent->awardXP( hit.entity, true, true ); + if ( my->actmagicIsOrbiting == 2 && my->actmagicOrbitCastFromSpell == 0 && parent->behavior == &actPlayer ) { - if ( !achievementObserver.playerAchievements[parent->skill[2]].gastricBypass ) + if ( hitstats->type == LICH || hitstats->type == LICH_ICE || hitstats->type == LICH_FIRE ) { - if ( achievementObserver.playerAchievements[parent->skill[2]].gastricBypassSpell.first == spell->ID ) + if ( true/*client_classes[parent->skill[2]] == CLASS_BREWER*/ ) { - Uint32 oldTicks = achievementObserver.playerAchievements[parent->skill[2]].gastricBypassSpell.second; - if ( parent->ticks - oldTicks < TICKS_PER_SECOND * 5 ) - { - steamAchievementEntity(parent, "BARONY_ACH_GASTRIC_BYPASS"); - achievementObserver.playerAchievements[parent->skill[2]].gastricBypass = true; - } + steamAchievementClient(parent->skill[2], "BARONY_ACH_SECRET_WEAPON"); } } + steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_BOMBARDIER, STEAM_STAT_INT, 1); } + spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); + } + } + else if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) + { + hit.entity->doorHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + return; + } + else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) + { + hit.entity->colliderHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + return; + } + else if ( hit.entity->behavior == &actChest ) + { + hit.entity->chestHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + return; + } + else if (hit.entity->behavior == &actFurniture ) + { + hit.entity->furnitureHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); } } } } - - if ( my->actmagicProjectileArc > 0 ) - { - Entity* caster = uidToEntity(spell->caster); - spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); - } - - if ( !(my->actmagicIsOrbiting == 2) ) + else if ( (spell->ID >= SPELL_SLIME_ACID && spell->ID <= SPELL_SLIME_METAL) || spell->ID == SPELL_GREASE_SPRAY ) { - my->removeLightField(); - if ( my->mynode ) + if ( hit.entity ) { - list_RemoveNode(my->mynode); - } - } + int volume = 128; + static ConsoleVariable cvar_slimehit_sfx("/slimehit_sfx", 173); + int hitsfx = *cvar_slimehit_sfx; + int hitvolume = (particleEmitterHitProps && particleEmitterHitProps->hits == 1) ? 128 : 64; + if ( spell->ID == SPELL_SLIME_WATER || spell->ID == SPELL_SLIME_TAR || spell->ID == SPELL_GREASE_SPRAY ) + { + hitsfx = 665; + } + if ( particleEmitterHitProps && particleEmitterHitProps->hits > 1 ) + { + volume = 0; + if ( spell->ID == SPELL_SLIME_WATER || spell->ID == SPELL_SLIME_TAR ) + { + hitvolume = 32; + } + } + + bool hasgoggles = false; + if ( hitstats && hitstats->mask && hitstats->mask->type == MASK_HAZARD_GOGGLES ) + { + if ( !(hit.entity->behavior == &actPlayer && hit.entity->effectShapeshift != NOTHING) ) + { + hasgoggles = true; + } + } + + if ( mimic ) + { + playSoundEntity(hit.entity, hitsfx, hitvolume); + hit.entity->chestHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + if ( spell->ID == SPELL_SLIME_FIRE ) + { + hit.entity->SetEntityOnFire((parent && parent->getStats()) ? parent : nullptr); + } + return; + } + else if ( hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer ) + { + playSoundEntity(hit.entity, hitsfx, hitvolume); + Entity* parent = uidToEntity(my->parent); + playSoundEntity(hit.entity, 28, volume); + + int oldHP = hitstats->HP; + + if ( spell->ID == SPELL_SLIME_FIRE ) + { + if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6239), Language::get(6238), MSG_COMBAT); + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6237)); + } + if ( hasgoggles ) + { + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); + } + } + } + + if ( !hasgoggles ) + { + if ( hit.entity->SetEntityOnFire((parent && parent->getStats()) ? parent : nullptr) ) + { + if ( parent && parent->behavior == &actPlayer ) + { + hit.entity->char_fire = std::min(hit.entity->char_fire, element->duration); + } + } + } + } + if ( spell->ID == SPELL_GREASE_SPRAY ) + { + if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6244), Language::get(6243), MSG_COMBAT); + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6236)); + } + } + + if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + { + int duration = element->duration; + duration = convertResistancePointsToMagicValue(duration, resistance); + + if ( hasgoggles ) + { + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); + } + } + else if ( hit.entity->setEffect(EFF_MAGIC_GREASE, true, duration, true) ) + { + hit.entity->setEffect(EFF_GREASY, true, duration / 2, false); + Uint32 color = makeColorRGB(255, 0, 0); + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6245)); + } + } + } + + hit.entity->updateEntityOnHit(parent, alertTarget); + } + if ( spell->ID == SPELL_SLIME_TAR ) + { + if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6244), Language::get(6243), MSG_COMBAT); + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6236)); + } + } + + if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + { + if ( local_rng.rand() % 2 == 0 ) + { + int duration = element->duration / 2; + duration = convertResistancePointsToMagicValue(duration, resistance); + + int status = hit.entity->behavior == &actPlayer ? EFF_MESSY : EFF_BLIND; + + if ( hasgoggles ) + { + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); + } + } + else if ( hit.entity->setEffect(status, true, duration / 2, false) ) + { + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(765)); + } + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(3878), Language::get(3879), MSG_COMBAT); + } + } + } + else + { + int duration = element->duration; + duration = convertResistancePointsToMagicValue(duration, resistance); + + if ( hasgoggles ) + { + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); + } + } + else if ( hit.entity->setEffect(EFF_GREASY, true, duration, false) ) + { + Uint32 color = makeColorRGB(255, 0, 0); + if ( hit.entity->behavior == &actPlayer ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6245)); + } + } + } + } + + if ( !hit.entity->isBlind() ) + { + if ( alertTarget && hit.entity->monsterState != MONSTER_STATE_ATTACK && (hitstats->type < LICH || hitstats->type >= SHOPKEEPER) ) + { + hit.entity->monsterAcquireAttackTarget(*parent, MONSTER_STATE_PATH, true); + } + } + else + { + alertTarget = false; + hit.entity->monsterReleaseAttackTarget(); + } + hit.entity->updateEntityOnHit(parent, alertTarget); + } + if ( spell->ID == SPELL_SLIME_ACID || spell->ID == SPELL_SLIME_METAL ) + { + bool hasamulet = false; + if ( (hitstats->amulet && hitstats->amulet->type == AMULET_POISONRESISTANCE) || hitstats->type == INSECTOID ) + { + resistance += 2; + hasamulet = true; + } + if ( hasgoggles ) + { + resistance += 2; + } + + if ( hasamulet && !hasgoggles ) + { + hit.entity->degradeAmuletProc(hitstats, AMULET_POISONRESISTANCE); + } + + int duration = (spell->ID == SPELL_SLIME_METAL ? 10 : 6) * TICKS_PER_SECOND; + duration = convertResistancePointsToMagicValue(duration, resistance); + if ( spell->ID == SPELL_SLIME_ACID ) + { + if ( !hasamulet && !hasgoggles ) + { + if ( hit.entity->setEffect(EFF_POISONED, true, duration, false) ) + { + hitstats->poisonKiller = my->parent; + } + } + } + + if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + if ( spell->ID == SPELL_SLIME_METAL ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(6241), Language::get(6240), MSG_COMBAT); + } + else + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2431), Language::get(2430), MSG_COMBAT); + } + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + if ( spell->ID == SPELL_SLIME_METAL ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6242)); + } + else + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(2432)); + } + if ( hasgoggles ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); + } + } + } + + if ( spell->ID == SPELL_SLIME_METAL ) + { + if ( !hasgoggles && hit.entity->setEffect(EFF_SLOW, true, duration, false) ) + { + if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + { + playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(394), Language::get(393), MSG_COMBAT); + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(395)); + } + } + } + } + + if ( hitstats->HP > 0 && !hasgoggles ) + { + // damage armor + Item* armor = nullptr; + int armornum = -1; + int acidChance = spell->ID == SPELL_SLIME_METAL ? 2 : 4; + if ( hitstats->defending && (local_rng.rand() % ((acidChance * 2) + resistance) == 0) ) // 1 in 8 to corrode shield + { + armornum = hitstats->pickRandomEquippedItem(&armor, true, false, true, true); + } + else if ( !hitstats->defending && (local_rng.rand() % (acidChance + resistance) == 0) ) // 1 in 4 to corrode armor + { + armornum = hitstats->pickRandomEquippedItem(&armor, true, false, false, false); + } + if ( armornum != -1 && armor != nullptr ) + { + hit.entity->degradeArmor(*hitstats, *armor, armornum); + } + } + } + else if ( spell->ID == SPELL_SLIME_WATER ) + { + if ( !hasgoggles && hit.entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + real_t pushbackMultiplier = 0.6; + if ( hit.entity->behavior == &actMonster ) + { + if ( !hit.entity->isMobile() ) + { + pushbackMultiplier += 0.3; + } + if ( parent ) + { + real_t tangent = atan2(hit.entity->y - parent->y, hit.entity->x - parent->x); + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackUID = my->parent; + hit.entity->monsterKnockbackTangentDir = tangent; + } + else + { + real_t tangent = atan2(hit.entity->y - my->y, hit.entity->x - my->x); + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackTangentDir = tangent; + } + } + else if ( hit.entity->behavior == &actPlayer ) + { + /*if ( parent ) + { + real_t dist = entityDist(parent, hit.entity); + if ( dist < TOUCHRANGE ) + { + pushbackMultiplier += 0.5; + } + }*/ + if ( !players[hit.entity->skill[2]]->isLocalPlayer() ) + { + hit.entity->monsterKnockbackVelocity = pushbackMultiplier; + hit.entity->monsterKnockbackTangentDir = my->yaw; + serverUpdateEntityFSkill(hit.entity, 11); + serverUpdateEntityFSkill(hit.entity, 9); + } + else + { + hit.entity->monsterKnockbackVelocity = pushbackMultiplier; + hit.entity->monsterKnockbackTangentDir = my->yaw; + } + } + } + + if ( particleEmitterHitProps && particleEmitterHitProps->hits == 1 ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(3215), Language::get(3214), MSG_COMBAT); + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + if ( !hasgoggles ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6235)); + } + else + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(6309)); + } + + if ( hitstats->type == VAMPIRE ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(644)); + } + else if ( hasgoggles ) + { + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6088)); + } + } + } + + if ( hit.entity->behavior == &actPlayer ) + { + if ( hitstats && hitstats->getEffectActive(EFF_POLYMORPH) ) + { + hit.entity->setEffect(EFF_POLYMORPH, false, 0, true); + hit.entity->effectPolymorph = 0; + serverUpdateEntitySkill(hit.entity, 50); + + messagePlayer(hit.entity->skill[2], MESSAGE_STATUS, Language::get(3192)); + if ( !hitstats->getEffectActive(EFF_SHAPESHIFT) ) + { + messagePlayer(hit.entity->skill[2], MESSAGE_STATUS, Language::get(3185)); + } + else + { + messagePlayer(hit.entity->skill[2], MESSAGE_STATUS, Language::get(4303)); + } + playSoundEntity(hit.entity, 400, 92); + createParticleDropRising(hit.entity, 593, 1.f); + serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_RISING_DROP, 593); + } + } + } + + hit.entity->modHP(-damage); + magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); + + // write the obituary + if ( parent ) + { + parent->killedByMonsterObituary(hit.entity, true); + } + + if ( damage > 0 ) + { + // update enemy bar for attacker + if ( !strcmp(hitstats->name, "") ) + { + updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + else + { + updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + if ( oldHP > 0 && hitstats->HP <= 0 && parent ) + { + parent->awardXP(hit.entity, true, true); + spawnBloodVialOnMonsterDeath(hit.entity, hitstats, parent); + } + } + } + else if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) + { + if ( spell->ID == SPELL_SLIME_FIRE ) + { + hit.entity->SetEntityOnFire((parent && parent->getStats()) ? parent : nullptr); + } + playSoundEntity(hit.entity, hitsfx, hitvolume); + + hit.entity->doorHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + return; + } + else if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) + { + if ( spell->ID == SPELL_SLIME_FIRE ) + { + hit.entity->SetEntityOnFire((parent && parent->getStats()) ? parent : nullptr); + } + playSoundEntity(hit.entity, hitsfx, hitvolume); + + hit.entity->colliderHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + return; + } + else if ( hit.entity->behavior == &actChest ) + { + if ( spell->ID == SPELL_SLIME_FIRE ) + { + hit.entity->SetEntityOnFire((parent && parent->getStats()) ? parent : nullptr); + } + playSoundEntity(hit.entity, hitsfx, hitvolume); + hit.entity->chestHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + return; + } + else if ( hit.entity->behavior == &actFurniture ) + { + if ( spell->ID == SPELL_SLIME_FIRE ) + { + hit.entity->SetEntityOnFire((parent && parent->getStats()) ? parent : nullptr); + } + playSoundEntity(hit.entity, hitsfx, hitvolume); + + hit.entity->furnitureHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_SPIN].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_DIZZY].element_internal_name) ) + { + if ( hit.entity ) + { + if ( (!mimic && hit.entity->behavior == &actMonster) ) + { + if ( hitstats ) + { + bool effect = false; + Entity* caster = uidToEntity(spell->caster); + bool dizzy = !strcmp(element->element_internal_name, spellElementMap[SPELL_DIZZY].element_internal_name); + if ( !hitstats->getEffectActive(EFF_DISORIENTED) && hit.entity->isMobile() ) + { + int duration = element->duration; + if ( dizzy ) + { + if ( hit.entity->setEffect(EFF_SPIN, true, duration, false) ) + { + effect = true; + if ( hit.entity->setEffect(EFF_KNOCKBACK, true, duration, false) ) + { + real_t pushbackMultiplier = 1.0; + if ( parent ) + { + real_t tangent = atan2(hit.entity->y - parent->y, hit.entity->x - parent->x); + tangent -= PI / 2; + tangent += (local_rng.rand() % 5) * PI / 4; + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackUID = my->parent; + hit.entity->monsterKnockbackTangentDir = tangent; + //hit.entity->lookAtEntity(*parent); + } + else + { + real_t tangent = atan2(hit.entity->y - my->y, hit.entity->x - my->x); + tangent -= PI / 2; + tangent += (local_rng.rand() % 5) * PI / 4; + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackTangentDir = tangent; + hit.entity->monsterKnockbackUID = 0; + //hit.entity->lookAtEntity(*my); + } + } + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + createParticleSpin(hit.entity); + serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_SPIN, -1); + if ( caster ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *hitstats, Language::get(6706), Language::get(6707), MSG_COMBAT); + } + } + } + + if ( !dizzy && hit.entity->setEffect(EFF_DISORIENTED, (Uint8)2, duration, false) ) + { + if ( hit.entity->monsterReleaseAttackTarget() ) + { + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + effect = true; + /*if ( caster ) + { + hit.entity->lookAtEntity(*caster); + }*/ + hit.entity->monsterLookDir = hit.entity->yaw; + hit.entity->monsterLookDir += (PI - PI / 4 + (local_rng.rand() % 10) * PI / 40); + spawnFloatingSpriteMisc(134, hit.entity->x + (-4 + local_rng.rand() % 9) + cos(hit.entity->yaw) * 2, + hit.entity->y + (-4 + local_rng.rand() % 9) + sin(hit.entity->yaw) * 2, hit.entity->z + local_rng.rand() % 4); + createParticleSpin(hit.entity); + serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_SPIN, -1); + + if ( caster ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *hitstats, Language::get(6704), Language::get(6705), MSG_COMBAT); + } + } + } + } + + if ( !effect ) + { + if ( caster ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *hitstats, Language::get(2905), Language::get(2906), MSG_COMBAT); + } + } + } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 1856); + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_ghostBolt.element_internal_name) ) + { + if ( hit.entity ) + { + if ( (!mimic && hit.entity->behavior == &actMonster) ) + { + Entity* parent = uidToEntity(my->parent); + real_t pushbackMultiplier = 0.6; + if ( !hit.entity->isMobile() ) + { + pushbackMultiplier += 0.3; + } + + bool doSlow = true; + const int duration = TICKS_PER_SECOND * 2; + if ( hitstats ) + { + if ( hitstats->getEffectActive(EFF_SLOW) || hitstats->EFFECTS_TIMERS[EFF_SLOW] > duration ) + { + doSlow = false; + } + } + + if ( doSlow ) + { + if ( hit.entity->setEffect(EFF_SLOW, true, duration, false) ) + { + //playSoundEntity(hit.entity, 396 + local_rng.rand() % 3, 64); + if ( parent ) + { + Uint32 color = makeColorRGB(0, 255, 0); + if ( parent->behavior == &actPlayer || parent->behavior == &actDeathGhost ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(394), Language::get(393), MSG_COMBAT); + } + } + /*Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(395)); + }*/ + } + } + + if ( hit.entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + if ( parent ) + { + real_t tangent = atan2(hit.entity->y - parent->y, hit.entity->x - parent->x); + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackUID = my->parent; + hit.entity->monsterKnockbackTangentDir = tangent; + //hit.entity->lookAtEntity(*parent); + } + else + { + real_t tangent = atan2(hit.entity->y - my->y, hit.entity->x - my->x); + hit.entity->vel_x = cos(tangent) * pushbackMultiplier; + hit.entity->vel_y = sin(tangent) * pushbackMultiplier; + hit.entity->monsterKnockbackVelocity = 0.01; + hit.entity->monsterKnockbackTangentDir = tangent; + hit.entity->monsterKnockbackUID = 0; + //hit.entity->lookAtEntity(*my); + } + } + /*if ( hit.entity->monsterAttack == 0 ) + { + hit.entity->monsterHitTime = std::max(HITRATE - 12, hit.entity->monsterHitTime); + }*/ + } + else + { + //if ( parent ) + //{ + // if ( parent->behavior == &actPlayer || parent->behavior == &actDeathGhost ) + // { + // messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(401)); // "No telling what it did..." + // } + //} + } + + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + } + } + else if (!strcmp(element->element_internal_name, spellElement_locking.element_internal_name)) + { + if ( hit.entity ) + { + if (hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) + { + if ( MFLAG_DISABLEOPENING || hit.entity->doorDisableOpening == 1 ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 255); + if ( hit.entity->behavior == &actIronDoor ) + { + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(6414)); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(6419)); // disabled locking spell. + } + else + { + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3097)); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3101)); // disabled locking spell. + } + } + } + else + { + playSoundEntity(hit.entity, 92, 64); + auto prevLocked = hit.entity->doorLocked; + hit.entity->doorLocked = 1; //Lock the door. + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + if ( hit.entity->behavior == &actIronDoor ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(6408)); + } + else + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(399)); + } + if ( prevLocked != hit.entity->doorLocked ) + { + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + } + } + else if (hit.entity->behavior == &actChest) + { + //Lock chest + playSoundEntity(hit.entity, 92, 64); + if ( !hit.entity->chestLocked ) + { + if ( parent && parent->behavior == &actPlayer && MFLAG_DISABLEOPENING ) + { + Uint32 color = makeColorRGB(255, 0, 255); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3099)); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3100)); // disabled locking spell. + } + else + { + auto prevLocked = hit.entity->chestLocked; + hit.entity->lockChest(); + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(400)); + if ( prevLocked != hit.entity->chestLocked ) + { + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + } + } + } + else if ( hit.entity->behavior == &actMonster && hit.entity->getMonsterTypeFromSprite() == MIMIC ) + { + //Lock chest + playSoundEntity(hit.entity, 92, 64); + + if ( hitstats ) + { + //if ( MFLAG_DISABLEOPENING ) + //{ + // if ( parent && parent->behavior == &actPlayer ) + // { + // Uint32 color = makeColorRGB(255, 0, 255); + // messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3099)); + // messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3100)); // disabled locking spell. + // } + //} + //else + { + if ( parent && parent->behavior == &actPlayer ) + { + if ( mimic ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(400)); + } + else + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(6083)); + } + } + if ( !hitstats->getEffectActive(EFF_MIMIC_LOCKED) ) + { + if ( hit.entity->setEffect(EFF_MIMIC_LOCKED, true, TICKS_PER_SECOND * 5, false) ) + { + hit.entity->monsterHitTime = HITRATE - 2; + hitstats->monsterMimicLockedBy = parent ? parent->getUID() : 0; + + if ( parent && parent->behavior == &actPlayer ) + { + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + } + } + } + else + { + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(401)); + } + } + if ( player >= 0 ) + { + messagePlayer(player, MESSAGE_COMBAT, Language::get(401)); + } + } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + } + } + else if (!strcmp(element->element_internal_name, spellElement_opening.element_internal_name)) + { + if (hit.entity) + { + if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) + { + if ( MFLAG_DISABLEOPENING || hit.entity->doorDisableOpening == 1 ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 255); + if ( hit.entity->behavior == &actIronDoor ) + { + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(6414)); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(6419)); // disabled opening spell. + } + else + { + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3097)); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3101)); // disabled opening spell. + } + } + } + else + { + // Open the Door + playSoundEntity(hit.entity, 91, 64); // "UnlockDoor.ogg" + if ( hit.entity->doorLocked ) + { + if ( parent && parent->behavior == &actPlayer ) + { + if ( hit.entity->behavior == &actIronDoor ) + { + Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_DOOR_UNLOCKED, "iron door", 1); + } + else + { + Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_DOOR_UNLOCKED, "door", 1); + } + } + } + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + hit.entity->doorLocked = 0; // Unlocks the Door + hit.entity->doorPreventLockpickExploit = 1; + + if ( !hit.entity->skill[0] && !hit.entity->skill[3] ) + { + hit.entity->skill[3] = 1 + (my->x > hit.entity->x); // Opens the Door + playSoundEntity(hit.entity, 21, 96); // "UnlockDoor.ogg" + } + else if ( hit.entity->skill[0] && !hit.entity->skill[3] ) + { + hit.entity->skill[3] = 1 + (my->x < hit.entity->x); // Opens the Door + playSoundEntity(hit.entity, 21, 96); // "UnlockDoor.ogg" + } + if ( parent ) + { + if ( parent->behavior == &actPlayer) + { + if ( hit.entity->behavior == &actIronDoor ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(6411)); + } + else + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(402)); + } + } + } + } + } + else if ( hit.entity->behavior == &actGate ) + { + if ( MFLAG_DISABLEOPENING || hit.entity->gateDisableOpening == 1 ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 255); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3098)); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3102)); // disabled opening spell. + } + } + else + { + // Open the Gate + if ( (hit.entity->skill[28] != 2 && hit.entity->gateInverted == 0) + || (hit.entity->skill[28] != 1 && hit.entity->gateInverted == 1) ) + { + if ( hit.entity->gateInverted == 1 ) + { + hit.entity->skill[28] = 1; // Depowers the Gate + } + else + { + hit.entity->skill[28] = 2; // Powers the Gate + } + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(403)); // "The spell opens the gate!" + Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_GATE_OPENED_SPELL, "portcullis", 1); + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + } + } + else if ( hit.entity->behavior == &actChest ) + { + // Unlock the Chest + if ( hit.entity->chestLocked ) + { + if ( MFLAG_DISABLEOPENING ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 255); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3099)); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3100)); // disabled opening spell. + } + } + else + { + playSoundEntity(hit.entity, 91, 64); // "UnlockDoor.ogg" + auto prevLocked = hit.entity->chestLocked; + hit.entity->unlockChest(); + if ( parent ) + { + if ( parent->behavior == &actPlayer) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(404)); // "The spell unlocks the chest!" + Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_CHESTS_UNLOCKED, "chest", 1); + if ( prevLocked != hit.entity->chestLocked ) + { + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + } + } + } + else if ( hit.entity->behavior == &actPowerCrystalBase ) + { + Entity* childentity = nullptr; + if ( hit.entity->children.first ) + { + childentity = static_cast((&hit.entity->children)->first->element); + if ( childentity != nullptr ) + { + //Unlock crystal + if ( childentity->crystalSpellToActivate ) + { + playSoundEntity(hit.entity, 151, 128); + childentity->crystalSpellToActivate = 0; + // send the clients the updated skill. + serverUpdateEntitySkill(childentity, 10); + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2358)); + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + } + } + } + else if ( hit.entity->behavior == &actMonster && hit.entity->getMonsterTypeFromSprite() == MIMIC ) + { + if ( hit.entity->isInertMimic() ) + { + if ( hitstats->getEffectActive(EFF_MIMIC_LOCKED) ) + { + hit.entity->setEffect(EFF_MIMIC_LOCKED, false, 0, false); + } + if ( hit.entity->disturbMimic(parent, false, true) ) + { + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_INTERACTION, Language::get(6081)); + } + } + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + else + { + if ( hitstats ) + { + //if ( MFLAG_DISABLEOPENING ) + //{ + // if ( parent && parent->behavior == &actPlayer ) + // { + // Uint32 color = makeColorRGB(255, 0, 255); + // messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, 0xFFFFFFFF, Language::get(3096), Language::get(3099)); + // messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3100)); // disabled locking spell. + // } + //} + //else + { + if ( hitstats->getEffectActive(EFF_MIMIC_LOCKED) ) + { + if ( hit.entity->setEffect(EFF_MIMIC_LOCKED, false, 0, false) ) + { + hit.entity->monsterHitTime = std::max(hit.entity->monsterHitTime, HITRATE / 2); + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + } + } + } + else + { + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(401)); // "No telling what it did..." + } + } + + if ( player >= 0 ) + { + messagePlayer(player, MESSAGE_COMBAT, Language::get(401)); // "No telling what it did..." + } + } + + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_SPLINTER_GEAR].element_internal_name) ) + { + if ( hit.entity ) + { + real_t ratio = 1.0; + bool found = false; + bool criticalEffect = false; + Entity* parent = uidToEntity(my->parent); + if ( mimic || hit.entity->behavior == &actChest ) + { + found = true; + ratio = std::max(100, getSpellDamageSecondaryFromID(SPELL_SPLINTER_GEAR, parent, nullptr, my, (my->actmagicSpellbookBonus / 100.f))) / 100.0; + if ( applyGenericMagicDamage(parent, hit.entity, *my, spell->ID, preResistanceDamage * ratio, false) ) + { + criticalEffect = true; + } + } + else if ( hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer ) + { + Item* armor = nullptr; + bool shields = hitstats->shield && itemCategory(hitstats->shield) == ARMOR; + int armornum = hitstats->pickRandomEquippedItemToDegradeOnHit(&armor, true, !shields, false, true); + + if ( armor != nullptr && armor->status > BROKEN ) + { + ItemType type = armor->type; + if ( hit.entity->degradeArmor(*hitstats, *armor, armornum) ) + { + found = true; + if ( !armor || (armor && armor->status == BROKEN) ) + { + criticalEffect = true; + ratio = std::max(100, getSpellDamageSecondaryFromID(SPELL_SPLINTER_GEAR, parent, nullptr, my, (my->actmagicSpellbookBonus / 100.f))) / 100.0; + } + + if ( parent ) + { + if ( armor->status > BROKEN ) + { + const char* msg = Language::get(6678); // named + if ( !strcmp(hitstats->name, "") || monsterNameIsGeneric(*hitstats) ) + { + msg = Language::get(6677); + } + + if ( !strcmp(hitstats->name, "") ) + { + messagePlayerColor(parent->isEntityPlayer(), MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + msg, getMonsterLocalizedName(hitstats->type).c_str(), items[type].getIdentifiedName()); + } + else + { + messagePlayerColor(parent->isEntityPlayer(), MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + msg, hitstats->name, items[type].getIdentifiedName()); + } + } + else + { + const char* msg = Language::get(6680); // named + if ( !strcmp(hitstats->name, "") || monsterNameIsGeneric(*hitstats) ) + { + msg = Language::get(6679); + } + if ( !strcmp(hitstats->name, "") ) + { + messagePlayerColor(parent->isEntityPlayer(), MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + msg, getMonsterLocalizedName(hitstats->type).c_str(), items[type].getIdentifiedName()); + } + else + { + messagePlayerColor(parent->isEntityPlayer(), MESSAGE_COMBAT, makeColorRGB(0, 255, 0), + msg, hitstats->name, items[type].getIdentifiedName()); + } + } + } + } + } + if ( hitstats->type == CRYSTALGOLEM + || hitstats->type == AUTOMATON + || hitstats->type == SENTRYBOT + || hitstats->type == SPELLBOT + || hitstats->type == DUMMYBOT + || hitstats->type == MINIMIMIC + || hitstats->type == MIMIC ) + { + if ( !found ) + { + if ( parent ) + { + messagePlayerMonsterEvent(parent->isEntityPlayer(), makeColorRGB(0, 255, 0), + *hitstats, Language::get(6809), Language::get(6810), MSG_COMBAT); + } + } + found = true; + if ( !criticalEffect ) + { + criticalEffect = true; + ratio = std::max(100, getSpellDamageSecondaryFromID(SPELL_SPLINTER_GEAR, parent, nullptr, my, (my->actmagicSpellbookBonus / 100.f))) / 100.0; + } + } + } + + if ( found ) + { + if ( hit.entity->behavior == &actPlayer ) + { + if ( criticalEffect ) + { + messagePlayerColor(hit.entity->isEntityPlayer(), MESSAGE_STATUS, makeColorRGB(255, 0, 0), Language::get(6812)); + } + else + { + messagePlayerColor(hit.entity->isEntityPlayer(), MESSAGE_STATUS, makeColorRGB(255, 0, 0), Language::get(6811)); + } + } + + for ( int i = 0; i < 5; ++i ) + { + Entity* gib = spawnGib(hit.entity, 2208); + gib->sprite = 2208; + serverSpawnGibForClient(gib); + } + if ( criticalEffect ) + { + Entity* spellTimer = createParticleTimer(hit.entity, 15, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SPLINTER_GEAR; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->yaw = hit.entity->yaw; + spellTimer->x = hit.entity->x; + spellTimer->y = hit.entity->y; + spellTimer->flags[NOUPDATE] = false; // spawn for client + spellTimer->flags[UPDATENEEDED] = true; + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + + if ( hitstats ) + { + if ( hit.entity->setEffect(EFF_BLEEDING, true, element->duration, false) ) + { + if ( parent ) + { + hitstats->bleedInflictedBy = parent->getUID(); + } + } + hit.entity->setEffect(EFF_SLOW, true, element->duration, false); + if ( !mimic ) + { + if ( applyGenericMagicDamage(parent, hit.entity, *my, spell->ID, preResistanceDamage * ratio, false) ) + { + + } + } + } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, 171); + playSoundEntity(hit.entity, 167, 128); + } + else + { + if ( parent && hitstats ) + { + messagePlayerMonsterEvent(parent->isEntityPlayer(), makeColorRGB(255, 0, 0), + *hitstats, Language::get(2905), Language::get(2906), MSG_COMBAT); + } + } + } + } + else if (!strcmp(element->element_internal_name, spellElement_dig.element_internal_name)) + { + if ( !hit.entity ) + { + if ( hit.mapx >= 1 && hit.mapx < map.width - 1 && hit.mapy >= 1 && hit.mapy < map.height - 1 ) + { + magicDig(parent, my, 8, 4); + } + } + else + { + if ( hit.entity->behavior == &actBoulder ) + { + if ( hit.entity->sprite == 989 || hit.entity->sprite == 990 ) + { + magicDig(parent, my, 0, 1); + } + else + { + magicDig(parent, my, 8, 4); + } + } + else if ( hit.entity->behavior == &actColliderDecoration && hit.entity->colliderDiggable != 0 ) + { + magicDig(parent, my, 1, 0); + } + else + { + if ( parent ) + { + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(401)); + } + } + if ( player >= 0 ) + { + messagePlayer(player, MESSAGE_COMBAT, Language::get(401)); + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_stoneblood.element_internal_name) ) + { + if ( hit.entity ) + { + if ( (!mimic && hit.entity->behavior == &actMonster) || hit.entity->behavior == &actPlayer ) + { + int effectDuration = element->duration; + effectDuration = convertResistancePointsToMagicValue(effectDuration, resistance); + int oldDuration = !hitstats->getEffectActive(EFF_PARALYZED) ? 0 : hitstats->EFFECTS_TIMERS[EFF_PARALYZED]; + if ( hit.entity->setEffect(EFF_PARALYZED, true, effectDuration, false) ) + { + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + if ( hit.entity->behavior == &actPlayer ) + { + serverUpdateEffects(hit.entity->skill[2]); + } + + // notify if effect wasn't active with identical duration, few ticks leeway + if ( abs(hitstats->EFFECTS_TIMERS[EFF_PARALYZED] - oldDuration) > 10 ) + { + playSoundEntity(hit.entity, 172, 64); //TODO: Paralyze spell sound. + if ( parent ) + { + Uint32 color = makeColorRGB(0, 255, 0); + if ( parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2421), Language::get(2420), MSG_COMBAT); + } + } + + Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(2422)); + } + } + } + else + { + if ( parent ) + { + Uint32 color = makeColorRGB(255, 0, 0); + if ( parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2905), Language::get(2906), MSG_COMBAT); + } + } + } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_bleed.element_internal_name) ) + { + playSoundEntity(my, 173, 128); + if ( hit.entity ) + { + if ( (hit.entity->behavior == &actMonster && !mimic) || hit.entity->behavior == &actPlayer ) + { + Entity* parent = uidToEntity(my->parent); + playSoundEntity(my, 173, 64); + playSoundEntity(hit.entity, 28, 128); + + Stat* casterStats = nullptr; + if ( parent ) + { + casterStats = parent->getStats(); + } + + Sint32 oldHP = hitstats->HP; + hit.entity->modHP(-damage); + magicOnEntityHit(parent, my, hit.entity, hitstats, preResistanceDamage, damage, oldHP, spell ? spell->ID : SPELL_NONE); + magicTrapOnHit(parent, hit.entity, hitstats, oldHP, spell ? spell->ID : SPELL_NONE); + + // write the obituary + if ( parent ) + { + parent->killedByMonsterObituary(hit.entity, true); + } + + int bleedDuration = element->duration; + bleedDuration = convertResistancePointsToMagicValue(bleedDuration, resistance); + bool wasBleeding = hit.entity->getStats() ? hit.entity->getStats()->getEffectActive(EFF_BLEEDING) : false; + if ( bleedDuration > 0 && hit.entity->setEffect(EFF_BLEEDING, true, bleedDuration, false) ) + { + if ( parent ) + { + hitstats->bleedInflictedBy = static_cast(my->parent); + } + if ( !wasBleeding && parent && casterStats ) + { + // energize if wearing punisher hood! + if ( casterStats->helmet && casterStats->helmet->type == PUNISHER_HOOD ) + { + int mpAmount = parent->modMP(1 + local_rng.rand() % 2); + parent->playerInsectoidIncrementHungerToMP(mpAmount); + Uint32 color = makeColorRGB(0, 255, 0); + parent->setEffect(EFF_MP_REGEN, true, std::max(casterStats->EFFECTS_TIMERS[EFF_MP_REGEN], 10 * TICKS_PER_SECOND), false); + if ( parent->behavior == &actPlayer ) + { + messagePlayerColor(parent->skill[2], MESSAGE_HINT, color, Language::get(3753)); + steamStatisticUpdateClient(parent->skill[2], STEAM_STAT_ITS_A_LIVING, STEAM_STAT_INT, 1); + } + playSoundEntity(parent, 168, 128); + } + } + } + + int slowDuration = element->duration / 4; + slowDuration = convertResistancePointsToMagicValue(slowDuration, resistance); + if ( slowDuration > 0 && hit.entity->setEffect(EFF_SLOW, true, slowDuration, false) ) + { + + } + if ( hit.entity->behavior == &actPlayer ) + { + serverUpdateEffects(hit.entity->skill[2]); + } + // update enemy bar for attacker + if ( parent ) + { + Uint32 color = makeColorRGB(0, 255, 0); + if ( parent->behavior == &actPlayer ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(2424), Language::get(2423), MSG_COMBAT); + } + } + + // write the obituary + if ( parent ) + { + parent->killedByMonsterObituary(hit.entity, true); + } + + // update enemy bar for attacker + if ( !strcmp(hitstats->name, "") ) + { + updateEnemyBar(parent, hit.entity, getMonsterLocalizedName(hitstats->type).c_str(), hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + else + { + updateEnemyBar(parent, hit.entity, hitstats->name, hitstats->HP, hitstats->MAXHP, + false, dmgGib); + } + + if ( hitstats->HP <= 0 && parent ) + { + parent->awardXP(hit.entity, true, true); + + if ( hit.entity->behavior == &actMonster ) + { + bool tryBloodVial = false; + if ( gibtype[hitstats->type] == 1 || gibtype[hitstats->type] == 2 ) + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( playerRequiresBloodToSustain(c) ) + { + tryBloodVial = true; + break; + } + } + if ( tryBloodVial ) + { + Item* blood = newItem(FOOD_BLOOD, EXCELLENT, 0, 1, gibtype[hitstats->type] - 1, true, &hitstats->inventory); + } + } + } + } + + Uint32 color = makeColorRGB(255, 0, 0); + if ( player >= 0 ) + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(2425)); + } + spawnMagicEffectParticles(hit.entity->x, hit.entity->y, hit.entity->z, my->sprite); + for ( int gibs = 0; gibs < 10; ++gibs ) + { + Entity* gib = spawnGib(hit.entity); + serverSpawnGibForClient(gib); + } + } + else + { + Entity* caster = uidToEntity(spell->caster); + bool forceFurnitureDamage = false; + if ( caster && caster->behavior == &actMonster && caster->getMonsterTypeFromSprite() == SHOPKEEPER ) + { + forceFurnitureDamage = true; + } + + if ( forceFurnitureDamage ) + { + if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) + { + hit.entity->colliderHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + return; + } + else if ( hit.entity->behavior == &actChest || mimic ) + { + hit.entity->chestHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + return; + } + else if ( hit.entity->behavior == &actFurniture ) + { + hit.entity->furnitureHandleDamageMagic(damage, *my, parent); + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + } + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_dominate.element_internal_name) ) + { + Entity *caster = uidToEntity(spell->caster); + if ( caster ) + { + if ( spellEffectDominate(*my, *element, *caster, parent) ) + { + //Success + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_acidSpray.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + spellEffectAcid(*my, *element, parent, preResistanceDamage, resistance); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_poison.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + spellEffectPoison(*my, *element, parent, preResistanceDamage, resistance); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_sprayWeb.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + spellEffectSprayWeb(*my, *element, parent, resistance); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_stealWeapon.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + spellEffectStealWeapon(*my, *element, parent, resistance); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_drainSoul.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + spellEffectDrainSoul(*my, *element, parent, preResistanceDamage, resistance); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_charmMonster.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + spellEffectCharmMonster(*my, *element, parent, resistance, static_cast(my->actmagicCastByMagicstaff)); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_telePull.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + if ( spellEffectTeleportPull(my, *element, parent, hit.entity, resistance) ) + { + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_shadowTag.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + spellEffectShadowTag(*my, *element, parent, resistance); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_demonIllusion.element_internal_name) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( caster ) + { + if ( spellEffectDemonIllusion(*my, *element, parent, hit.entity, resistance) ) + { + magicOnEntityHit(parent, my, hit.entity, hitstats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + } + } + + if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_SPHERE_SILENCE].element_internal_name) ) + { + real_t spawnx = my->x; + real_t spawny = my->y; + Entity* follow = nullptr; + if ( !hit.entity ) + { + if ( hit.mapx >= 0 && hit.mapx < map.width - 1 && hit.mapy >= 1 && hit.mapy < map.height - 1 ) + { + real_t x = my->x - 4.0 * cos(my->yaw); + real_t y = my->y - 4.0 * sin(my->yaw); + int mapx = x / 16; + int mapy = y / 16; + if ( mapx >= 1 && mapx < map.width - 2 && mapy >= 1 && mapy < map.height - 2 ) + { + spawnx = x; + spawny = y; + } + } + } + else + { + if ( (hit.entity->behavior == &actMonster && !mimic) || hit.entity->behavior == &actPlayer ) + { + spawnx = hit.entity->x; + spawny = hit.entity->y; + follow = hit.entity; + } + else if ( hit.entity->behavior == &actChest || mimic ) + { + spawnx = hit.entity->x; + spawny = hit.entity->y; + follow = hit.entity; + } + } + + + createRadiusMagic(spell->ID, uidToEntity(spell->caster), spawnx, spawny, + std::max(16, std::min(255, getSpellEffectDurationSecondaryFromID(SPELL_SPHERE_SILENCE, parent, nullptr, my))), + getSpellEffectDurationFromID(SPELL_SPHERE_SILENCE, parent, nullptr, my), + follow); + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_SHADE_BOLT].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_WONDERLIGHT].element_internal_name) ) + { + real_t spawnx = my->x; + real_t spawny = my->y; + Uint32 lightParent = 0; + if ( !hit.entity ) + { + if ( hit.mapx >= 0 && hit.mapx < map.width - 1 && hit.mapy >= 1 && hit.mapy < map.height - 1 ) + { + real_t x = my->x - 4.0 * cos(my->yaw); + real_t y = my->y - 4.0 * sin(my->yaw); + int mapx = x / 16; + int mapy = y / 16; + if ( mapx >= 1 && mapx < map.width - 2 && mapy >= 1 && mapy < map.height - 2 ) + { + spawnx = x; + spawny = y; + } + } + } + else + { + if ( (hit.entity->behavior == &actMonster && !mimic) || hit.entity->behavior == &actPlayer ) + { + spawnx = hit.entity->x; + spawny = hit.entity->y; + lightParent = hit.entity->getUID(); + if ( spell->ID == SPELL_SHADE_BOLT ) + { + if ( hit.entity->behavior == &actMonster + && hit.entity->setEffect(EFF_BLIND, true, element->duration, false) ) + { + if ( hit.entity->behavior == &actMonster && !hit.entity->isBossMonster() ) + { + hit.entity->monsterReleaseAttackTarget(); + } + if ( hit.entity->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 0); + messagePlayerColor(hit.entity->skill[2], MESSAGE_COMBAT, color, Language::get(765)); + } + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerMonsterEvent(parent->skill[2], color, *hitstats, Language::get(3878), Language::get(3879), MSG_COMBAT); + } + } + } + } + else if ( hit.entity->behavior == &actChest || mimic ) + { + spawnx = hit.entity->x; + spawny = hit.entity->y; + lightParent = hit.entity->getUID(); + } + } + + Entity* entity = newEntity(spell->ID == SPELL_SHADE_BOLT ? 1801 : 1802, 1, map.entities, nullptr); // black magic ball + entity->parent = lightParent; // who to follow + entity->x = spawnx; + entity->y = spawny; + entity->z = my->z; + entity->skill[7] = -5; //Base z. + entity->fskill[1] = entity->skill[7] - entity->z; + entity->fskill[2] = 0.0; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = my->yaw + PI; + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + entity->behavior = &actMagiclightBall; + entity->skill[4] = spawnx; //Store what x it started shooting out from the player at. + entity->skill[5] = spawny; //Store what y it started shooting out from the player at. + entity->skill[0] = 1; // set init to not shoot out + + entity->skill[12] = getSpellEffectDurationSecondaryFromID(spell->ID, parent, nullptr, my); + node_t* spellnode = list_AddNodeLast(&entity->children); + spellnode->element = copySpell(getSpellFromID(spell->ID == SPELL_SHADE_BOLT ? SPELL_DEEP_SHADE : SPELL_LIGHT)); //We need to save the spell since this is a channeled spell. + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = spell->caster; + ((spell_t*)spellnode->element)->magicstaff = true; + spellnode->deconstructor = &spellDeconstructor; + playSoundEntity(entity, 165, 128); + } + + if ( hitstats ) + { + if ( player >= 0 ) + { + entityHealth -= hitstats->HP; + if ( entityHealth > 0 ) + { + // entity took damage, shake screen. + if ( multiplayer == SERVER && player > 0 ) + { + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 10; // turns into .1 + net_packet->data[5] = 10; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + else if (player == 0 || (splitscreen && player > 0) ) + { + cameravars[player].shakex += .1; + cameravars[player].shakey += 10; + } + } + } + else + { + if ( parent && parent->behavior == &actPlayer ) + { + if ( hitstats->HP <= 0 ) + { + if ( hitstats->type == SCARAB ) + { + // killed a scarab with magic. + steamAchievementEntity(parent, "BARONY_ACH_THICK_SKULL"); + } + if ( my->actmagicMirrorReflected == 1 && static_cast(my->actmagicMirrorReflectedCaster) == hit.entity->getUID() ) + { + // killed a monster with it's own spell with mirror reflection. + steamAchievementEntity(parent, "BARONY_ACH_NARCISSIST"); + } + if ( stats[parent->skill[2]] && stats[parent->skill[2]]->playerRace == RACE_INSECTOID && stats[parent->skill[2]]->stat_appearance == 0 ) + { + if ( !achievementObserver.playerAchievements[parent->skill[2]].gastricBypass ) + { + if ( achievementObserver.playerAchievements[parent->skill[2]].gastricBypassSpell.first == spell->ID ) + { + Uint32 oldTicks = achievementObserver.playerAchievements[parent->skill[2]].gastricBypassSpell.second; + if ( parent->ticks - oldTicks < TICKS_PER_SECOND * 5 ) + { + steamAchievementEntity(parent, "BARONY_ACH_GASTRIC_BYPASS"); + achievementObserver.playerAchievements[parent->skill[2]].gastricBypass = true; + } + } + } + } + } + } + } + } + + if ( my->actmagicProjectileArc > 0 ) + { + Entity* caster = uidToEntity(spell->caster); + if ( spell->ID == SPELL_SPORE_BOMB || spell->ID == SPELL_MYCELIUM_BOMB ) + { + floorMagicCreateSpores(hit.entity, my->x, my->y, parent, preResistanceDamage, spell->ID); + } + else + { + spawnMagicTower(caster, my->x, my->y, spell->ID, nullptr, true); + } + } + + if ( (spell->ID == SPELL_METEOR || spell->ID == SPELL_METEOR_SHOWER) + && (!strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR_SHOWER].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR].element_internal_name)) ) + { + Entity* caster = uidToEntity(spell->caster); + if ( Entity* aoe = createSpellExplosionArea(spell->ID, caster, my->x, my->y, my->z, 32.0, preResistanceDamage, hit.entity) ) + { + aoe->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + aoe->actmagicFromSpellbook = my->actmagicFromSpellbook; + } + } + + if ( spell->ID == SPELL_SCEPTER_BLAST || spell->ID == SPELL_BLOOD_WAVES || spell->ID == SPELL_HOLY_BEAM ) + { + if ( hit.entity && ((hit.entity->behavior == &actMonster && !mimic) || hit.entity->behavior == &actPlayer) ) + { + // phase through + } + else + { + my->removeLightField(); + if ( my->mynode ) + { + list_RemoveNode(my->mynode); + } + } + } + else if ( !(my->actmagicIsOrbiting == 2) ) + { + my->removeLightField(); + if ( my->mynode ) + { + list_RemoveNode(my->mynode); + } + } + return; + } + + if ( my->actmagicSpray == 1 ) + { + my->vel_x = my->vel_x * .95; + my->vel_y = my->vel_y * .95; + } + } + + //Go down two levels to the next element. This will need to get re-written shortly. + node = spell->elements.first; + element = (spellElement_t*)node->element; + //element = (spellElement_t *)spell->elements->first->element; + //element = (spellElement_t *)element->elements->first->element; //Go down two levels to the second element. + node = element->elements.first; + element = (spellElement_t*)node->element; + //if (!strcmp(element->element_internal_name, spellElement_fire.element_internal_name) + // || !strcmp(element->element_internal_name, spellElement_lightning.element_internal_name)) + if (1) + { + //Make the ball light up stuff as it travels. + if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, false)); + } + + if ( flickerLights ) + { + //Magic light ball will never flicker if this setting is disabled. + lightball_flicker++; + } + my->skill[2] = -11; // so clients know to create a light field + + if (lightball_flicker > 5) + { + lightball_lighting = (lightball_lighting == 1) + 1; + + if (lightball_lighting == 1) + { + my->removeLightField(); + if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, false)); + } + } + else + { + my->removeLightField(); + if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, true)); + } + } + lightball_flicker = 0; + } + } + else + { + my->skill[2] = -12; // so clients know to simply spawn particles + } + + // spawn particles + if ( my->actmagicSpray == 1 ) + { + } + else if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + spawnBasicMagicParticleForMissile(my); + } + + spawnAdditionalParticleForMissile(my); + } +} + +void actMagicClient(Entity* my) +{ + my->removeLightField(); + if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, false)); + } + + if ( flickerLights ) + { + //Magic light ball will never flicker if this setting is disabled. + lightball_flicker++; + } + my->skill[2] = -11; // so clients know to create a light field + + if (lightball_flicker > 5) + { + lightball_lighting = (lightball_lighting == 1) + 1; + + if (lightball_lighting == 1) + { + my->removeLightField(); + if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, false)); + } + } + else + { + my->removeLightField(); + if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, true)); + } + } + lightball_flicker = 0; + } + + // spawn particles + if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + spawnBasicMagicParticleForMissile(my); + } + + spawnAdditionalParticleForMissile(my); +} + +void actMagicClientNoLight(Entity* my) +{ + // simply spawn particles + if ( !my->flags[INVISIBLE] || my->flags[INVISIBLE_DITHER] ) + { + if ( *cvar_magic_fx_use_vismap && !intro ) + { + int x = my->x / 16.0; + int y = my->y / 16.0; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) + { + if ( Entity* particle = spawnMagicParticle(my) ) + { + particle->flags[SPRITE] = my->flags[SPRITE]; + particle->flags[INVISIBLE] = my->flags[INVISIBLE]; + particle->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + } + break; + } + } + } + } + else + { + if ( Entity* particle = spawnMagicParticle(my) ) + { + particle->flags[SPRITE] = my->flags[SPRITE]; + particle->flags[INVISIBLE] = my->flags[INVISIBLE]; + particle->flags[INVISIBLE_DITHER] = my->flags[INVISIBLE_DITHER]; + } + } + } +} + +void actMagicParticle(Entity* my) +{ + my->x += my->vel_x; + my->y += my->vel_y; + my->z += my->vel_z; + if ( my->sprite == 943 || my->sprite == 979 ) + { + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + } + else if ( my->sprite == 1764 ) // lightning impact + { + if ( my->ticks > 5 ) + { + my->scalex -= 0.1; + my->scaley -= 0.1; + my->scalez -= 0.1; + if ( my->ticks > 10 ) + { + //list_RemoveNode(my->mynode); + //return; + } + } + } + else if ( my->sprite == 1787 || (my->sprite == 245 && my->flags[SPRITE]) ) + { + // grease droplet + my->vel_z += 0.04; + my->scalex -= std::max(0.01, my->scalex * 0.05); + my->scaley -= std::max(0.01, my->scaley * 0.05); + my->scalez -= std::max(0.01, my->scalez * 0.05); + if ( my->z >= 8.0 ) + { + list_RemoveNode(my->mynode); + return; + } + } + else if ( my->sprite == 1479 ) + { + my->scalex -= 0.025; + my->scaley -= 0.025; + my->scalez -= 0.025; + my->pitch += 0.25; + my->yaw += 0.25; + //if ( my->parent == 0 && local_rng.rand() % 10 == 0 ) + //{ + // if ( Entity* particle = spawnMagicParticle(my) ) + // { + // particle->parent = my->getUID(); + // particle->x = my->x; + // particle->y = my->y; + // //particle->z = my->z; + // } + //} + } + else if ( my->sprite == 1866 || my->sprite == 2374 ) + { + my->scalex -= 0.01; + my->scaley -= 0.01; + my->scalez -= 0.01; + } + else if ( my->sprite == 2191 ) + { + my->scalex -= 0.025; + my->scaley -= 0.025; + my->scalez -= 0.025; + } + else if ( my->sprite == 2364 || my->sprite == 2365 || my->sprite == 2366 ) + { + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + + my->pitch += 0.25; + //my->vel_z += 0.04; + } + else if ( my->sprite == 2406 || my->sprite == 2407 ) + { + my->scalex -= 0.025; + my->scaley -= 0.025; + my->scalez -= 0.025; + if ( my->sprite == 2406 ) + { + my->yaw += 0.25; + my->vel_x *= 0.9; + my->vel_y *= 0.9; + my->vel_z *= 0.9; + } + } + else if ( my->sprite == 2209 ) + { + my->scalex -= 0.025; + my->scaley -= 0.025; + my->scalez -= 0.025; + } + else if ( my->sprite == 2410 ) + { + my->scalex -= 0.025; + my->scaley -= 0.025; + my->scalez -= 0.025; + my->roll += 0.2; + + my->z += my->fskill[0]; + } + else if ( my->sprite == 2363 ) + { + my->scalex -= 0.05 * (1.0 - my->fskill[1]); + my->scaley -= 0.05 * (1.0 - my->fskill[1]); + my->scalez -= 0.05 * (1.0 - my->fskill[1]); + } + else if ( my->sprite == 262 && my->flags[SPRITE] ) + { + my->scalex -= 0.025; + my->scaley -= 0.025; + my->scalez -= 0.025; + } + else if ( my->sprite == 2192 || my->sprite == 2193 || my->sprite == 2210 || my->sprite == 2211 ) + { + my->scalex -= 0.025; + my->scaley -= 0.025; + my->scalez -= 0.025; + my->pitch += 0.1; + } + else if ( my->sprite >= 2152 && my->sprite <= 2157 ) + { + my->scalex -= std::max(0.01, my->fskill[1]); + my->scaley -= std::max(0.01, my->fskill[1]); + my->scalez -= std::max(0.01, my->fskill[1]); + my->vel_z += my->fskill[0]; + + if ( (my->sprite == 2153 || my->sprite == 2155) && ticks % 4 == 0 ) + { + my->roll = (local_rng.rand() % 8) * PI / 4; + } + } + else if ( my->sprite >= PINPOINT_PARTICLE_START && my->sprite < PINPOINT_PARTICLE_END ) + { + my->scalex -= std::max(0.01, my->fskill[1]); + my->scaley -= std::max(0.01, my->fskill[1]); + my->scalez -= std::max(0.01, my->fskill[1]); + } + else if ( my->sprite >= 233 && my->sprite <= 244 && my->flags[SPRITE] ) + { + if ( my->skill[0] > 0 ) + { + --my->skill[0]; + } + else + { + my->scalex -= std::max(0.01, my->fskill[1]); + my->scaley -= std::max(0.01, my->fskill[1]); + my->scalez -= std::max(0.01, my->fskill[1]); + } + my->vel_z += my->fskill[0]; + if ( my->ticks % 4 == 0 ) + { + if ( my->sprite < 244 ) + { + ++my->sprite; + } + } + } + else if ( my->sprite >= 263 && my->sprite <= 274 && my->flags[SPRITE] ) + { + if ( my->skill[0] > 0 ) + { + --my->skill[0]; + } + else + { + my->scalex -= std::max(0.01, my->fskill[1]); + my->scaley -= std::max(0.01, my->fskill[1]); + my->scalez -= std::max(0.01, my->fskill[1]); + } + my->vel_z += my->fskill[0]; + if ( my->ticks % 4 == 0 ) + { + if ( my->sprite < 274 ) + { + ++my->sprite; + } + } + } + else if ( my->sprite >= 288 && my->sprite <= 299 && my->flags[SPRITE] ) + { + if ( my->skill[0] > 0 ) + { + --my->skill[0]; + } + else + { + my->scalex -= std::max(0.01, my->fskill[1]); + my->scaley -= std::max(0.01, my->fskill[1]); + my->scalez -= std::max(0.01, my->fskill[1]); + } + my->vel_z += my->fskill[0]; + if ( my->ticks % 4 == 0 ) + { + if ( my->sprite < 299 ) + { + ++my->sprite; + } + } + } + else + { + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + } + if ( my->scalex <= 0 ) + { + my->scalex = 0; + my->scaley = 0; + my->scalez = 0; + list_RemoveNode(my->mynode); + return; + } +} + +void actTouchCastThirdPersonParticle(Entity* my) +{ + my->x += my->vel_x; + my->y += my->vel_y; + my->z += my->vel_z; + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + if ( my->scalex <= 0 ) + { + my->scalex = 0; + my->scaley = 0; + my->scalez = 0; + list_RemoveNode(my->mynode); + return; + } +} + +void actHUDMagicParticle(Entity* my) +{ + my->x += my->vel_x; + my->y += my->vel_y; + my->z += my->vel_z; + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + if ( my->scalex <= 0 ) + { + my->scalex = 0; + my->scaley = 0; + my->scalez = 0; + list_RemoveNode(my->mynode); + return; + } +} + +void createEnsembleTargetParticleCircling(Entity* parent) +{ + if ( !parent ) { return; } + + // world particle + Entity* entity = newEntity(198, 1, map.entities, nullptr); + entity->yaw = (local_rng.rand() % 3) * 2 * PI / 3; + entity->x = parent->x; + entity->y = parent->y; + entity->z = 0.0; + double missile_speed = 4; + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->actmagicIsOrbiting = 2; + entity->actmagicOrbitDist = 4.0; + entity->actmagicOrbitStationaryCurrentDist = 0.0; + entity->actmagicOrbitStartZ = entity->z; + entity->actmagicOrbitVerticalSpeed = -0.025; + entity->actmagicOrbitVerticalDirection = 1; + entity->actmagicOrbitLifetime = TICKS_PER_SECOND; + entity->actmagicOrbitStationaryX = entity->x; + entity->actmagicOrbitStationaryY = entity->y; + entity->vel_z = -0.1; + entity->scalex = 0.3; + entity->scaley = 0.3; + entity->scalez = 0.3; + entity->behavior = &actMagicParticleEnsembleCircling; + + entity->flags[BRIGHT] = true; + entity->flags[SPRITE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = false; + //entity->flags[OVERDRAW] = true; + entity->skill[11] = parent->behavior == &actPlayer ? parent->skill[2] : -1; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); +} + +void createEnsembleHUDParticleCircling(Entity* parent) +{ + if ( !parent ) { return; } + + if ( (parent->behavior == &actPlayer && players[parent->skill[2]]->isLocalPlayer() && parent->skill[3] == 0) + || parent->behavior == &actDeathGhost ) + { + // create overdraw HUD particle + Entity* entity = newEntity(198, 1, map.entities, nullptr); + float x = 6 * 10; + float y = 0.1; + float z = 7; + entity->yaw = (local_rng.rand() % 3) * 2 * PI / 3; + entity->x = x; + entity->y = y; + entity->z = z; + double missile_speed = 4; + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->actmagicIsOrbiting = 2; + entity->actmagicOrbitDist = 16.0; + entity->actmagicOrbitStationaryCurrentDist = 0.0; + entity->actmagicOrbitStartZ = entity->z; + entity->actmagicOrbitVerticalSpeed = -0.3; + entity->actmagicOrbitVerticalDirection = 1; + entity->actmagicOrbitLifetime = TICKS_PER_SECOND; + entity->actmagicOrbitStationaryX = x; + entity->actmagicOrbitStationaryY = y; + entity->vel_z = -0.1; + entity->behavior = &actHUDMagicParticleCircling; + + entity->flags[BRIGHT] = true; + entity->flags[SPRITE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[OVERDRAW] = true; + entity->skill[11] = (parent->behavior == &actPlayer || parent->behavior == &actDeathGhost) ? parent->skill[2] : -1; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +void actMagicParticleEnsembleCircling(Entity* my) +{ + real_t turnRate = 0.25; + my->yaw += 0.2; + my->x = my->actmagicOrbitStationaryX + my->actmagicOrbitStationaryCurrentDist * cos(my->yaw); + my->y = my->actmagicOrbitStationaryY + my->actmagicOrbitStationaryCurrentDist * sin(my->yaw); + my->actmagicOrbitStationaryCurrentDist = + std::min(my->actmagicOrbitStationaryCurrentDist + 0.5, static_cast(my->actmagicOrbitDist)); + my->z += my->vel_z * my->actmagicOrbitVerticalDirection; + + my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); + my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; + my->roll = std::max(my->roll, -PI / 4); + + --my->actmagicOrbitLifetime; + if ( my->actmagicOrbitLifetime <= 0 ) + { + list_RemoveNode(my->mynode); + return; + } +} + +void actHUDMagicParticleCircling(Entity* my) +{ + int turnRate = 4; + my->yaw += 0.2; + turnRate = 4; + my->x = my->actmagicOrbitStationaryX + my->actmagicOrbitStationaryCurrentDist * cos(my->yaw); + my->y = my->actmagicOrbitStationaryY + my->actmagicOrbitStationaryCurrentDist * sin(my->yaw); + my->actmagicOrbitStationaryCurrentDist = + std::min(my->actmagicOrbitStationaryCurrentDist + 0.5, static_cast(my->actmagicOrbitDist)); + my->z += my->vel_z * my->actmagicOrbitVerticalDirection; + + my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); + my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; + my->roll = std::max(my->roll, -PI / 4); + + --my->actmagicOrbitLifetime; + if ( my->actmagicOrbitLifetime <= 0 ) + { + list_RemoveNode(my->mynode); + return; + } + + if ( !my->flags[SPRITE] ) + { + Entity* entity; + + entity = newEntity(my->sprite, 1, map.entities, nullptr); //Particle entity. + + entity->x = my->x + (local_rng.rand() % 50 - 25) / 200.f; + entity->y = my->y + (local_rng.rand() % 50 - 25) / 200.f; + entity->z = my->z + (local_rng.rand() % 50 - 25) / 200.f; + entity->scalex = 0.7; + entity->scaley = 0.7; + entity->scalez = 0.7; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = my->yaw; + entity->pitch = my->pitch; + entity->roll = my->roll; + entity->flags[NOUPDATE] = true; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[OVERDRAW] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->behavior = &actHUDMagicParticle; + entity->skill[11] = my->skill[11]; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +void actMagicParticleCircling2(Entity* my) +{ + int turnRate = 4; + my->yaw += 0.2; + turnRate = 4; + my->x = my->actmagicOrbitStationaryX + my->actmagicOrbitStationaryCurrentDist * cos(my->yaw); + my->y = my->actmagicOrbitStationaryY + my->actmagicOrbitStationaryCurrentDist * sin(my->yaw); + my->actmagicOrbitStationaryCurrentDist = + std::min(my->actmagicOrbitStationaryCurrentDist + 0.5, static_cast(my->actmagicOrbitDist)); + my->z += my->vel_z * my->actmagicOrbitVerticalDirection; + + my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); + my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; + my->roll = std::max(my->roll, -PI / 4); + + --my->actmagicOrbitLifetime; + if ( my->actmagicOrbitLifetime <= 0 ) + { + list_RemoveNode(my->mynode); + return; + } + + if ( !my->flags[SPRITE] ) + { + Entity* entity; + + entity = newEntity(my->sprite, 1, map.entities, nullptr); //Particle entity. + + entity->x = my->x + (local_rng.rand() % 50 - 25) / 200.f; + entity->y = my->y + (local_rng.rand() % 50 - 25) / 200.f; + entity->z = my->z + (local_rng.rand() % 50 - 25) / 200.f; + entity->scalex = 0.7; + entity->scaley = 0.7; + entity->scalez = 0.7; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = my->yaw; + entity->pitch = my->pitch; + entity->roll = my->roll; + entity->flags[NOUPDATE] = true; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->behavior = &actHUDMagicParticle; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +Entity* spawnMagicParticle(Entity* parentent) +{ + if ( !parentent ) + { + return nullptr; + } + Entity* entity; + + entity = newEntity(parentent->sprite, 1, map.entities, nullptr); //Particle entity. + + entity->x = parentent->x + (local_rng.rand() % 50 - 25) / 20.f; + entity->y = parentent->y + (local_rng.rand() % 50 - 25) / 20.f; + entity->z = parentent->z + (local_rng.rand() % 50 - 25) / 20.f; + entity->parent = 0; + entity->scalex = 0.7; + entity->scaley = 0.7; + entity->scalez = 0.7; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = parentent->yaw; + entity->pitch = parentent->pitch; + entity->roll = parentent->roll; + entity->flags[NOUPDATE] = true; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->behavior = &actMagicParticle; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + + return entity; +} + +Entity* spawnMagicParticleCustom(Entity* parentent, int sprite, real_t scale, real_t spreadReduce) +{ + if ( !parentent ) + { + return nullptr; + } + Entity* entity; + + entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + + int size = 50 / spreadReduce; + entity->x = parentent->x + (local_rng.rand() % size - size / 2) / 20.f; + entity->y = parentent->y + (local_rng.rand() % size - size / 2) / 20.f; + entity->z = parentent->z + (local_rng.rand() % size - size / 2) / 20.f; + entity->scalex = scale; + entity->scaley = scale; + entity->scalez = scale; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = parentent->yaw; + entity->pitch = parentent->pitch; + entity->roll = parentent->roll; + entity->flags[NOUPDATE] = true; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->behavior = &actMagicParticle; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + + return entity; +} + +void spawnMagicEffectParticles(Sint16 x, Sint16 y, Sint16 z, Uint32 sprite) +{ + int c; + if ( multiplayer == SERVER ) + { + for ( c = 1; c < MAXPLAYERS; c++ ) + { + if ( client_disconnected[c] || players[c]->isLocalPlayer() ) + { + continue; + } + strcpy((char*)net_packet->data, "MAGE"); + SDLNet_Write16(x, &net_packet->data[4]); + SDLNet_Write16(y, &net_packet->data[6]); + SDLNet_Write16(z, &net_packet->data[8]); + SDLNet_Write32(sprite, &net_packet->data[10]); + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 14; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } + } + + // boosty boost + for ( c = 0; c < 10; c++ ) + { + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->x = x - 5 + local_rng.rand() % 11; + entity->y = y - 5 + local_rng.rand() % 11; + entity->z = z - 10 + local_rng.rand() % 21; + entity->scalex = 0.7; + entity->scaley = 0.7; + entity->scalez = 0.7; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = (local_rng.rand() % 360) * PI / 180.f; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->behavior = &actMagicParticle; + entity->vel_z = -1; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +void createParticleCircling(Entity* parent, int duration, int sprite) +{ + if ( !parent ) + { + return; + } + + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x; + entity->y = parent->y; + entity->focalx = 8; + entity->z = -7; + entity->vel_z = 0.15; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = duration; + entity->skill[1] = -1; + //entity->scalex = 0.01; + //entity->scaley = 0.01; + entity->fskill[0] = -0.1; + entity->behavior = &actParticleCircle; + entity->flags[PASSABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->setUID(-3); + + real_t tmp = entity->yaw; + + entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x; + entity->y = parent->y; + entity->focalx = 8; + entity->z = -7; + entity->vel_z = 0.15; + entity->yaw = tmp + (2 * PI / 3); + entity->particleDuration = duration; + entity->skill[1] = -1; + //entity->scalex = 0.01; + //entity->scaley = 0.01; + entity->fskill[0] = -0.1; + entity->behavior = &actParticleCircle; + entity->flags[PASSABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->setUID(-3); + + entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x; + entity->y = parent->y; + entity->focalx = 8; + entity->z = -7; + entity->vel_z = 0.15; + entity->yaw = tmp - (2 * PI / 3); + entity->particleDuration = duration; + entity->skill[1] = -1; + //entity->scalex = 0.01; + //entity->scaley = 0.01; + entity->fskill[0] = -0.1; + entity->behavior = &actParticleCircle; + entity->flags[PASSABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->setUID(-3); + + entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x; + entity->y = parent->y; + entity->focalx = 16; + entity->z = -12; + entity->vel_z = 0.2; + entity->yaw = tmp; + entity->particleDuration = duration; + entity->skill[1] = -1; + //entity->scalex = 0.01; + //entity->scaley = 0.01; + entity->fskill[0] = 0.1; + entity->behavior = &actParticleCircle; + entity->flags[PASSABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->setUID(-3); + + entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x; + entity->y = parent->y; + entity->focalx = 16; + entity->z = -12; + entity->vel_z = 0.2; + entity->yaw = tmp + (2 * PI / 3); + entity->particleDuration = duration; + entity->skill[1] = -1; + //entity->scalex = 0.01; + //entity->scaley = 0.01; + entity->fskill[0] = 0.1; + entity->behavior = &actParticleCircle; + entity->flags[PASSABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->setUID(-3); + + entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x; + entity->y = parent->y; + entity->focalx = 16; + entity->z = -12; + entity->vel_z = 0.2; + entity->yaw = tmp - (2 * PI / 3); + entity->particleDuration = duration; + entity->skill[1] = -1; + //entity->scalex = 0.01; + //entity->scaley = 0.01; + entity->fskill[0] = 0.1; + entity->behavior = &actParticleCircle; + entity->flags[PASSABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->setUID(-3); +} + +#define PARTICLE_LIFE my->skill[0] + +void actParticleCircle(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + return; + } + else + { + --PARTICLE_LIFE; + my->yaw += my->fskill[0]; + if ( my->fskill[0] < 0.4 && my->fskill[0] > (-0.4) ) + { + my->fskill[0] = my->fskill[0] * 1.05; + } + my->z += my->vel_z; + if ( my->focalx > 0.05 ) + { + if ( my->vel_z == 0.15 ) + { + my->focalx = my->focalx * 0.97; + } + else + { + my->focalx = my->focalx * 0.97; + } + } + my->scalex *= 0.995; + my->scaley *= 0.995; + my->scalez *= 0.995; + } +} + +void createParticleDot(Entity* parent) +{ + if ( !parent ) + { + return; + } + for ( int c = 0; c < 50; c++ ) + { + Entity* entity = newEntity(576, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x + (-4 + local_rng.rand() % 9); + entity->y = parent->y + (-4 + local_rng.rand() % 9); + entity->z = 7.5 + local_rng.rand()%50; + entity->vel_z = -1; + //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = 10 + local_rng.rand()% 50; + entity->behavior = &actParticleDot; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +Entity* createParticleBolas(Entity* parent, int sprite, int duration, Item* item) +{ + if ( !parent ) { return nullptr; } + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->parent = parent->getUID(); + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = duration; + entity->behavior = &actParticleBolas; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + + if ( multiplayer != CLIENT && item ) + { + entity->skill[10] = item->type; + entity->skill[11] = item->status; + entity->skill[12] = item->beatitude; + entity->skill[13] = item->count; + entity->skill[14] = item->appearance; + entity->skill[15] = item->identified; + entity->skill[16] = item->ownerUid; + } + return entity; +} + +Entity* createParticleAestheticOrbit(Entity* parent, int sprite, int duration, int effectType) +{ + if ( effectType == PARTICLE_EFFECT_NULL_PARTICLE + || effectType == PARTICLE_EFFECT_IGNITE_ORBIT + || effectType == PARTICLE_EFFECT_IGNITE_ORBIT_LOOP + || effectType == PARTICLE_EFFECT_METEOR_STATIONARY_ORBIT + || effectType == PARTICLE_EFFECT_BLOOD_BUBBLE ) + { + // no need parent + } + else + { + if ( !parent ) + { + return nullptr; + } + } + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->actmagicOrbitDist = 6; + entity->yaw = parent ? parent->yaw : 0.0; + entity->x = parent ? parent->x + entity->actmagicOrbitDist * cos(entity->yaw) : 0.0; + entity->y = parent ? parent->y + entity->actmagicOrbitDist * sin(entity->yaw) : 0.0; + entity->z = parent ? parent->z : 0.0; + entity->skill[1] = effectType; + entity->parent = parent ? parent->getUID() : 0; + //entity->vel_z = -1; + //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = duration; + entity->fskill[0] = entity->x; + entity->fskill[1] = entity->y; + entity->behavior = &actParticleAestheticOrbit; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( effectType == PARTICLE_EFFECT_DEFY_FLESH_ORBIT || effectType == PARTICLE_EFFECT_SMITE_PINPOINT ) + { + TileEntityList.addEntity(*entity); + } + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + return entity; +} + +void createParticleRock(Entity* parent, int sprite, bool light) +{ + if ( !parent ) + { + return; + } + for ( int c = 0; c < 5; c++ ) + { + Entity* entity = newEntity(sprite != -1 ? sprite : 78, 1, map.entities, nullptr); //Particle entity. + if ( entity->sprite == 1336 ) + { + entity->sprite = 1336 + local_rng.rand() % 3; + } + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x + (-4 + local_rng.rand() % 9); + entity->y = parent->y + (-4 + local_rng.rand() % 9); + entity->z = 7.5; + entity->yaw = c * 2 * PI / 5;//(local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + + entity->vel_x = 0.2 * cos(entity->yaw); + entity->vel_y = 0.2 * sin(entity->yaw); + entity->vel_z = 3;// 0.25 - (local_rng.rand() % 5) / 10.0; + + entity->skill[0] = 50; // particle life + entity->skill[1] = 0; // particle direction, 0 = upwards, 1 = downwards. + + entity->behavior = &actParticleRock; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + if ( !light ) + { + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + } + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +void createParticleShatteredGem(real_t x, real_t y, real_t z, int sprite, Entity* parent) +{ + for ( int c = 0; c < 5; c++ ) + { + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + if ( parent ) + { + entity->x = parent->x + (-4 + local_rng.rand() % 9); + entity->y = parent->y + (-4 + local_rng.rand() % 9); + } + else + { + entity->x = x + (-4 + local_rng.rand() % 9); + entity->y = y + (-4 + local_rng.rand() % 9); + } + entity->z = z; + entity->yaw = c * 2 * PI / 5;//(local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + + entity->vel_x = 0.2 * cos(entity->yaw); + entity->vel_y = 0.2 * sin(entity->yaw); + entity->vel_z = 3;// 0.25 - (local_rng.rand() % 5) / 10.0; + + real_t scale = .4; + entity->scalex = scale; + entity->scaley = scale; + entity->scalez = scale; + + entity->skill[0] = 50; // particle life + entity->skill[1] = 0; // particle direction, 0 = upwards, 1 = downwards. + + entity->behavior = &actParticleRock; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +void actParticleRock(Entity* my) +{ + if ( PARTICLE_LIFE < 0 || my->z > 10 ) + { + list_RemoveNode(my->mynode); + } + else + { + --PARTICLE_LIFE; + my->x += my->vel_x; + my->y += my->vel_y; + + my->roll += 0.1; + + if ( my->vel_z < 0.01 ) + { + my->skill[1] = 1; // start moving downwards + my->vel_z = 0.1; + } + + if ( my->skill[1] == 0 ) // upwards motion + { + my->z -= my->vel_z; + my->vel_z *= 0.7; + } + else // downwards motion + { + my->z += my->vel_z; + my->vel_z *= 1.1; + } + } + return; +} + +void actParticleDot(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + } + else + { + --PARTICLE_LIFE; + my->z += my->vel_z; + //my->z -= 0.01; + } + return; +} + +void actParticleBolas(Entity* my) +{ + Entity* parent = uidToEntity(my->parent); + bool destroy = false; + if ( !parent ) + { + destroy = true; + } + if ( PARTICLE_LIFE < 0 ) + { + destroy = true; + } + + if ( parent ) + { + my->x = parent->x; + my->y = parent->y; + my->z = parent->z + 1.0; + } + + if ( my->skill[1] == 0 ) + { + my->skill[1] = 1; + my->fskill[1] = 1.0; + } + + my->z = std::min(6.0, my->z); + my->fskill[0] += 0.25; + my->yaw += 0.25 * sin(my->fskill[1]); + my->fskill[1] *= 0.9; + my->scalex = 1.1 + 0.1 * sin(my->fskill[0]); + my->scaley = 1.1 + 0.1 * sin(my->fskill[0]); + my->scalez = 1.1 - 0.1 * sin(my->fskill[0]); + + if ( destroy ) + { + if ( multiplayer != CLIENT && my->skill[10] > 0 && my->skill[12] >= 0 ) // not cursed + { + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. + entity->flags[INVISIBLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + entity->sizex = 2; + entity->sizey = 2; + entity->yaw = my->yaw; + entity->pitch = my->pitch; + entity->roll = my->roll; + entity->vel_x = 0.25; + entity->vel_y = 0.25; + entity->vel_z = -0.5; + entity->behavior = &actItem; + entity->skill[10] = my->skill[10]; + entity->skill[11] = my->skill[11]; + entity->skill[12] = my->skill[12]; + entity->skill[13] = my->skill[13]; + entity->skill[14] = my->skill[14]; + entity->skill[15] = my->skill[15]; + entity->parent = my->skill[16]; // owner UID + if ( Entity* owner = uidToEntity(entity->parent) ) + { + if ( Stat* stats = owner->getStats() ) + { + if ( stats->getEffectActive(EFF_RETURN_ITEM) ) + { + entity->itemReturnUID = owner->getUID(); + } + } + } + } + + if ( multiplayer != CLIENT ) + { + if ( Entity* parent = uidToEntity(my->skill[16]) ) + { + if ( parent->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdate(parent->skill[2], Compendium_t::CPDM_EFFECT_DURATION, BOLAS, my->ticks); + } + } + } + + list_RemoveNode(my->mynode); + return; + } + + --PARTICLE_LIFE; +} + +Uint32 nullParticleSfxTick = 0; +void actParticleAestheticOrbit(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + } + else + { + Entity* parent = uidToEntity(my->parent); + if ( !parent ) + { + if ( my->skill[1] == PARTICLE_EFFECT_NULL_PARTICLE + || my->skill[1] == PARTICLE_EFFECT_NULL_PARTICLE_NOSOUND + || my->skill[1] == PARTICLE_EFFECT_SHATTER_EARTH_ORBIT + || my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT + || my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT_LOOP + || my->skill[1] == PARTICLE_EFFECT_METEOR_STATIONARY_ORBIT + || my->skill[1] == PARTICLE_EFFECT_BLOOD_BUBBLE + || my->skill[1] == PARTICLE_EFFECT_SMITE_PINPOINT + || my->skill[1] == PARTICLE_EFFECT_FOCI_LIGHT + || my->skill[1] == PARTICLE_EFFECT_FOCI_DARK ) + { + // no need for parent + } + else + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + if ( my->skill[1] == PARTICLE_EFFECT_SPELLBOT_ORBIT ) + { + my->yaw = parent->yaw; + my->x = parent->x + 2 * cos(parent->yaw); + my->y = parent->y + 2 * sin(parent->yaw); + my->z = parent->z - 1.5; + Entity* particle = spawnMagicParticle(my); + if ( particle ) + { + particle->x = my->x + (-10 + local_rng.rand() % 21) / (50.f); + particle->y = my->y + (-10 + local_rng.rand() % 21) / (50.f); + particle->z = my->z + (-10 + local_rng.rand() % 21) / (50.f); + particle->scalex = my->scalex; + particle->scaley = my->scaley; + particle->scalez = my->scalez; + } + //spawnMagicParticle(my); + } + else if ( my->skill[1] == PARTICLE_EFFECT_SPELL_WEB_ORBIT ) + { + Stat* stats = parent->getStats(); + if ( my->sprite == 863 && (!stats || !stats->getEffectActive(EFF_WEBBED)) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + my->yaw += 0.2; + spawnMagicParticle(my); + my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); + my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); + } + else if ( my->skill[1] == PARTICLE_EFFECT_HOLY_FIRE ) + { + Stat* stats = parent->getStats(); + if ( stats && !stats->getEffectActive(EFF_HOLY_FIRE) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + + my->flags[INVISIBLE] = true; + my->flags[SPRITE] = true; + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, "blue_fire"); + + /*if ( my->ticks % 10 == 0 ) + { + }*/ + if ( my->ticks % 40 == 0 ) + { + if ( Entity* fx = spawnFlameSprites(parent, 288) ) + { + } + if ( multiplayer != CLIENT ) + { + Entity* caster = uidToEntity(my->skill[3]); + int burnDamage = getSpellDamageFromID(SPELL_HOLY_FIRE, caster, caster ? caster->getStats() : nullptr, + caster); + applyGenericMagicDamage(caster, parent, *my, SPELL_HOLY_FIRE, burnDamage, true, true); + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT || my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT_LOOP + || my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT_FOLLOW + || my->skill[1] == PARTICLE_EFFECT_RADIANT_ORBIT_FOLLOW + || my->skill[1] == PARTICLE_EFFECT_FLAMES_BURNING + || my->skill[1] == PARTICLE_EFFECT_HEAT_ORBIT_SPIN ) + { + //my->fskill[2] += 0.01; + my->yaw = my->fskill[2]; + my->yaw += (PI / 32) * sin(my->fskill[3]); + if ( my->skill[1] == PARTICLE_EFFECT_RADIANT_ORBIT_FOLLOW ) + { + my->fskill[3] += my->fskill[4]; + } + else if ( my->skill[1] == PARTICLE_EFFECT_HEAT_ORBIT_SPIN ) + { + my->yaw = my->fskill[2]; + my->fskill[2] += my->fskill[4]; + } + else + { + my->fskill[3] += 0.1; + } + + if ( my->skill[1] == PARTICLE_EFFECT_FLAMES_BURNING ) + { + if ( parent ) + { + my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); + my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); + my->z = parent->z; + my->z += local_rng.rand() % 7 - 4; + int sizex = parent->sizex; + int sizey = parent->sizey; + if ( parent->behavior == &actBell ) + { + my->x += parent->focalx * cos(parent->yaw) + parent->focaly * cos(parent->yaw + PI / 2); + my->y += parent->focalx * sin(parent->yaw) + parent->focaly * sin(parent->yaw + PI / 2); + } + else if ( parent->behavior == &actDoor ) + { + if ( abs(parent->focaly) > 0 ) + { + sizex = parent->sizey; + sizey = parent->sizex; + my->x += parent->focaly * cos(parent->yaw + PI / 2); + my->y += parent->focaly * sin(parent->yaw + PI / 2); + } + } + int sizeModX = local_rng.rand() % (sizex * 2 + 1) - sizex; + int sizeModY = local_rng.rand() % (sizey * 2 + 1) - sizey; + if ( abs(sizex) <= 2 ) + { + sizeModX += sizeModX > 0 ? 1 : -1; + } + if ( abs(sizey) <= 2 ) + { + sizeModY += sizeModY > 0 ? 1 : -1; + } + + if ( parent->behavior == &actPlayer ) + { + my->x += 1.0 * cos(parent->yaw + PI); + my->y += 1.0 * sin(parent->yaw + PI); + } + my->x += sizeModX; + my->y += sizeModY; + my->fskill[0] = my->x; + my->fskill[1] = my->y; + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT_FOLLOW ) + { + if ( parent ) + { + my->yaw = parent->yaw + PI; + my->yaw += (PI / 32) * sin(my->fskill[3]); + my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); + my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); + + if ( !my->actmagicNoLight ) + { + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, "magic_spray_orange_flicker"); + } + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_RADIANT_ORBIT_FOLLOW ) + { + if ( parent ) + { + my->yaw = parent->yaw + PI; + my->yaw += (PI / 32) * sin(my->fskill[3]) + my->fskill[5]; + my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); + my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); + + if ( !my->actmagicNoLight ) + { + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, "magic_spray_orange_flicker"); + } + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_HEAT_ORBIT_SPIN ) + { + if ( parent ) + { + my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); + my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); + /*if ( !my->actmagicNoLight ) + { + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, "magic_spray_orange_flicker"); + } + }*/ + } + } + else + { + my->x = my->fskill[0] + my->actmagicOrbitDist * cos(my->yaw); + my->y = my->fskill[1] + my->actmagicOrbitDist * sin(my->yaw); + } + my->z += my->vel_z; + + if ( ((my->sprite >= 233 && my->sprite <= 244) + || (my->sprite >= 263 && my->sprite <= 274) + || (my->sprite >= 288 && my->sprite <= 299)) && my->flags[SPRITE] ) + { + int endSprite = 244; + if ( (my->sprite >= 263 && my->sprite <= 274) ) + { + endSprite = 274; + } + else if ( (my->sprite >= 288 && my->sprite <= 299) ) + { + endSprite = 299; + } + if ( my->ticks % 4 == 0 ) + { + if ( my->sprite < endSprite ) + { + ++my->sprite; + if ( my->sprite == endSprite && my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT_LOOP ) + { + my->z = 7.5; + my->fskill[2] += PI / 4; + my->yaw = my->fskill[2]; + my->bNeedsRenderPositionInit = true; + } + } + } + + if ( my->ticks % 2 == 0 ) + { + bool doParticle = true; + if ( *cvar_magic_fx_use_vismap && !intro ) + { + if ( !parent + || (parent && parent->behavior != actPlayer + && parent->behavior != actPlayerLimb + && !parent->flags[OVERDRAW] + && !parent->flags[GENIUS]) ) + { + int x = my->x / 16.0; + int y = my->y / 16.0; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + bool anyVismap = false; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] && players[i]->isLocalPlayer() ) + { + if ( cameras[i].vismap && cameras[i].vismap[y + x * map.height] ) + { + anyVismap = true; + break; + } + } + } + if ( !anyVismap ) + { + doParticle = false; + } + } + } + } + + if ( doParticle ) + { + real_t spread = 1.0; + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, 1.0, spread) ) + { + fx->sprite = my->sprite; + fx->flags[SPRITE] = my->flags[SPRITE]; + fx->ditheringDisabled = true; + fx->fskill[1] = 0.025; // decay size + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + if ( my->skill[1] == PARTICLE_EFFECT_FLAMES_BURNING ) + { + fx->scalex = 0.5; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->skill[0] = 4 + (endSprite - fx->sprite) * 4; // life before decay + fx->fskill[1] = 0.5; // decay size + //fx->vel_z = -0.25; + //fx->fskill[0] = -0.005; //gravity + } + else if ( my->skill[1] == PARTICLE_EFFECT_RADIANT_ORBIT_FOLLOW ) + { + //fx->x = my->x; + //fx->y = my->y; + fx->scalex = my->scalex; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + //fx->fskill[1] = 0.5; // decay size + } + else if ( my->skill[1] == PARTICLE_EFFECT_HEAT_ORBIT_SPIN ) + { + //fx->flags[BRIGHT] = true; + fx->lightBonus = my->lightBonus; + } + /*if ( my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT || my->skill[1] == PARTICLE_EFFECT_IGNITE_ORBIT_LOOP ) + { + real_t dir = atan2(my->vel_y, my->vel_x); + fx->x -= 2.0 * cos(dir); + fx->y -= 2.0 * sin(dir); + }*/ + } + } + } + } + + if ( PARTICLE_LIFE <= 10 ) + { + my->scalex -= 0.1; + my->scaley -= 0.1; + my->scalez -= 0.1; + if ( my->scalex <= 0.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_SABOTAGE_ORBIT ) + { + my->yaw = my->fskill[2]; + my->yaw += (PI / 32) * sin(my->fskill[3]); + my->fskill[3] += 0.1; + my->x = my->fskill[0] + 4.0 * cos(my->yaw); + my->y = my->fskill[1] + 4.0 * sin(my->yaw); + my->z += my->vel_z; + + if ( my->ticks % 4 == 0 ) + { + my->sprite++; + if ( my->sprite > 274 ) + { + my->sprite = 263; + } + } + //spawnMagicParticleCustom(my, my->sprite, my->scalex, 1.0); + if ( PARTICLE_LIFE <= 10 ) + { + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + if ( my->scalex <= 0.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_CONFUSE_ORBIT ) + { + my->fskill[2] += 0.05; + my->yaw -= 0.05; + my->x = parent->x + my->actmagicOrbitDist * cos(my->fskill[2]); + my->y = parent->y + my->actmagicOrbitDist * sin(my->fskill[2]); + + Stat* stats = parent->getStats(); + if ( PARTICLE_LIFE <= 10 || (!stats || !stats->getEffectActive(EFF_CONFUSED)) ) + { + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + if ( my->scalex <= 0.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + else + { + my->scalex = std::min(my->scalex + 0.05, 0.5); + my->scaley = std::min(my->scaley + 0.05, 0.5); + my->scalez = std::min(my->scalez + 0.05, 0.5); + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_BLOOD_BUBBLE ) + { + my->z -= 0.1; + my->fskill[2] += my->fskill[3]; + my->x = my->actmagicOrbitStationaryX + my->actmagicOrbitDist * cos(my->fskill[2]); + my->y = my->actmagicOrbitStationaryY + my->actmagicOrbitDist * sin(my->fskill[2]); + + if ( PARTICLE_LIFE <= 40 ) + { + if ( my->ticks % 4 == 0 ) + { + if ( my->sprite < 286 ) + { + my->sprite++; + } + else if ( my->ticks % 16 == 0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + } + + if ( PARTICLE_LIFE <= 10 ) + { + my->scalex -= 0.0125; + my->scaley -= 0.0125; + my->scalez -= 0.0125; + if ( my->scalex <= 0.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + else + { + my->scalex = std::min(my->scalex + 0.0025, 0.15); + my->scaley = std::min(my->scaley + 0.0025, 0.15); + my->scalez = std::min(my->scalez + 0.0025, 0.15); + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_MAGICIANS_ARMOR_ORBIT ) + { + my->fskill[2] += 0.05; + my->yaw -= 0.05; + my->x = parent->x + my->actmagicOrbitDist * cos(my->fskill[2]); + my->y = parent->y + my->actmagicOrbitDist * sin(my->fskill[2]); + my->z += my->vel_z; + Stat* stats = parent->getStats(); + + if ( PARTICLE_LIFE <= 40 ) + { + if ( my->ticks % 8 == 0 ) + { + if ( my->sprite < 279 ) + { + my->sprite++; + } + else if ( my->ticks % 16 == 0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + } + + if ( PARTICLE_LIFE <= 10 || (!stats || !stats->getEffectActive(EFF_MAGICIANS_ARMOR)) ) + { + my->scalex -= 0.0125; + my->scaley -= 0.0125; + my->scalez -= 0.0125; + if ( my->scalex <= 0.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + else + { + my->scalex = std::min(my->scalex + 0.0025, 0.15); + my->scaley = std::min(my->scaley + 0.0025, 0.15); + my->scalez = std::min(my->scalez + 0.0025, 0.15); + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_GUARD_BODY_ORBIT + || my->skill[1] == PARTICLE_EFFECT_GUARD_SPIRIT_ORBIT + || my->skill[1] == PARTICLE_EFFECT_GUARD_DIVINE_ORBIT + || my->skill[1] == PARTICLE_EFFECT_BLOOD_WARD_ORBIT ) + { + my->fskill[2] += 0.05; + my->yaw -= 0.05; + my->x = parent->x + my->actmagicOrbitDist * cos(my->fskill[2]); + my->y = parent->y + my->actmagicOrbitDist * sin(my->fskill[2]); + my->z += my->vel_z; + Stat* stats = parent->getStats(); + int effectID = my->skill[1] == PARTICLE_EFFECT_GUARD_BODY_ORBIT ? EFF_GUARD_BODY + : (my->skill[1] == PARTICLE_EFFECT_GUARD_SPIRIT_ORBIT ? EFF_GUARD_SPIRIT + : (my->skill[1] == PARTICLE_EFFECT_GUARD_DIVINE_ORBIT ? EFF_DIVINE_GUARD + : (my->skill[1] == PARTICLE_EFFECT_BLOOD_WARD_ORBIT ? EFF_BLOOD_WARD : -1))); + if ( PARTICLE_LIFE <= 10 || (!stats || effectID < 0 || !stats->getEffectActive(effectID)) ) + { + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + if ( my->scalex <= 0.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + else + { + my->scalex = std::min(my->scalex + 0.05, 0.25); + my->scaley = std::min(my->scaley + 0.05, 0.25); + my->scalez = std::min(my->scalez + 0.05, 0.25); + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_MUSHROOM_SPELL ) + { + int mapx = static_cast(my->x) >> 4; + int mapy = static_cast(my->y) >> 4; + int mapIndex = (mapy)*MAPLAYERS + (mapx)*MAPLAYERS * map.height; + if ( mapx > 0 && mapy > 0 && mapx < map.width - 1 && mapy < map.height - 1 ) + { + if ( map.tiles[OBSTACLELAYER + mapIndex] ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + + spawnMagicParticle(my); + if ( my->ticks % 4 == 0 ) + { + my->yaw += PI / 2; + } + my->x = parent->x + (my->actmagicOrbitDist * my->fskill[2]) * cos(my->fskill[4] + my->fskill[3]); + my->y = parent->y + (my->actmagicOrbitDist * my->fskill[2]) * sin(my->fskill[4] + my->fskill[3]); + + my->fskill[2] = std::min(my->fskill[2] + 0.05, 1.0); + + if ( my->skill[3] == 0 ) + { + my->pitch = -PI + (3 * PI / 4) * sin(my->fskill[2] * PI / 2); + my->z = 0.0 - 7.55 * sin(my->fskill[2] * PI / 2); + if ( my->z <= -7.5 ) + { + my->z = -7.5; + my->skill[3] = 1; + my->vel_z = 0.0; + } + } + else if ( my->skill[3] == 1 ) + { + my->fskill[3] += 0.005; + my->vel_z += 0.02; + my->pitch = std::min(0.0, my->pitch + my->vel_z); + my->z += my->vel_z; + if ( my->z > 8.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_VORTEX_ORBIT ) + { + my->yaw += 0.2; + if ( Entity* fx = spawnMagicParticle(my) ) + { + fx->scalex = my->scalex * 0.9; + fx->scaley = my->scaley * 0.9; + fx->scalez = my->scalez * 0.9; + } + my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); + my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); + } + else if ( my->skill[1] == PARTICLE_EFFECT_FOCI_ORBIT ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 1.0) ) + { + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + fx->ditheringDisabled = true; + fx->flags[SPRITE] = my->flags[SPRITE]; + } + my->x = parent->x + 8.0 * cos(parent->yaw); + my->y = parent->y + 8.0 * sin(parent->yaw); + my->yaw = parent->yaw + PI + my->fskill[0]; + my->focalx = 0.5; + my->focalz = 0.5; + my->pitch -= 0.1; + //my->roll = PI / 2; + } + else if ( my->skill[1] == PARTICLE_EFFECT_SHATTER_EARTH_ORBIT ) + { + //my->yaw += 0.2; + if ( Entity* fx = spawnMagicParticle(my) ) + { + fx->scalex = my->scalex * 0.9; + fx->scaley = my->scaley * 0.9; + fx->scalez = my->scalez * 0.9; + } + my->x = my->actmagicOrbitStationaryX + my->actmagicOrbitDist * cos(my->yaw); + my->y = my->actmagicOrbitStationaryY + my->actmagicOrbitDist * sin(my->yaw); + my->z += my->vel_z; + + if ( abs(my->vel_x) > 0.01 ) + { + my->actmagicOrbitStationaryX += my->vel_x; + } + if ( abs(my->vel_y) > 0.01 ) + { + my->actmagicOrbitStationaryY += my->vel_y; + } + + if ( my->sprite == 2205 ) + { + my->pitch = PI / 2 + atan(my->vel_z / (sqrt(pow(my->vel_x, 2) + pow(my->vel_y, 2)))); + } + my->vel_z += 0.04; + } + else if ( my->skill[1] == PARTICLE_EFFECT_STATIC_ORBIT ) + { + //my->yaw += 0.2; + my->z -= 0.1; + my->actmagicOrbitDist = std::min(80, my->actmagicOrbitDist + 1); + my->bNeedsRenderPositionInit = true; + my->x = parent->x + (my->actmagicOrbitDist / 10.0) * cos(my->yaw); + my->y = parent->y + (my->actmagicOrbitDist / 10.0) * sin(my->yaw); + my->flags[INVISIBLE] = true; + Uint32 anim = (my->ticks % 50) / 10; + my->removeLightField(); + if ( !my->actmagicNoLight ) + { + if ( (my->sprite >= 1758 && my->sprite <= 1763) ) + { + my->light = addLight(my->x / 16, my->y / 16, "orb_blue"); + } + else if ( my->sprite >= 2335 && my->sprite <= 2340 ) + { + my->light = addLight(my->x / 16, my->y / 16, "orb_red"); + } + else if ( my->sprite >= 2347 && my->sprite <= 2352 ) + { + my->light = addLight(my->x / 16, my->y / 16, "orb_purple"); + } + else if ( my->sprite >= 2341 && my->sprite <= 2346 ) + { + my->light = addLight(my->x / 16, my->y / 16, "orb_blue"); + } + } + + if ( anim == 0 || anim == 2 || anim >= 4 ) + { + my->flags[INVISIBLE] = false; + } + if ( my->ticks % 4 == 0 ) + { + int prevSprite = my->sprite; + my->sprite++; + my->yaw += 1 * PI / 3; + if ( prevSprite == 1763 ) + { + my->sprite = 1758; + } + else if ( prevSprite == 2340 ) + { + my->sprite = 2335; + } + else if ( prevSprite == 2346 ) + { + my->sprite = 2341; + } + else if ( prevSprite == 2352 ) + { + my->sprite = 2347; + } + + if ( local_rng.rand() % 4 == 0 ) + { + bool doSound = true; + if ( !(my->sprite >= 1758 && my->sprite <= 1763) /*&& parent->behavior == &actPlayer && players[parent->skill[2]]->isLocalPlayer()*/ ) + { + if ( local_rng.rand() % 4 > 0 ) + { + doSound = false; + } + } + if ( doSound ) + { + playSoundEntityLocal(my, 808 + local_rng.rand() % 5, 32); + } + } + } + if ( !my->flags[INVISIBLE] ) + { + spawnMagicParticle(my); + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_ETERNALS_GAZE_STATIC ) + { + //my->yaw += 0.2; + my->z -= 0.1; + my->actmagicOrbitDist = std::min(80, my->actmagicOrbitDist + 1); + my->bNeedsRenderPositionInit = true; + my->x = parent->x + (my->actmagicOrbitDist / 10.0) * cos(my->yaw); + my->y = parent->y + (my->actmagicOrbitDist / 10.0) * sin(my->yaw); + my->flags[INVISIBLE] = true; + Uint32 anim = (my->ticks % 50) / 10; + my->removeLightField(); + if ( !my->actmagicNoLight ) + { + my->light = addLight(my->x / 16, my->y / 16, "orb_blue"); + } + + if ( anim == 0 || anim == 2 || anim >= 4 ) + { + my->flags[INVISIBLE] = false; + } + if ( !my->flags[INVISIBLE] ) + { + spawnMagicParticle(my); + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_NULL_PARTICLE || my->skill[1] == PARTICLE_EFFECT_NULL_PARTICLE_NOSOUND ) + { + if ( my->ticks == 1 && my->skill[1] == PARTICLE_EFFECT_NULL_PARTICLE ) + { + if ( nullParticleSfxTick == 0 || (ticks - nullParticleSfxTick) > 5 ) + { + playSoundEntityLocal(my, 166, 128); + nullParticleSfxTick = ticks; + } + } + //my->yaw += 0.2; + my->z -= 0.1; + //my->actmagicOrbitDist = std::min(80, my->actmagicOrbitDist + 1); + my->bNeedsRenderPositionInit = true; + //my->x = parent->x + (my->actmagicOrbitDist / 10.0) * cos(my->yaw); + //my->y = parent->y + (my->actmagicOrbitDist / 10.0) * sin(my->yaw); + my->flags[INVISIBLE] = true; + Uint32 anim = (my->ticks % 50) / 10; + /*my->removeLightField(); + if ( !my->actmagicNoLight ) + { + my->light = addLight(my->x / 16, my->y / 16, "orb_blue"); + }*/ + + if ( anim == 0 || anim == 2 || anim >= 4 ) + { + my->flags[INVISIBLE] = false; + } + /*if ( my->ticks % 4 == 0 ) + { + my->sprite++; + my->yaw += 1 * PI / 3; + if ( my->sprite > 1763 ) + { + my->sprite = 1758; + } + }*/ + if ( !my->flags[INVISIBLE] ) + { + spawnMagicParticle(my); + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_REVENANT_CURSE ) + { + if ( parent ) + { + my->x = parent->x; + my->y = parent->y; + real_t setpoint = -7.5; + real_t dist = setpoint - 7.5; + my->z = 7.5 + dist * (sin(std::min(1.0, my->ticks / ((real_t)1.5 * TICKS_PER_SECOND)) * PI / 2)); + + if ( my->ticks >= TICKS_PER_SECOND ) + { + my->pitch += 0.025; + my->pitch = std::min(PI / 4, my->pitch); + } + + my->yaw = parent->yaw; + my->ditheringOverride = 6; + + Entity* fx = spawnMagicParticle(my); + fx->vel_z = 0.25; + fx->ditheringOverride = 6; + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_PSYCHIC_SPEAR ) + { + if ( parent ) + { + Uint32 tickOffset = TICKS_PER_SECOND; + real_t anim = my->ticks < tickOffset ? (0.5 - 0.5 * (my->ticks / (real_t)tickOffset)) + : std::min(1.0, (my->ticks - tickOffset) / ((real_t)0.15 * TICKS_PER_SECOND)); + my->yaw = my->fskill[0];// +anim * (my->fskill[1] - my->fskill[0]); + my->pitch = my->fskill[1]; + + if ( my->ticks == 1 ) + { + playSoundEntityLocal(my, 822, 92); + } + + my->scalex = std::min(1.0, (my->ticks / (real_t)tickOffset)); + my->scaley = my->scalex; + my->scalez = my->scalex; + my->flags[INVISIBLE] = my->scalex < 0.99; + + my->x = parent->x - (sin((1.0 - anim) * PI / 2)) * (20.0 * cos(my->pitch)) * cos(my->yaw); + my->y = parent->y - (sin((1.0 - anim) * PI / 2)) * (20.0 * cos(my->pitch)) * sin(my->yaw); + my->z = parent->z - (sin((1.0 - anim) * PI / 2)) * 20.0; + + Uint32 impactTick = (Uint32)(0.25 * TICKS_PER_SECOND + tickOffset); + if ( my->ticks == impactTick ) + { + playSoundEntityLocal(my, 821, 92); + + Entity* caster = uidToEntity(my->skill[3]); + int damage = getSpellDamageFromID(SPELL_PSYCHIC_SPEAR, caster, caster ? caster->getStats() : nullptr, my, my->actmagicSpellbookBonus / 100.0); + if ( Stat* parentStats = parent->getStats() ) + { + real_t hpThreshold = getSpellEffectDurationSecondaryFromID(SPELL_PSYCHIC_SPEAR, caster, caster ? caster->getStats() : nullptr, my, my->actmagicSpellbookBonus / 100.0) / 100.0; + real_t parentHPRatio = std::min(1.0, parentStats->HP / std::max(1.0, (real_t)parentStats->MAXHP)); + if ( parentHPRatio >= hpThreshold ) + { + real_t scale = std::min(1.0, std::max(0.0, (parentHPRatio - hpThreshold) / (1.0 - hpThreshold))); + damage += scale * getSpellDamageSecondaryFromID(SPELL_PSYCHIC_SPEAR, caster, caster ? caster->getStats() : nullptr, my, my->actmagicSpellbookBonus / 100.0); + } + } + + applyGenericMagicDamage(caster, parent, *my, SPELL_PSYCHIC_SPEAR, damage, true); + + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, 0.0, TICKS_PER_SECOND, 16.0) ) + { + //fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(255, 0, 255); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.9; + indicator->cacheType = AOEIndicators_t::CACHE_PSYCHIC_SPEAR; + } + } + for ( int i = 0; i < 10; ++i ) + { + if ( Entity* gib = multiplayer == CLIENT ? spawnGibClient(0, 0, 0, 2360) : spawnGib(my, 2360) ) + { + gib->sprite = 2360; + gib->x = my->x; + gib->y = my->y; + gib->z = my->z; + } + } + } + + if ( my->ticks >= impactTick + 15 ) + { + list_RemoveNode(my->mynode); + return; + } + + spawnMagicParticleCustom(my, my->sprite, std::max(0.0, my->scalex - 0.3), 1.0); + + if ( my->ticks < tickOffset ) + { + my->fskill[2] += 0.25; + //if ( my->ticks % 2 == 0 ) + { + for ( int i = 0; i < 3; ++i ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, 2360, 1.0, 10.0) ) + { + fx->yaw = my->fskill[2] + i * 2 * PI / 3; + fx->x = parent->x + 4.0 * cos(fx->yaw); + fx->y = parent->y + 4.0 * sin(fx->yaw); + fx->z = 7.5 - my->fskill[2] * 3; + fx->vel_z = -0.05; + } + } + } + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_SMITE_PINPOINT || my->skill[1] == PARTICLE_EFFECT_TURN_UNDEAD ) + { + if ( my->skill[5] == 0 ) + { + my->fskill[0] = 0.0; + my->fskill[1] = 0.0; + my->fskill[2] = 0.0; + my->z = -7.5; + my->skill[5] = 1; + if ( !my->actmagicNoLight ) + { + if ( my->skill[1] == PARTICLE_EFFECT_SMITE_PINPOINT ) + { + createParticleFociLight(parent ? parent : my, SPELL_PINPOINT, false); + } + playSoundEntityLocal(my, 822, 128); + } + } + if ( parent ) + { + my->fskill[4] = parent->x; + my->fskill[5] = parent->y; + } + my->focalz = 0.5; + + my->fskill[0] = std::min(my->fskill[0] + 0.05, 1.0); + my->z = -10.0; + + my->removeLightField(); + if ( !my->actmagicNoLight ) + { + my->light = addLight(my->x / 16, my->y / 16, "magic_white"); + } + + int breakpoint1 = 40; + if ( my->fskill[0] >= 1.0 ) + { + if ( my->ticks >= breakpoint1 ) + { + my->fskill[1] = std::min(my->fskill[1] + 0.15, 1.0); + if ( my->fskill[1] >= 1.0 ) + { + if ( my->ticks >= breakpoint1 + 20 ) + { + if ( my->ticks == breakpoint1 + 20 ) + { + if ( !my->actmagicNoLight ) + { + createParticleFociLight(parent ? parent : my, SPELL_PINPOINT, false); + } + } + + if ( my->ticks == breakpoint1 + 25 ) + { + if ( !my->actmagicNoLight ) + { + playSoundEntityLocal(my, 849, 128); + // main particle + if ( multiplayer != CLIENT ) + { + if ( parent ) + { + Entity* caster = uidToEntity(my->skill[3]); + + if ( my->skill[1] == PARTICLE_EFFECT_SMITE_PINPOINT ) + { + int flatDamage = getSpellDamageFromID(SPELL_PINPOINT, caster, caster ? caster->getStats() : nullptr, + my, my->actmagicSpellbookBonus / 100.0); + if ( parent->isSmiteWeakMonster() ) + { + flatDamage *= 2.0; + playSoundEntity(parent, 249, 64); + } + flatDamage += my->skill[4]; + applyGenericMagicDamage(caster, parent, *my, SPELL_PINPOINT, flatDamage, true); + //spawnMagicEffectParticles(parent->x, parent->y, parent->z, 981); + + if ( caster && parent->getStats() ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *parent->getStats(), Language::get(6950), Language::get(6951), MSG_COMBAT); + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_TURN_UNDEAD ) + { + int damage = getSpellDamageFromID(SPELL_TURN_UNDEAD, caster, caster ? caster->getStats() : nullptr, + my, my->actmagicSpellbookBonus / 100.0); + applyGenericMagicDamage(caster, parent, *my, SPELL_TURN_UNDEAD, damage, true); + + if ( my->skill[6] == EFF_HOLY_FIRE ) + { + if ( parent->setEffect(EFF_HOLY_FIRE, true, 5 * TICKS_PER_SECOND, true) ) + { + int burnDuration = getSpellEffectDurationFromID(SPELL_HOLY_FIRE, caster, caster ? caster->getStats() : nullptr, + my); + if ( Entity* fx = createParticleAestheticOrbit(parent, 288, burnDuration, PARTICLE_EFFECT_HOLY_FIRE) ) + { + fx->flags[SPRITE] = true; + fx->flags[INVISIBLE] = true; + fx->skill[3] = caster ? caster->getUID() : 0; + } + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_HOLY_FIRE, 288, 0, burnDuration); + } + } + //spawnMagicEffectParticles(parent->x, parent->y, parent->z, 981); + } + } + else + { + //spawnMagicEffectParticles(my->x, my->y, my->z, 981); + } + } + } + } + + my->fskill[2] = std::min(my->fskill[2] + 0.06, 1.0); + if ( my->fskill[2] >= 1.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + } + } + } + if ( my->ticks < breakpoint1 ) + { + my->fskill[6] += 0.05 * sin(my->fskill[0] * PI / 2); + } + my->yaw = my->fskill[6]; + + my->z -= 4.0 * sin(my->fskill[1] * PI / 2); + my->z += 20.0 * sin(my->fskill[2] * PI / 2); + + my->x = my->fskill[4]; + my->y = my->fskill[5]; + if ( my->skill[1] == PARTICLE_EFFECT_SMITE_PINPOINT ) + { + my->roll -= 0.25; + my->pitch = PI / 2 + my->fskill[1] * PI / 6; + my->x += 6.0 * cos(my->yaw) * sin(my->fskill[0] * PI / 2); + my->y += 6.0 * sin(my->yaw) * sin(my->fskill[0] * PI / 2); + my->x += cos(my->yaw) * (4.0 * sin(my->fskill[1] * PI / 2) - 16.0 * sin(my->fskill[2] * PI / 2)); + my->y += sin(my->yaw) * (4.0 * sin(my->fskill[1] * PI / 2) - 16.0 * sin(my->fskill[2] * PI / 2)); + if ( my->fskill[1] > 0.05 ) + { + if ( my->ticks % 2 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 10.0) ) + { + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + } + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_TURN_UNDEAD ) + { + if ( my->fskill[1] > 0.05 ) + { + my->fskill[7] = std::min(my->fskill[7] + 0.1, 1.0); + if ( my->ticks % 2 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 10.0) ) + { + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + } + } + } + else + { + my->roll += 0.25; + } + my->pitch = PI / 2 + my->fskill[7] * 2 * PI; + } + + + } + else if ( my->skill[1] == PARTICLE_EFFECT_DEFY_FLESH_ORBIT ) + { + if ( parent ) + { + Stat* stats = parent->getStats(); + if ( !stats || (stats && !stats->getEffectActive(EFF_DEFY_FLESH)) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + + if ( my->skill[5] == 0 ) + { + my->skill[5] = 1; + playSoundEntityLocal(parent, 166, 128); + } + + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, "orb_red"); + + my->fskill[0] += 0.15; + my->x = parent->x + 8.0 * cos(my->fskill[0]); + my->y = parent->y + 8.0 * sin(my->fskill[0]); + my->z = 0.0 + 0.75 * sin(2 * PI * (my->ticks % 50) / 50.0); + my->pitch = -0.15 * sin(2 * PI * (my->ticks % 50) / 50.0); + //my->pitch = PI / 2; + my->yaw = my->fskill[0] + PI / 2; + my->roll += 0.25; + my->focalz = 0.25; + my->flags[INVISIBLE] = false; + + my->scalex = 0.5; + my->scaley = 0.5; + my->scalez = 0.5; + + /*Uint32 anim = (my->ticks % 50) / 10; + if ( anim == 0 || anim == 2 || anim >= 4 ) + { + my->flags[INVISIBLE] = false; + } + if ( my->ticks % 4 == 0 ) + { + int prevSprite = my->sprite; + my->sprite++; + my->yaw += 1 * PI / 3; + if ( !(my->sprite >= 2335 && my->sprite < 2340) ) + { + my->sprite = 2335; + } + if ( prevSprite == 2340 ) + { + my->sprite = 2335; + } + }*/ + + if ( my->ticks % 2 >= 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 10.0) ) + { + fx->focalz = my->focalz; + fx->fskill[1] = 0.5; + } + } + + bool trigger = false; + if ( my->skill[8] > 0 ) // cooldown + { + --my->skill[8]; + } + else + { + if ( my->skill[7] > 0 ) // trigger + { + my->skill[7] = 0; + trigger = true; + } + } + + if ( multiplayer != CLIENT ) + { + if ( trigger ) + { + my->skill[8] = TICKS_PER_SECOND / 10; + Entity* fx = createParticleAestheticOrbit(parent, 2363, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_DEFY_FLESH); + fx->yaw = my->fskill[0] + PI; + fx->yaw = fmod(fx->yaw, 2 * PI); + fx->skill[3] = my->skill[3]; // caster + fx->pitch = PI / 2; + fx->fskill[0] = fx->yaw; + fx->fskill[1] = PI / 4 - PI / 8; + fx->fskill[2] = parent->z; + fx->x = parent->x - 8.0 * cos(fx->yaw); + fx->y = parent->y - 8.0 * sin(fx->yaw); + fx->z = parent->z; + fx->scalex = 0.0; + fx->scaley = 0.0; + fx->scalez = 0.0; + fx->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + fx->actmagicFromSpellbook = my->actmagicFromSpellbook; + + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_DEFY_FLESH, 2363, 0, 3 * TICKS_PER_SECOND, fx->yaw * 256.0); + + if ( stats ) + { + Uint8 charges = stats->getEffectActive(EFF_DEFY_FLESH) & 0xF; + if ( charges == 0 ) + { + parent->setEffect(EFF_DEFY_FLESH, false, 0, true); + } + } + } + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_DEFY_FLESH ) + { + if ( parent ) + { + Uint32 tickOffset = 0;// TICKS_PER_SECOND; + bool trigger = my->ticks >= tickOffset; + + if ( trigger ) + { + my->skill[4]++; + if ( my->skill[5] == 0 ) + { + playSoundEntityLocal(my, 823, 64); + my->skill[5] = 1; + } + } + real_t anim = std::min(my->skill[4] / 25.0, 1.0); + if ( anim >= 0.75 ) + { + my->skill[4]++; + } + + my->yaw = my->fskill[0]; + my->pitch = PI / 2 - anim * ((PI / 2) - my->fskill[1]); + my->roll += 0.25; + my->focalz = 0.25; + + my->scalex = std::min(1.0, 0.5 + 0.5 * anim); + my->scaley = my->scalex; + my->scalez = my->scalex; + my->flags[INVISIBLE] = false;// my->scalex < 0.01; + + my->x = parent->x - (8.0 * (1.0 - std::min(0.8, anim)) + (sin((1.0 - anim) * PI)) * (8.0)) * cos(my->yaw); + my->y = parent->y - (8.0 * (1.0 - std::min(0.8, anim)) + (sin((1.0 - anim) * PI)) * (8.0)) * sin(my->yaw); + my->z = my->fskill[2] - (sin((1.0 - anim) * PI)) * 4.0; + + Uint32 impactTick = TICKS_PER_SECOND / 2 + tickOffset; + + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, std::max(0.0, my->scalex - 0.3), 10.0) ) + { + real_t dir = my->yaw; + fx->x -= 2.0 * cos(dir) * cos(my->pitch); + fx->y -= 2.0 * sin(dir) * cos(my->pitch); + fx->fskill[1] = 0.25; + } + + if ( my->ticks == impactTick ) + { + if ( multiplayer != CLIENT ) + { + Entity* caster = uidToEntity(my->skill[3]); + int damage = getSpellDamageFromID(SPELL_DEFY_FLESH, caster, caster ? caster->getStats() : nullptr, + my, my->actmagicSpellbookBonus / 100.0); + applyGenericMagicDamage(caster, parent, *my, SPELL_DEFY_FLESH, damage, true); + } + + for ( int i = 0; i < 1; ++i ) + { + if ( Entity* gib = multiplayer == CLIENT ? spawnGibClient(0, 0, 0, 2360) : spawnGib(my, 2360) ) + { + gib->sprite = 5; + gib->x = parent->x; + gib->y = parent->y; + gib->z = parent->z; + } + } + } + /*if ( my->ticks >= impactTick ) + { + my->x += ((my->ticks - impactTick) / 5.0) * 8 * cos(my->yaw); + my->y += ((my->ticks - impactTick) / 5.0) * 8 * sin(my->yaw); + my->z += ((my->ticks - impactTick) / 5.0) * 4; + }*/ + if ( my->ticks >= impactTick + 5 ) + { + list_RemoveNode(my->mynode); + return; + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_FOCI_LIGHT + || my->skill[1] == PARTICLE_EFFECT_FOCI_DARK ) + { + bool particle = false; + if ( true ) + { + if ( my->skill[3] == 0 ) + { + real_t diff = std::max(0.1, (32.0 - my->scaley) / 2.5); + my->scaley = std::min(my->scaley + diff, 32.0); + if ( my->scaley >= 32.0 ) + { + my->skill[3] = 1; + } + particle = true; + } + else if ( my->skill[3] == 1 ) + { + real_t diff = std::max(0.1, (my->scaley - 1.0) / 2.5); + my->scaley = std::max(my->scaley - diff, 1.0); + if ( my->scaley <= 1.0 ) + { + my->skill[3] = 2; + my->flags[INVISIBLE] = true; + } + particle = true; + } + + { + real_t diff = std::min(-0.05, (my->fskill[3] - my->fskill[2]) / 10.0); + my->fskill[2] += diff; + my->fskill[2] = std::max(my->fskill[2], my->fskill[3]); + } + } + + //my->bNeedsRenderPositionInit = true; + if ( parent ) + { + my->fskill[4] = parent->x; + my->fskill[5] = parent->y; + } + my->x = my->fskill[4] + (my->actmagicOrbitDist * my->fskill[7]) * cos(my->yaw - 3 * PI / 4); + my->y = my->fskill[5] + (my->actmagicOrbitDist * my->fskill[7]) * sin(my->yaw - 3 * PI / 4); + my->yaw += my->fskill[6]; + /*if ( my->ticks >= TICKS_PER_SECOND / 2 ) + { + real_t diff = std::max(0.01, (0.15 - my->fskill[6]) / 50.0); + my->fskill[6] = std::min(0.15, my->fskill[6] + diff); + }*/ + { + real_t diff = std::max(0.01, (1.0 - my->fskill[7]) / 25.0); + real_t prev = my->fskill[7]; + my->fskill[7] = std::min(1.0, my->fskill[7] + diff); + } + /*if ( my->ticks >= TICKS_PER_SECOND && my->ticks < TICKS_PER_SECOND + 5 ) + { + particle = true; + }*/ + my->z = my->fskill[2]; // baseline z + my->z += (my->scaley - 1.0) / 2; // center it along z + //my->z -= (my->scaley - 1.0) / 4; + + if ( particle ) + { + Entity* fx = spawnMagicParticleCustom(my, my->sprite, 0.25, 1.0); + fx->vel_z = -0.3; + fx->ditheringDisabled = true; + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_ETERNALS_GAZE1 ) + { + bool particle = false; + if ( my->ticks >= 2 * TICKS_PER_SECOND ) + { + static ConsoleVariable cvar_magic_egaze3("/magic_egaze3", 2.5); + if ( my->skill[3] == 0 ) + { + real_t diff = std::max(0.1, (32.0 - my->scaley) / *cvar_magic_egaze3); + my->scaley = std::min(my->scaley + diff, 32.0); + if ( my->scaley >= 32.0 ) + { + my->skill[3] = 1; + } + particle = true; + } + else if ( my->skill[3] == 1 ) + { + real_t diff = std::max(0.1, (my->scaley - 1.0) / *cvar_magic_egaze3); + my->scaley = std::max(my->scaley - diff, 1.0); + if ( my->scaley <= 1.0 ) + { + my->skill[3] = 2; + my->flags[INVISIBLE] = true; + } + particle = true; + } + + { + static ConsoleVariable cvar_magic_egaze4("/magic_egaze4", 10.0); + real_t diff = std::min(-0.05, (my->fskill[3] - my->fskill[2]) / *cvar_magic_egaze4); + my->fskill[2] += diff; + my->fskill[2] = std::max(my->fskill[2], my->fskill[3]); + } + } + + //my->bNeedsRenderPositionInit = true; + my->x = parent->x + (my->actmagicOrbitDist * my->fskill[7]) * cos(my->yaw - 3 * PI / 4); + my->y = parent->y + (my->actmagicOrbitDist * my->fskill[7]) * sin(my->yaw - 3 * PI / 4); + my->yaw += my->fskill[6]; + /*if ( my->ticks >= TICKS_PER_SECOND / 2 ) + { + real_t diff = std::max(0.01, (0.15 - my->fskill[6]) / 50.0); + my->fskill[6] = std::min(0.15, my->fskill[6] + diff); + }*/ + { + real_t diff = std::max(0.01, (1.0 - my->fskill[7]) / 25.0); + real_t prev = my->fskill[7]; + my->fskill[7] = std::min(1.0, my->fskill[7] + diff); + } + /*if ( my->ticks >= TICKS_PER_SECOND && my->ticks < TICKS_PER_SECOND + 5 ) + { + particle = true; + }*/ + my->z = my->fskill[2]; // baseline z + my->z += (my->scaley - 1.0) / 2; // center it along z + //my->z -= (my->scaley - 1.0) / 4; + + if ( particle ) + { + static ConsoleVariable cvar_magic_egaze5("/magic_egaze5", 0.25); + static ConsoleVariable cvar_magic_egaze6("/magic_egaze6", 1.0); + Entity* fx = spawnMagicParticleCustom(my, 1866, *cvar_magic_egaze5, *cvar_magic_egaze6); + fx->vel_z = -0.3; + fx->ditheringDisabled = true; + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_ETERNALS_GAZE2 ) + { + static ConsoleVariable cvar_magic_egaze8("/eg8", 2.5); + if ( my->skill[3] == 0 ) + { + real_t diff = std::max(0.1, (64.0 - my->scaley) / *cvar_magic_egaze8); + my->scaley = std::min(my->scaley + diff, 64.0); + if ( my->scaley >= 64.0 ) + { + my->skill[3] = 1; + } + //particle = true; + } + + if ( my->skill[3] == 1 ) + { + if ( my->ticks >= TICKS_PER_SECOND ) + { + real_t diff = std::max(0.1, (my->scaley - 0.0) / *cvar_magic_egaze8); + my->scaley = std::max(my->scaley - diff, 0.0); + if ( my->scaley <= 1.0 ) + { + my->scalex = my->scaley; + my->scalez = my->scalex; + if ( my->scalex <= 0.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + + { + real_t diff = std::min(-0.05, (my->fskill[3] - my->fskill[2]) / 5.0); + my->fskill[2] += diff; + my->fskill[2] = std::max(my->fskill[2], my->fskill[3]); + } + + Entity* fx = spawnMagicParticleCustom(my, 1866, 0.25, 1.0); + fx->vel_z = -0.3; + fx->ditheringDisabled = true; + } + } + + my->z = my->fskill[2]; // baseline z + my->z += (my->scaley - 1.0) / 2; // center it along z + my->x = parent->x + (my->actmagicOrbitDist) * cos(my->yaw - 3 * PI / 4); + my->y = parent->y + (my->actmagicOrbitDist) * sin(my->yaw - 3 * PI / 4); + + my->fskill[4] = (local_rng.rand() % 30 - 10) / 50.f; + my->fskill[5] = (local_rng.rand() % 30 - 10) / 50.f; + my->x += my->fskill[4] * cos(my->yaw - 3 * PI / 4 + PI / 2); + my->y += my->fskill[5] * sin(my->yaw - 3 * PI / 4 + PI / 2); + + my->yaw += my->fskill[6]; + } + else if ( my->skill[1] == PARTICLE_EFFECT_HOLY_BEAM_ORBIT ) + { + if ( parent ) + { + my->x = parent->x + (0.0 + my->fskill[1]) * cos(my->yaw + PI); + my->y = parent->y + (0.0 + my->fskill[1]) * sin(my->yaw + PI); + my->z = parent->z - 0.5; + + my->focalx = 0.0; + my->focaly = 0.0; + my->focalz = 0.5; + + //my->roll += 0.1;// *my->fskill[1]; + + if ( my->ticks % 4 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 5.0) ) + { + fx->ditheringDisabled = true; + fx->yaw = my->yaw; + fx->pitch = my->pitch; + fx->roll = my->roll; + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + int dir = (my->ticks % 16) / 4; + fx->vel_x = my->fskill[1] * 0.25 * cos(dir * PI / 2) * cos(fx->yaw + PI / 2); + fx->vel_y = my->fskill[1] * 0.25 * cos(dir * PI / 2) * sin(fx->yaw + PI / 2); + fx->vel_z = my->fskill[1] * 0.25 * sin(dir * PI / 2); + } + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_BLOOD_WAVES_ORBIT ) + { + if ( parent ) + { + my->x = parent->x; + my->y = parent->y; + my->z = parent->z; + + my->fskill[0] += 0.5; + my->pitch = my->fskill[0]; + + my->skill[3]++; + real_t anim = std::min(1.0, my->skill[3] / 20.0); + my->scalez = anim; + my->z += anim * 2.0 * sin(0.5 * my->fskill[0]) * my->fskill[1]; + + my->removeLightField(); + if ( my->fskill[1] > 0.0 ) + { + my->light = addLight(my->x / 16, my->y / 16, "orb_red"); + } + + if ( my->ticks % 2 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 5.0) ) + { + fx->ditheringDisabled = true; + fx->yaw = my->yaw; + fx->pitch = my->pitch; + fx->roll = my->roll; + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + } + } + + if ( my->ticks % 10 == 0 ) + { + Entity* fx = createParticleAestheticOrbit(my, 283, TICKS_PER_SECOND, PARTICLE_EFFECT_BLOOD_BUBBLE); + fx->x = my->x - (4.0 + (local_rng.rand() % 9)) * cos(my->yaw + PI / 2); + fx->y = my->y - (4.0 + (local_rng.rand() % 9)) * sin(my->yaw + PI / 2); + fx->z = my->z; + fx->flags[SPRITE] = true; + + fx->fskill[2] = 2 * PI * (local_rng.rand() % 10) / 10.0; + fx->fskill[3] = 0.05; // speed osc + fx->scalex = 0.0125; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 2; + fx->actmagicOrbitStationaryX = my->x; + fx->actmagicOrbitStationaryY = my->y; + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_SCEPTER_BLAST_ORBIT1 + || my->skill[1] == PARTICLE_EFFECT_METEOR_STATIONARY_ORBIT /* doesn't need parent entity */ ) + { + if ( my->sprite == 2192 || my->sprite == 2210 ) + { + if ( parent ) + { + my->x = parent->x; + my->y = parent->y; + my->z = parent->z - 0.5; + } + my->roll = 0.0; + my->pitch -= 0.1; + my->scalex = 0.75; + my->scaley = 0.75; + my->scalez = 0.75; + my->focalx = 0.0; + my->focaly = 0.0; + my->focalz = 0.5; + + if ( my->sprite == 2210 ) + { + if ( multiplayer == CLIENT && my->skill[1] == PARTICLE_EFFECT_SCEPTER_BLAST_ORBIT1 ) + { + my->scalex = 0.75; + my->scaley = 0.75; + my->scalez = 0.75; + } + else + { + my->scalex = 0.75 * std::min(50, (int)my->ticks) / 50.0; + my->scaley = 0.75 * std::min(50, (int)my->ticks) / 50.0; + my->scalez = 0.75 * std::min(50, (int)my->ticks) / 50.0; + } + + if ( Entity* fx = spawnMagicParticle(my) ) + { + fx->sprite = 13; + fx->flags[SPRITE] = true; + } + + if ( my->ticks % 8 == 4 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 50.0) ) + { + fx->ditheringDisabled = true; + fx->yaw = my->yaw; + fx->pitch = my->pitch; + fx->roll = my->roll; + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + } + } + } + else + { + if ( my->ticks % 8 == 4 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 10.0) ) + { + fx->ditheringDisabled = true; + fx->yaw = my->yaw; + fx->pitch = my->pitch; + fx->roll = my->roll; + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + } + } + } + } + else if ( my->sprite == 2193 || my->sprite == 2211 ) + { + if ( parent ) + { + my->x = parent->x; + my->y = parent->y; + my->z = parent->z - 0.5; + } + my->pitch += 0.1; + my->roll = 0.0; + my->scalex = 0.75; + my->scaley = 0.75; + my->scalez = 0.75; + my->focalx = 0.0; + my->focaly = 0.0; + my->focalz = 0.5; + + if ( my->sprite == 2211 ) + { + if ( multiplayer == CLIENT && my->skill[1] == PARTICLE_EFFECT_SCEPTER_BLAST_ORBIT1 ) + { + my->scalex = 0.75; + my->scaley = 0.75; + my->scalez = 0.75; + } + else + { + my->scalex = 0.75 * std::min(50, (int)my->ticks) / 50.0; + my->scaley = 0.75 * std::min(50, (int)my->ticks) / 50.0; + my->scalez = 0.75 * std::min(50, (int)my->ticks) / 50.0; + } + + if ( my->ticks % 8 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 50.0) ) + { + fx->ditheringDisabled = true; + fx->yaw = my->yaw; + fx->pitch = my->pitch; + fx->roll = my->roll; + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + } + } + } + else + { + if ( my->ticks % 8 == 0 ) + { + if ( Entity* fx = spawnMagicParticleCustom(my, my->sprite, my->scalex, 10.0) ) + { + fx->ditheringDisabled = true; + fx->yaw = my->yaw; + fx->pitch = my->pitch; + fx->roll = my->roll; + fx->focalx = my->focalx; + fx->focaly = my->focaly; + fx->focalz = my->focalz; + } + } + } + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_STASIS_RIFT_ORBIT ) + { + Stat* stats = parent->getStats(); + if ( stats && !stats->getEffectActive(EFF_STASIS) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + + my->yaw += 0.2; + //spawnMagicParticle(my); + my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); + my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); + my->z = parent->z; + //my->z -= 0.1; + //my->bNeedsRenderPositionInit = true; + my->scalex = 1.0; + my->scaley = my->scalex; + my->scalez = my->scalex; + + if ( Entity* fx = spawnMagicParticle(my) ) + { + fx->yaw += PI / 2; + fx->scalex = my->scalex; + fx->scaley = my->scaley; + fx->scalez = my->scalez; + } + } + else if ( my->skill[1] == PARTICLE_EFFECT_THORNS_ORBIT ) + { + Stat* stats = parent->getStats(); + if ( stats && !stats->getEffectActive(EFF_THORNS) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + + my->x = parent->x + 4.0 * cos(my->yaw); + my->y = parent->y + 4.0 * sin(my->yaw); + + //my->z -= 0.05; + my->scalex -= 0.025; + my->scaley = my->scalex; + my->scalez = my->scalex; + + if ( my->scalex <= 0.0 ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + + /*if ( Entity* fx = spawnMagicParticle(my) ) + { + fx->yaw += PI / 2; + fx->scalex = my->scalex; + fx->scaley = my->scaley; + fx->scalez = my->scalez; + }*/ + } + --PARTICLE_LIFE; + } + return; +} + +void actParticleTest(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + return; + } + else + { + --PARTICLE_LIFE; + my->x += my->vel_x; + my->y += my->vel_y; + my->z += my->vel_z; + //my->z -= 0.01; + } +} + +void createParticleErupt(real_t x, real_t y, int sprite) +{ + real_t yaw = 0; + int numParticles = 8; + for ( int c = 0; c < 8; c++ ) + { + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = x; + entity->y = y; + entity->z = 7.5; // start from the ground. + entity->yaw = yaw; + entity->vel_x = 0.2; + entity->vel_y = 0.2; + entity->vel_z = -2; + entity->skill[0] = 100; + entity->skill[1] = 0; // direction. + entity->fskill[0] = 0.1; + entity->behavior = &actParticleErupt; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + yaw += 2 * PI / numParticles; + } +} + +void createParticleErupt(Entity* parent, int sprite) +{ + if ( !parent ) + { + return; + } + + createParticleErupt(parent->x, parent->y, sprite); +} + +Entity* createParticleSapCenter(Entity* parent, Entity* target, int spell, int sprite, int endSprite) +{ + if ( !parent || !target ) + { + return nullptr; + } + // spawns the invisible 'center' of the magic particle + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = target->x; + entity->y = target->y; + entity->parent = (parent->getUID()); + entity->yaw = parent->yaw + PI; // face towards the caster. + entity->skill[0] = 45; + entity->skill[2] = -13; // so clients know my behavior. + entity->skill[3] = 0; // init + entity->skill[4] = sprite; // visible sprites. + entity->skill[5] = endSprite; // sprite to spawn on return to caster. + entity->skill[6] = spell; + entity->behavior = &actParticleSapCenter; + if ( target->sprite == 977 ) + { + // boomerang. + entity->yaw = target->yaw; + entity->roll = target->roll; + entity->pitch = target->pitch; + entity->z = target->z; + } + entity->flags[INVISIBLE] = true; + entity->flags[PASSABLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->flags[UNCLICKABLE] = true; + return entity; +} + +void createParticleSap(Entity* parent) +{ + real_t speed = 0.4; + if ( !parent ) + { + return; + } + for ( int c = 0; c < 4; c++ ) + { + // 4 particles, in an 'x' pattern around parent sprite. + int sprite = parent->sprite; + if ( parent->sprite == 977 ) + { + if ( c > 0 ) + { + continue; + } + // boomerang return. + sprite = parent->sprite; + } + if ( parent->sprite == 2178 ) // testing particle + { + if ( c >= 2 ) + { + continue; + } + } + if ( parent->skill[6] == SPELL_STEAL_WEAPON || parent->skill[6] == SHADOW_SPELLCAST ) + { + sprite = parent->sprite; + } + else if ( parent->skill[6] == SPELL_DRAIN_SOUL ) + { + if ( c == 0 || c == 3 ) + { + sprite = parent->sprite; + } + else + { + sprite = 599; + } + } + else if ( parent->skill[6] == SPELL_SUMMON ) + { + sprite = parent->sprite; + } + else if ( parent->skill[6] == SPELL_FEAR ) + { + sprite = parent->sprite; + } + else if ( multiplayer == CLIENT ) + { + // client won't receive the sprite skill data in time, fix for this until a solution is found! + if ( sprite == 598 ) + { + if ( c == 0 || c == 3 ) + { + // drain HP particle + sprite = parent->sprite; + } + else + { + // drain MP particle + sprite = 599; + } + } + } + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x; + entity->y = parent->y; + entity->z = 0; + entity->scalex = 0.9; + entity->scaley = 0.9; + entity->scalez = 0.9; + if ( sprite == 598 || sprite == 599 ) + { + entity->scalex = 0.5; + entity->scaley = 0.5; + entity->scalez = 0.5; + } + if ( parent->sprite == 2178 ) + { + entity->scalex = 0.5; + entity->scaley = 0.5; + entity->scalez = 0.5; + } + entity->parent = (parent->getUID()); + entity->yaw = parent->yaw; + if ( c == 0 ) + { + entity->vel_z = -speed; + entity->vel_x = speed * cos(entity->yaw + PI / 2); + entity->vel_y = speed * sin(entity->yaw + PI / 2); + entity->yaw += PI / 3; + entity->pitch -= PI / 6; + entity->fskill[2] = -(PI / 3) / 25; // yaw rate of change. + entity->fskill[3] = (PI / 6) / 25; // pitch rate of change. + } + else if ( c == 1 ) + { + entity->vel_z = -speed; + entity->vel_x = speed * cos(entity->yaw - PI / 2); + entity->vel_y = speed * sin(entity->yaw - PI / 2); + entity->yaw -= PI / 3; + entity->pitch -= PI / 6; + entity->fskill[2] = (PI / 3) / 25; // yaw rate of change. + entity->fskill[3] = (PI / 6) / 25; // pitch rate of change. + } + else if ( c == 2 ) + { + entity->vel_x = speed * cos(entity->yaw + PI / 2); + entity->vel_y = speed * sin(entity->yaw + PI / 2); + entity->vel_z = speed; + entity->yaw += PI / 3; + entity->pitch += PI / 6; + entity->fskill[2] = -(PI / 3) / 25; // yaw rate of change. + entity->fskill[3] = -(PI / 6) / 25; // pitch rate of change. + } + else if ( c == 3 ) + { + entity->vel_x = speed * cos(entity->yaw - PI / 2); + entity->vel_y = speed * sin(entity->yaw - PI / 2); + entity->vel_z = speed; + entity->yaw -= PI / 3; + entity->pitch += PI / 6; + entity->fskill[2] = (PI / 3) / 25; // yaw rate of change. + entity->fskill[3] = -(PI / 6) / 25; // pitch rate of change. + } + + entity->skill[3] = c; // particle index + entity->fskill[0] = entity->vel_x; // stores the accumulated x offset from center + entity->fskill[1] = entity->vel_y; // stores the accumulated y offset from center + entity->skill[0] = 200; // lifetime + entity->skill[1] = 0; // direction outwards + entity->behavior = &actParticleSap; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + + if ( sprite == 977 ) // boomerang + { + entity->z = parent->z; + entity->scalex = 1.f; + entity->scaley = 1.f; + entity->scalez = 1.f; + entity->skill[0] = 175; + entity->fskill[2] = -((PI / 3) + (PI / 6)) / (150); // yaw rate of change over 3 seconds + entity->fskill[3] = 0.f; + entity->focalx = 2; + entity->focalz = 0.5; + entity->pitch = parent->pitch; + entity->yaw = parent->yaw; + entity->roll = parent->roll; + + entity->vel_x = 1 * cos(entity->yaw); + entity->vel_y = 1 * sin(entity->yaw); + int x = entity->x / 16; + int y = entity->y / 16; + if ( !map.tiles[(MAPLAYERS - 1) + y * MAPLAYERS + x * MAPLAYERS * map.height] ) + { + // no ceiling, bounce higher. + entity->vel_z = -0.4; + entity->skill[3] = 1; // high bounce. + } + else + { + entity->vel_z = -0.08; + } + entity->yaw += PI / 3; + } + else + { + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + } + } +} + +void createParticleDropRising(Entity* parent, int sprite, double scale) +{ + if ( !parent ) + { + return; + } + + for ( int c = 0; c < 50; c++ ) + { + // shoot drops to the sky + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x - 4 + local_rng.rand() % 9; + entity->y = parent->y - 4 + local_rng.rand() % 9; + entity->z = 7.5 + local_rng.rand() % 50; + entity->vel_z = -1; + //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->particleDuration = 10 + local_rng.rand() % 50; + entity->scalex *= scale; + entity->scaley *= scale; + entity->scalez *= scale; + entity->behavior = &actParticleDot; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +Entity* createParticleTimer(Entity* parent, int duration, int sprite) +{ + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Timer entity. + entity->sizex = 1; + entity->sizey = 1; + if ( parent ) + { + entity->x = parent->x; + entity->y = parent->y; + entity->parent = (parent->getUID()); + } + entity->behavior = &actParticleTimer; + entity->particleTimerDuration = duration; + entity->flags[INVISIBLE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + /*if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3);*/ + + return entity; +} + +void actParticleErupt(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + return; + } + else + { + // particles jump up from the ground then back down again. + --PARTICLE_LIFE; + my->x += my->vel_x * cos(my->yaw); + my->y += my->vel_y * sin(my->yaw); + my->scalex *= 0.99; + my->scaley *= 0.99; + my->scalez *= 0.99; + if ( *cvar_magic_fx_use_vismap && !intro ) + { + int x = my->x / 16.0; + int y = my->y / 16.0; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) + { + spawnMagicParticle(my); + break; + } + } + } + } + else + { + spawnMagicParticle(my); + } + if ( my->skill[1] == 0 ) // rising + { + my->z += my->vel_z; + my->vel_z *= 0.8; + my->pitch = std::min(my->pitch + my->fskill[0], PI / 2); + my->fskill[0] = std::max(my->fskill[0] * 0.85, 0.05); + if ( my->vel_z > -0.02 ) + { + my->skill[1] = 1; + } + } + else // falling + { + my->pitch = std::min(my->pitch + my->fskill[0], 15 * PI / 16); + my->fskill[0] = std::min(my->fskill[0] * (1 / 0.99), 0.1); + my->z -= my->vel_z; + my->vel_z *= (1 / 0.8); + my->vel_z = std::max(my->vel_z, -0.8); + } + } +} + +void floorMagicClientReceive(Entity* my) +{ + if ( !my ) { return; } + if ( multiplayer != CLIENT ) { return; } + + if ( my->actfloorMagicClientReceived != 0 ) { return; } + + my->actfloorMagicType = (my->skill[2] >> 20) & 0xFF; + /*if ( my->actfloorMagicType == ParticleTimerEffect_t::EFFECT_LIGHTNING_BOLT ) + { + floorMagicSetLightningParticle(my); + }*/ + if ( my->actfloorMagicType == ParticleTimerEffect_t::EFFECT_ICE_WAVE ) + { + my->sizex = 4; + my->sizey = 4; + my->scalex = 0.25; + my->scaley = 0.25; + my->scalez = 0.25; + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EFFECT_DISRUPT_EARTH ) + { + my->sizex = 4; + my->sizey = 4; + my->scalex = 0.25; + my->scaley = 0.25; + my->scalez = 0.25; + my->actmagicNoParticle = 1; + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_PATH + || my->actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_SELF + || my->actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_TILE_VOID + || my->actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_SELF_SUSTAIN + || my->actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_TILE ) + { + my->scalex = 0.25; + my->scaley = 0.25; + my->scalez = 0.25; + my->sizex = 6; + my->sizey = 6; + my->skill[0] = (my->skill[2] >> 8) & 0xFFF; // duration + my->actmagicNoParticle = 1; + } + + my->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + + my->actfloorMagicClientReceived = 1; +} + +void floorMagicParticleSetUID(Entity& fx, bool noupdate) +{ + Sint32 val = (1 << 31); + val |= (Uint8)(noupdate ? 21 : 20); + val |= (((Uint16)(0) & 0xFFF) << 8); + if ( fx.actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_PATH + || fx.actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_SELF + || fx.actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_SELF_SUSTAIN + || fx.actfloorMagicType == ParticleTimerEffect_t::EFFECT_ROOTS_TILE ) + { + val |= (((Uint16)(fx.skill[0]) & 0xFFF) << 8); + } + + val |= (Uint8)(fx.actfloorMagicType & 0xFF) << 20; + fx.skill[2] = val; +} + +Entity* floorMagicCreateRoots(real_t x, real_t y, Entity* caster, int damage, int spellID, int duration, int particleTimerAction) +{ + int mapx = static_cast(x) >> 4; + int mapy = static_cast(y) >> 4; + int mapIndex = (mapy)*MAPLAYERS + (mapx) * MAPLAYERS * map.height; + if ( mapx > 0 && mapy > 0 && mapx < map.width - 1 && mapy < map.height - 1 ) + { + if ( !map.tiles[mapIndex] + || swimmingtiles[map.tiles[mapIndex]] + || lavatiles[map.tiles[mapIndex]] ) + { + return nullptr; + } + } + else + { + return nullptr; + } + + Entity* spellTimer = createParticleTimer(caster, duration + 10, -1); + spellTimer->particleTimerCountdownAction = particleTimerAction; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->flags[UPDATENEEDED] = false; // no update clients + spellTimer->flags[NOUPDATE] = true; + if ( caster ) + { + spellTimer->yaw = caster->yaw; + } + else + { + spellTimer->yaw = 0.0; + } + spellTimer->x = x; + spellTimer->y = y; + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + + spellTimer->particleTimerVariable1 = damage; + spellTimer->particleTimerVariable2 = spellID; + return spellTimer; +} + +void floorMagicCreateSpores(Entity* spawnOnEntity, real_t x, real_t y, Entity* caster, int damage, int spellID) +{ + if ( multiplayer == CLIENT ) + { + return; + } + + if ( spawnOnEntity ) + { + x = static_cast(spawnOnEntity->x / 16) * 16.0 + 8.0; + y = static_cast(spawnOnEntity->y / 16) * 16.0 + 8.0; + } + else + { + x = static_cast(x / 16) * 16.0 + 8.0; + y = static_cast(y / 16) * 16.0 + 8.0; + } + + int mapx = static_cast(x) >> 4; + int mapy = static_cast(y) >> 4; + + int mapIndex = (mapy)*MAPLAYERS + (mapx)*MAPLAYERS * map.height; + if ( mapx > 0 && mapy > 0 && mapx < map.width - 1 && mapy < map.height - 1 ) + { + if ( map.tiles[OBSTACLELAYER + mapIndex] ) + { + return; + } + } + else + { + return; + } + bool freeSpot = true; + if ( spellID == SPELL_SPORE_BOMB || spellID == SPELL_MYCELIUM_BOMB ) + { + // allow overlap + } + else + { + auto entLists = TileEntityList.getEntitiesWithinRadius(mapx, mapy, 0); + for ( auto it : entLists ) + { + if ( !freeSpot ) + { + break; + } + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->behavior == &actParticleTimer && entity->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPORES ) + { + freeSpot = false; + break; + } + } + } + } + } + + if ( !freeSpot ) + { + return; + } + + Uint32 lifetime = TICKS_PER_SECOND * 6; + Entity* spellTimer = createParticleTimer(caster, lifetime + TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SPORES; + spellTimer->particleTimerCountdownSprite = (spellID == SPELL_MYCELIUM_BOMB || spellID == SPELL_MYCELIUM_SPORES) ? 248: 227; + spellTimer->yaw = 0.0; + spellTimer->x = x; + spellTimer->y = y; + spellTimer->particleTimerVariable1 = damage; + spellTimer->particleTimerVariable2 = spellID; + spellTimer->particleTimerVariable4 = 0; + + auto& timerEffects = particleTimerEffects[spellTimer->getUID()]; + + if ( caster && caster->behavior == &actPlayer && spellID == SPELL_SPORES ) + { + if ( Stat* casterStats = caster->getStats() ) + { + if ( casterStats->getEffectActive(EFF_GROWTH) >= 2 && casterStats->type == MYCONID ) + { + if ( casterStats->getEffectActive(EFF_GROWTH) == 2 ) + { + spellTimer->particleTimerVariable4 = 1; + } + else if ( casterStats->getEffectActive(EFF_GROWTH) == 3 ) + { + spellTimer->particleTimerVariable4 = 2; + } + else if ( casterStats->getEffectActive(EFF_GROWTH) == 4 ) + { + spellTimer->particleTimerVariable4 = 3; + } + } + } + } + + std::vector> coords; + std::map> effLocations; + auto particleEffectType = (spellID == SPELL_MYCELIUM_BOMB || spellID == SPELL_MYCELIUM_SPORES) ? ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM + : ParticleTimerEffect_t::EffectType::EFFECT_SPORES; + for ( int i = -1; i < 2; ++i ) + { + for ( int j = -1; j < 2; ++j ) + { + coords.push_back(std::make_pair(i, j)); + effLocations[particleEffectType].push_back(ParticleTimerEffect_t::EffectLocations_t()); + auto& data = effLocations[particleEffectType].back(); + if ( spellID == SPELL_SPORE_BOMB || spellID == SPELL_MYCELIUM_BOMB ) + { + data.seconds = 0.0; + } + else + { + data.seconds = 1 / 4.0; + if ( spellID == SPELL_SPORES ) + { + if ( spellTimer->particleTimerVariable4 == 1 ) + { + data.seconds = 0.2; + } + else if ( spellTimer->particleTimerVariable4 == 2 ) + { + data.seconds = 0.16; + } + else if ( spellTimer->particleTimerVariable4 == 3 ) + { + data.seconds = 0.12; + } + } + } + } + } + + int index = -1; + Uint32 lifetime_tick = 1; + while ( lifetime_tick <= lifetime ) + { + ++index; + auto& effect = timerEffects.effectMap[lifetime_tick == 0 ? 1 : lifetime_tick]; // first behavior tick only occurs at 1 + effect.effectType = particleEffectType; + if ( timerEffects.effectMap.size() == 1 ) + { + effect.firstEffect = true; + } + + auto& data = effLocations[effect.effectType][index]; + effect.sfx = data.sfx; + + int pick = local_rng.rand() % coords.size(); + auto coord = coords[pick]; + coords.erase(coords.begin() + pick); + + effect.x = spellTimer->x + coord.first * 16.0; + effect.y = spellTimer->y + coord.second * 16.0; + effect.yaw = 0.0; + + lifetime_tick += std::max(1.0, TICKS_PER_SECOND * data.seconds); + if ( index + 1 >= effLocations[effect.effectType].size() ) + { + break; + } + } + + if ( damage > 0 ) + { + if ( spawnOnEntity ) + { + if ( auto particleEmitterHitPropsTimer = getParticleEmitterHitProps(spellTimer->getUID(), spawnOnEntity) ) + { + particleEmitterHitPropsTimer->hits++; + particleEmitterHitPropsTimer->tick = ticks; + } + } + + int gibSprite = (spellID == SPELL_MYCELIUM_BOMB || spellID == SPELL_MYCELIUM_SPORES) ? 1886 : 1816; + for ( int i = 0; i < 16; ++i ) + { + Entity* gib = spawnGib(spellTimer); + gib->sprite = gibSprite; + gib->yaw = i * PI / 4 + (-2 + local_rng.rand() % 5) * PI / 64; + gib->vel_x = 1.75 * cos(gib->yaw); + gib->vel_y = 1.75 * sin(gib->yaw); + gib->scalex = 0.5; + gib->scaley = 0.5; + gib->scalez = 0.5; + gib->z = local_rng.uniform(8, spellTimer->z - 4); + gib->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + } + + serverSpawnMiscParticlesAtLocation(spellTimer->x, spellTimer->y, spellTimer->z, PARTICLE_EFFECT_SPORE_BOMB, gibSprite); + } +} + +void floorMagicCreateLightningSequence(Entity* spellTimer, int startTickOffset) +{ + if ( !spellTimer ) { return; } + + int lifetime_tick = startTickOffset; + if ( lifetime_tick == 0 ) + { + lifetime_tick = 100;// getSpellEffectDurationFromID(SPELL_LIGHTNING_BOLT, uidToEntity(spellTimer->parent), nullptr, spellTimer); + } + + auto& timerEffects = particleTimerEffects[spellTimer->getUID()]; + std::map> effLocations; + auto particleEffectType = ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT; + for ( int i = 0; i < 8; ++i ) + { + effLocations[particleEffectType].push_back(ParticleTimerEffect_t::EffectLocations_t()); + auto& data = effLocations[particleEffectType].back(); + data.seconds = 1 / 10.0; + data.dist = 1.0; + data.xOffset = ((local_rng.rand() % 21) / 10.0) + -1.0; + switch ( i % 4 ) + { + case 0: + data.yawOffset = 0 * (PI / 2); + break; + case 1: + data.yawOffset = 2 * (PI / 2); + break; + case 2: + data.yawOffset = 1 * (PI / 2); + break; + case 3: + data.yawOffset = 3 * (PI / 2); + break; + default: + data.yawOffset = i * (PI / 2); + break; + } + + if ( i == 0 ) + { + data.sfx = 171; + } + else if ( i == 1 ) + { + data.sfx = 517; + } + } + + int index = -1; + Uint32 lifetime = spellTimer->particleTimerEffectLifetime > 0 + ? std::min(spellTimer->particleTimerDuration, spellTimer->particleTimerEffectLifetime) + : spellTimer->particleTimerDuration; + while ( lifetime_tick <= lifetime ) + { + ++index; + auto& effect = timerEffects.effectMap[lifetime_tick == 0 ? 1 : lifetime_tick]; // first behavior tick only occurs at 1 + effect.effectType = particleEffectType; + if ( timerEffects.effectMap.size() == 1 ) + { + effect.firstEffect = true; + } + + auto& data = effLocations[effect.effectType][index]; + effect.sfx = data.sfx; + effect.x = data.xOffset * cos(spellTimer->yaw + PI / 2); + effect.y = data.xOffset * sin(spellTimer->yaw + PI / 2); + effect.yaw = spellTimer->yaw + data.yawOffset; + + lifetime_tick += std::max(1.0, TICKS_PER_SECOND * data.seconds); + if ( index + 1 >= effLocations[effect.effectType].size() ) + { + break; + } + } +} + +Entity* floorMagicSetLightningParticle(Entity* my) +{ + if ( !my ) { return nullptr; } + my->scalex = 1.0; + my->scaley = 1.0; + my->scalez = 1.0; + my->ditheringDisabled = true; + my->actmagicNoParticle = 1; + my->sizex = 10; + my->sizey = 10; + + my->flags[NOUPDATE] = true; + my->flags[UPDATENEEDED] = false; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + my->setUID(-3); + + if ( multiplayer == CLIENT ) + { + my->actfloorMagicClientReceived = 1; + } + + if ( flickerLights ) + { + my->light = addLight(my->x / 16, my->y / 16, "explosion"); + } + else + { + Entity* parent = uidToEntity(my->parent); + if ( parent ) + { + if ( !parent->light ) + { + parent->light = addLight(my->x / 16, my->y / 16, "explosion"); + } + } + } + return my; +} + +void actParticleTimer(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + if ( multiplayer != CLIENT ) + { + if ( my->particleTimerEndAction == PARTICLE_EFFECT_INCUBUS_TELEPORT_STEAL ) + { + // teleport to random location spell. + Entity* parent = uidToEntity(my->parent); + if ( parent ) + { + createParticleErupt(parent, my->particleTimerEndSprite); + + int x1 = 0; + int x2 = 0; + int y1 = 0; + int y2 = 0; + if ( Stat* stats = parent->getStats() ) + { + if ( stats->type == INCUBUS && stats->getAttribute("special_npc") == "johann" ) + { + if ( !strcmp(map.filename, "fraternity.lmp") ) + { + x1 = 17; + y1 = 1; + x2 = 30; + y2 = 12; + } + } + } + if ( parent->teleportRandom(x1, x2, y1, y2) ) + { + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_INCUBUS_TELEPORT_TARGET ) + { + // teleport to target spell. + Entity* parent = uidToEntity(my->parent); + Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); + if ( parent && target ) + { + createParticleErupt(parent, my->particleTimerEndSprite); + if ( parent->teleportAroundEntity(target, my->particleTimerVariable1) ) + { + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_TELEPORT_PULL ) + { + // teleport to target spell. + Entity* parent = uidToEntity(my->parent); + Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); + if ( parent && target ) + { + real_t oldx = target->x; + real_t oldy = target->y; + my->flags[PASSABLE] = true; + int tx = static_cast(std::floor(my->x)) >> 4; + int ty = static_cast(std::floor(my->y)) >> 4; + if ( !target->isBossMonster() && + target->teleport(tx, ty) ) + { + // teleport success. + if ( parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + if ( target->getStats() ) + { + messagePlayerMonsterEvent(parent->skill[2], color, *(target->getStats()), Language::get(3450), Language::get(3451), MSG_COMBAT); + } + } + if ( target->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 255, 255); + messagePlayerColor(target->skill[2], MESSAGE_COMBAT, color, Language::get(3461)); + } + real_t distance = sqrt((target->x - oldx) * (target->x - oldx) + (target->y - oldy) * (target->y - oldy)) / 16.f; + //real_t distance = (entityDist(parent, target)) / 16; + createParticleErupt(target, my->particleTimerEndSprite); + int durationToStun = 0; + if ( distance >= 2 ) + { + durationToStun = 25 + std::min((distance - 4) * 10, 50.0); + } + if ( target->behavior == &actMonster ) + { + if ( durationToStun > 0 && target->setEffect(EFF_DISORIENTED, true, durationToStun, false) ) + { + int numSprites = std::min(3, durationToStun / 25); + for ( int i = 0; i < numSprites; ++i ) + { + spawnFloatingSpriteMisc(134, target->x + (-4 + local_rng.rand() % 9) + cos(target->yaw) * 2, + target->y + (-4 + local_rng.rand() % 9) + sin(target->yaw) * 2, target->z + local_rng.rand() % 4); + } + } + target->monsterReleaseAttackTarget(); + target->lookAtEntity(*parent); + target->monsterLookDir += (PI - PI / 4 + (local_rng.rand() % 10) * PI / 40); + } + else if ( target->behavior == &actPlayer ) + { + durationToStun = std::max(50, durationToStun); + target->setEffect(EFF_DISORIENTED, true, durationToStun, false); + int numSprites = std::min(3, durationToStun / 50); + for ( int i = 0; i < numSprites; ++i ) + { + spawnFloatingSpriteMisc(134, target->x + (-4 + local_rng.rand() % 9) + cos(target->yaw) * 2, + target->y + (-4 + local_rng.rand() % 9) + sin(target->yaw) * 2, target->z + local_rng.rand() % 4); + } + Uint32 color = makeColorRGB(255, 255, 255); + messagePlayerColor(target->skill[2], MESSAGE_COMBAT, color, Language::get(3462)); + } + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(target, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_PORTAL_SPAWN ) + { + Entity* parent = uidToEntity(my->parent); + if ( parent ) + { + parent->flags[INVISIBLE] = false; + serverUpdateEntityFlag(parent, INVISIBLE); + playSoundEntity(parent, 164, 128); + } + spawnExplosion(my->x, my->y, 0); + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_SUMMON_MONSTER + || my->particleTimerEndAction == PARTICLE_EFFECT_DEVIL_SUMMON_MONSTER ) + { + playSoundEntity(my, 164, 128); + spawnExplosion(my->x, my->y, -4.0); + bool forceLocation = false; + if ( my->particleTimerEndAction == PARTICLE_EFFECT_DEVIL_SUMMON_MONSTER && + !map.tiles[static_cast(my->y / 16) * MAPLAYERS + static_cast(my->x / 16) * MAPLAYERS * map.height] ) + { + if ( my->particleTimerVariable1 == SHADOW || my->particleTimerVariable1 == CREATURE_IMP ) + { + forceLocation = true; + } + } + Entity* monster = summonMonster(static_cast(my->particleTimerVariable1), my->x, my->y, forceLocation); + if ( monster ) + { + if ( Stat* monsterStats = monster->getStats() ) + { + if ( my->parent != 0 ) + { + Entity* parent = uidToEntity(my->parent); + if ( parent ) + { + if ( parent->getRace() == LICH_ICE ) + { + //monsterStats->leader_uid = my->parent; + switch ( monsterStats->type ) + { + case AUTOMATON: + strcpy(monsterStats->name, "corrupted automaton"); + monster->setEffect(EFF_CONFUSED, Uint8(MAXPLAYERS + 1), -1, true, true, true, true); + break; + default: + break; + } + + if ( Stat* parentStats = parent->getStats() ) + { + monsterStats->monsterNoDropItems = 1; + monsterStats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS] = 1; + monsterStats->LVL = 5; + std::string lich_num_summons = parentStats->getAttribute("lich_num_summons"); + if ( lich_num_summons == "" ) + { + parentStats->setAttribute("lich_num_summons", "1"); + } + else + { + int numSummons = std::stoi(parentStats->getAttribute("lich_num_summons")); + if ( numSummons >= 25 ) + { + monsterStats->MISC_FLAGS[STAT_FLAG_XP_PERCENT_AWARD] = 1; + } + ++numSummons; + parentStats->setAttribute("lich_num_summons", std::to_string(numSummons)); + } + } + } + else if ( parent->getRace() == DEVIL ) + { + monsterStats->monsterNoDropItems = 1; + monsterStats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS] = 1; + monsterStats->LVL = 5; + if ( parent->monsterDevilNumSummons >= 25 ) + { + monsterStats->MISC_FLAGS[STAT_FLAG_XP_PERCENT_AWARD] = 1; + } + if ( my->particleTimerVariable2 >= 0 + && players[my->particleTimerVariable2] && players[my->particleTimerVariable2]->entity ) + { + monster->monsterAcquireAttackTarget(*(players[my->particleTimerVariable2]->entity), MONSTER_STATE_ATTACK); + } + } + } + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_SPELL_SUMMON ) + { + //my->removeLightField(); + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_SHADOW_TELEPORT ) + { + // teleport to target spell. + Entity* parent = uidToEntity(my->parent); + if ( parent ) + { + if ( parent->monsterSpecialState == SHADOW_TELEPORT_ONLY ) + { + //messagePlayer(0, "Resetting shadow's monsterSpecialState!"); + parent->monsterSpecialState = 0; + serverUpdateEntitySkill(parent, 33); // for clients to keep track of animation + } + } + Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); + if ( parent ) + { + bool teleported = false; + createParticleErupt(parent, my->particleTimerEndSprite); + if ( target ) + { + teleported = parent->teleportAroundEntity(target, my->particleTimerVariable1); + } + else + { + teleported = parent->teleportRandom(); + } + + if ( teleported ) + { + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_LICHFIRE_TELEPORT_STATIONARY ) + { + // teleport to fixed location spell. + node_t* node; + int c = 0 + local_rng.rand() % 3; + Entity* target = nullptr; + for ( node = map.entities->first; node != nullptr; node = node->next ) + { + target = (Entity*)node->element; + if ( target->behavior == &actDevilTeleport ) + { + if ( (c == 0 && target->sprite == 72) + || (c == 1 && target->sprite == 73) + || (c == 2 && target->sprite == 74) ) + { + break; + } + } + } + Entity* parent = uidToEntity(my->parent); + if ( parent && target ) + { + createParticleErupt(parent, my->particleTimerEndSprite); + if ( parent->teleport(target->x / 16, target->y / 16) ) + { + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_LICH_TELEPORT_ROAMING ) + { + bool teleported = false; + // teleport to target spell. + node_t* node; + Entity* parent = uidToEntity(my->parent); + Entity* target = nullptr; + if ( parent ) + { + for ( node = map.entities->first; node != nullptr; node = node->next ) + { + target = (Entity*)node->element; + if ( target->behavior == &actDevilTeleport + && target->sprite == 128 ) + { + break; // found specified center of map + } + } + + if ( target ) + { + createParticleErupt(parent, my->particleTimerEndSprite); + teleported = parent->teleport((target->x / 16) - 11 + local_rng.rand() % 23, (target->y / 16) - 11 + local_rng.rand() % 23); + + if ( teleported ) + { + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_LICHICE_TELEPORT_STATIONARY ) + { + // teleport to fixed location spell. + node_t* node; + Entity* target = nullptr; + for ( node = map.entities->first; node != nullptr; node = node->next ) + { + target = (Entity*)node->element; + if ( target->behavior == &actDevilTeleport + && target->sprite == 128 ) + { + break; + } + } + Entity* parent = uidToEntity(my->parent); + if ( parent && target ) + { + createParticleErupt(parent, my->particleTimerEndSprite); + if ( parent->teleport(target->x / 16, target->y / 16) ) + { + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + parent->lichIceCreateCannon(); + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_SHRINE_TELEPORT ) + { + // teleport to target spell. + Entity* toTeleport = uidToEntity(my->particleTimerVariable2); + Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); + if ( toTeleport && target ) + { + bool teleported = false; + createParticleErupt(toTeleport, my->particleTimerEndSprite); + serverSpawnMiscParticles(toTeleport, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + teleported = toTeleport->teleportAroundEntity(target, my->particleTimerVariable1); + if ( teleported ) + { + createParticleErupt(toTeleport, my->particleTimerEndSprite); + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(toTeleport, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_DESTINY_TELEPORT ) + { + // teleport to target spell. + Entity* toTeleport = uidToEntity(my->particleTimerVariable2); + Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); + if ( toTeleport && target ) + { + bool teleported = false; + createParticleErupt(toTeleport, my->particleTimerEndSprite); + serverSpawnMiscParticles(toTeleport, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + teleported = toTeleport->teleportAroundEntity(target, my->particleTimerVariable1); + if ( teleported ) + { + createParticleErupt(toTeleport, my->particleTimerEndSprite); + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(toTeleport, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + else if ( my->particleTimerEndAction == PARTICLE_EFFECT_GHOST_TELEPORT ) + { + // teleport to target spell. + if ( Entity* parent = uidToEntity(my->parent) ) + { + if ( my->particleTimerTarget != 0 ) + { + if ( Entity* target = uidToEntity(static_cast(my->particleTimerTarget)) ) + { + bool teleported = false; + createParticleErupt(parent, my->particleTimerEndSprite); + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + teleported = parent->teleportAroundEntity(target, my->particleTimerVariable1); + if ( teleported ) + { + createParticleErupt(parent, my->particleTimerEndSprite); + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + + if ( parent->behavior == &actDeathGhost && target->behavior == &actPlayer + && parent->skill[2] == target->skill[2] ) + { + if ( stats[target->skill[2]]->getEffectActive(EFF_PROJECT_SPIRIT) ) + { + stats[target->skill[2]]->EFFECTS_TIMERS[EFF_PROJECT_SPIRIT] = 1; + } + } + } + } + } + else + { + int tx = (my->particleTimerVariable2 >> 16) & 0xFFFF; + int ty = (my->particleTimerVariable2 >> 0) & 0xFFFF; + int dist = my->particleTimerVariable1; + bool forceSpot = false; + std::vector> goodspots; + for ( int iy = std::max(1, ty - dist); !forceSpot && iy <= std::min(ty + dist, static_cast(map.height) - 1); ++iy ) + { + for ( int ix = std::max(1, tx - dist); !forceSpot && ix <= std::min(tx + dist, static_cast(map.width) - 1); ++ix ) + { + if ( !checkObstacle((ix << 4) + 8, (iy << 4) + 8, parent, NULL) ) + { + real_t tmpx = parent->x; + real_t tmpy = parent->y; + parent->x = (ix << 4) + 8; + parent->y = (iy << 4) + 8; + if ( !entityInsideSomething(parent) ) + { + if ( ix == tx && iy == ty ) + { + forceSpot = true; // directly ontop + goodspots.clear(); + } + goodspots.push_back(std::make_pair(ix, iy)); + } + // restore coordinates. + parent->x = tmpx; + parent->y = tmpy; + } + } + } + + if ( !goodspots.empty() ) + { + auto picked = goodspots.at(goodspots.size() - 1); + bool teleported = false; + createParticleErupt(parent, my->particleTimerEndSprite); + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + teleported = parent->teleport(picked.first, picked.second); + if ( teleported ) + { + createParticleErupt(parent, my->particleTimerEndSprite); + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + } + } + } + } + + } + } + } + + particleTimerEffects.erase(my->getUID()); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else + { + --PARTICLE_LIFE; + if ( my->particleTimerPreDelay <= 0 ) + { + // shoot particles for the duration of the timer, centered at caster. + if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SHOOT_PARTICLES ) + { + Entity* parent = uidToEntity(my->parent); + // shoot drops to the sky + if ( parent && my->particleTimerCountdownSprite != 0 ) + { + Entity* entity = newEntity(my->particleTimerCountdownSprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x - 4 + local_rng.rand() % 9; + entity->y = parent->y - 4 + local_rng.rand() % 9; + entity->z = 7.5; + entity->vel_z = -1; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->particleDuration = 10 + local_rng.rand() % 30; + entity->behavior = &actParticleDot; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } + } + // fire once off. + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPAWN_PORTAL ) + { + Entity* parent = uidToEntity(my->parent); + if ( parent && my->particleTimerCountdownAction < 100 ) + { + playSoundEntityLocal(parent, 167, 128); + createParticleDot(parent); + createParticleCircling(parent, 100, my->particleTimerCountdownSprite); + my->particleTimerCountdownAction = 0; + } + } + // fire once off. + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SUMMON_MONSTER ) + { + if ( my->particleTimerCountdownAction < 100 ) + { + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, true)); + } + playSoundEntityLocal(my, 167, 128); + createParticleDropRising(my, 680, 1.0); + createParticleCircling(my, 70, my->particleTimerCountdownSprite); + my->particleTimerCountdownAction = 0; + } + } + // fire once off. + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_DEVIL_SUMMON_MONSTER ) + { + if ( my->particleTimerCountdownAction < 100 ) + { + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, true)); + } + playSoundEntityLocal(my, 167, 128); + createParticleDropRising(my, 593, 1.0); + createParticleCircling(my, 70, my->particleTimerCountdownSprite); + my->particleTimerCountdownAction = 0; + } + } + // continually fire + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPELL_SUMMON ) + { + if ( multiplayer != CLIENT && my->particleTimerPreDelay != -100 ) + { + // once-off hack :) + spawnExplosion(my->x, my->y, -1); + playSoundEntity(my, 171, 128); + my->particleTimerPreDelay = -100; + + createParticleErupt(my, my->particleTimerCountdownSprite); + serverSpawnMiscParticles(my, PARTICLE_EFFECT_ERUPT, my->particleTimerCountdownSprite); + } + } + // fire once off. + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_TELEPORT_PULL_TARGET_LOCATION ) + { + createParticleDropRising(my, my->particleTimerCountdownSprite, 1.0); + my->particleTimerCountdownAction = 0; + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_MAGIC_SPRAY ) + { + Entity* parent = uidToEntity(my->parent); + if ( !parent ) + { + PARTICLE_LIFE = 0; + } + else + { + int sound = 0; + int spellID = SPELL_NONE; + switch ( my->particleTimerCountdownSprite ) + { + case 180: + sound = 169; + spellID = SPELL_SLIME_ACID; + break; + case 181: + sound = 169; + spellID = SPELL_SLIME_WATER; + break; + case 182: + sound = 164; + spellID = SPELL_SLIME_FIRE; + break; + case 183: + sound = 169; + spellID = SPELL_SLIME_TAR; + break; + case 184: + sound = 169; + spellID = SPELL_SLIME_METAL; + break; + case 245: + sound = 169; + spellID = SPELL_GREASE_SPRAY; + break; + default: + break; + } + if ( my->particleTimerVariable1 == 0 ) + { + // first fired after delay + my->particleTimerVariable1 = 1; + if ( sound > 0 ) + { + playSoundEntityLocal(parent, sound, 128); + } + } + + my->x = parent->x; + my->y = parent->y; + my->yaw = parent->yaw; + + Entity* entity = nullptr; + if ( multiplayer != CLIENT && my->ticks % 2 == 0 ) + { + // damage frames + entity = newEntity(my->particleTimerCountdownSprite, 1, map.entities, nullptr); + entity->behavior = &actMagicMissile; + } + else + { + entity = multiplayer == CLIENT ? spawnGibClient(0, 0, 0, -1) : spawnGib(my); + } + if ( entity ) + { + entity->sprite = my->particleTimerCountdownSprite; + entity->x = parent->x; + entity->y = parent->y; + if ( parent->behavior == &actMonster && parent->getMonsterTypeFromSprite() == SLIME ) + { + entity->z = -2 + parent->z + parent->focalz; + } + else + { + entity->z = parent->z; + } + entity->parent = parent->getUID(); + + entity->ditheringDisabled = true; + entity->flags[SPRITE] = true; + entity->flags[INVISIBLE] = false; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + + entity->sizex = 2; + entity->sizey = 2; + entity->yaw = my->yaw - 0.2 + (local_rng.rand() % 20) * 0.02; + entity->pitch = (local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + double vel = (20 + (local_rng.rand() % 5)) / 10.f; + entity->vel_x = vel * cos(entity->yaw) + parent->vel_x; + entity->vel_y = vel * sin(entity->yaw) + parent->vel_y; + entity->vel_z = -.5; + + if ( entity->behavior == &actGib && spellID == SPELL_GREASE_SPRAY ) + { + if ( multiplayer != CLIENT ) + { + if ( my->ticks % 5 == 0 ) + { + entity->actGibHitGroundEvent = 1; + } + } + } + if ( entity->behavior == &actGib ) + { + if ( my->ticks % 5 == 0 ) + { + // add lighting + entity->skill[6] = 1; + } + } + else if ( entity->behavior == &actMagicMissile ) + { + spell_t* spell = getSpellFromID(spellID); + entity->skill[4] = 0; // life start + entity->skill[5] = TICKS_PER_SECOND; //lifetime + entity->actmagicSpray = 1; + entity->actmagicSprayGravity = 0.04; + entity->actmagicEmitter = my->getUID(); + entity->actmagicFromSpellbook = my->actmagicFromSpellbook; + entity->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + node_t* node = list_AddNodeFirst(&entity->children); + node->element = copySpell(spell); + ((spell_t*)node->element)->caster = parent->getUID(); + node_t* elementNode = ((spell_t*)node->element)->elements.first; + spellElement_t* element = (spellElement_t*)elementNode->element; + { + elementNode = element->elements.first; + element = (spellElement_t*)elementNode->element; + if ( spellID == SPELL_GREASE_SPRAY ) + { + } + else + { + if ( Stat* stats = parent->getStats() ) + { + element->setDamage(element->getDamage() + stats->getProficiency(PRO_SORCERY) / 10); + } + } + ((spell_t*)node->element)->mana = 5; + } + + node->deconstructor = &spellDeconstructor; + node->size = sizeof(spell_t); + TileEntityList.addEntity(*entity); + --entity_uids; + entity->setUID(-3); + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_FOCI_SPRAY ) + { + Entity* parent = uidToEntity(my->parent); + if ( !parent ) + { + PARTICLE_LIFE = 0; + } + else + { + if ( my->particleTimerVariable3 <= 0 ) + { + // done with particles + PARTICLE_LIFE = 0; + } + else if ( my->particleTimerVariable1 == 0 ) + { + my->particleTimerVariable1 = 10; // refire rate + --my->particleTimerVariable3; + spell_t* spell = getSpellFromID(my->particleTimerVariable2); + if ( spell ) + { + if ( spell_t* newSpell = copySpell(spell) ) + { + if ( newSpell->elements.first ) + { + if ( spellElement_t* element = (spellElement_t*)(newSpell->elements.first->element) ) + { + // rename the propulsion + auto find = spellElementMap.find(SPELL_ELEMENT_PROPULSION_FOCI_SPRAY); + if ( find != spellElementMap.end() ) + { + strcpy(element->element_internal_name, find->second.element_internal_name); + } + } + } + Entity* missile = castSpell(parent->getUID(), newSpell, true, false); + spellDeconstructor(newSpell); + } + } + } + else + { + my->particleTimerVariable1--; + } + } + } + //else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_FOCI_SPRAY ) + //{ + // Entity* parent = uidToEntity(my->parent); + // if ( !parent ) + // { + // PARTICLE_LIFE = 0; + // } + // else + // { + // int sound = 164; + // int spellID = SPELL_FOCI_FIRE; + // if ( my->particleTimerVariable1 == 0 ) + // { + // // first fired after delay + // my->particleTimerVariable1 = 1; + // if ( sound > 0 ) + // { + // playSoundEntityLocal(parent, sound, 128); + // } + // } + + // my->x = parent->x; + // my->y = parent->y; + // my->yaw = parent->yaw; + + // Entity* entity = nullptr; + // if ( multiplayer != CLIENT && my->ticks % 2 == 0 && false ) + // { + // // damage frames + // entity = newEntity(my->particleTimerCountdownSprite, 1, map.entities, nullptr); + // entity->behavior = &actMagicMissile; + // } + // else if ( my->ticks % 10 == 2 ) + // { + // //entity = multiplayer == CLIENT ? spawnGibClient(0, 0, 0, -1) : spawnGib(my); + // real_t velocityBonus = 0.0; + // { + // real_t velocityDir = atan2(parent->vel_y, parent->vel_x); + // real_t casterDir = fmod(my->yaw, 2 * PI); + // real_t yawDiff = velocityDir - casterDir; + // while ( yawDiff > PI ) + // { + // yawDiff -= 2 * PI; + // } + // while ( yawDiff <= -PI ) + // { + // yawDiff += 2 * PI; + // } + // if ( abs(yawDiff) <= PI ) + // { + // real_t vel = sqrt(pow(parent->vel_x, 2) + pow(parent->vel_y, 2)); + // velocityBonus = std::max(0.0, cos(yawDiff) * vel); + // } + // } + // entity = spawnFociGib(parent->x, parent->y, parent->z + 2, parent->yaw, velocityBonus, parent->getUID(), my->particleTimerCountdownSprite, local_rng.rand()); + + // /*Entity* fx = createParticleAestheticOrbit(parent, 16, 50, PARTICLE_EFFECT_FOCI_ORBIT); + // fx->scalex = 0.5; + // fx->scaley = 0.5; + // fx->scalez = 0.5; + // fx->flags[SPRITE] = true; + // fx->x = parent->x + 8.0 * cos(parent->yaw); + // fx->y = parent->y + 8.0 * sin(parent->yaw); + // fx->yaw = parent->yaw; + // fx->fskill[0] = (PI / 4) + (local_rng.rand() % 2) * 3 * PI / 2; + // fx->z = 0.0;*/ + // //fx->actmagicOrbitDist = 4; + // } + // } + //} + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_IGNITE ) + { + my->removeLightField(); + if ( Entity* caster = uidToEntity(my->parent) ) + { + my->x = caster->x; + my->y = caster->y; + if ( PARTICLE_LIFE <= 5 ) + { + my->light = addLight(my->x / 16, my->y / 16, "explosion"); + if ( PARTICLE_LIFE == 5 ) + { + for ( int i = 0; i < 4; ++i ) + { + if ( Entity* fx = createParticleAestheticOrbit(caster, 233, 25, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->fskill[2] = (i / 8.0) * 2 * PI; + fx->fskill[3] += (local_rng.rand() % 10) * PI / 10.0; + fx->z = 7.5; + fx->vel_z = -0.1 + (local_rng.rand() % 10) * -.025; + fx->actmagicOrbitDist = 30; + } + } + + for ( int i = 0; i < 8; ++i ) + { + if ( Entity* fx = createParticleAestheticOrbit(caster, 233, 25, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->fskill[2] = (i / 8.0) * 2 * PI + PI / 8; + fx->fskill[3] += (local_rng.rand() % 10) * PI / 10.0; + fx->z = 7.5; + fx->vel_z = -0.1 + (local_rng.rand() % 10) * -.025; + fx->actmagicOrbitDist = 16; + } + } + + if ( multiplayer != CLIENT ) + { + int numTargets = 0; + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(caster, 3); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( !entity->flags[BURNABLE] || entity->flags[BURNING] ) + { + continue; + } + if ( entityDist(caster, entity) > 32.0 + 4.0 ) + { + continue; + } + Stat* stats = (entity->behavior == &actMonster || entity->behavior == &actPlayer) ? entity->getStats() : nullptr; + if ( stats ) + { + if ( caster && caster->getStats() ) + { + if ( caster == entity ) { continue; } + + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } + if ( entity->monsterIsTargetable() ) + { + //if ( caster->checkEnemy(entity) ) + { + if ( entity->SetEntityOnFire(caster) ) + { + applyGenericMagicDamage(caster, entity, *caster, SPELL_IGNITE, 0, true); + stats->burningInflictedBy = caster->getUID(); + if ( caster ) + { + entity->char_fire = std::min(entity->char_fire, getSpellEffectDurationFromID(SPELL_IGNITE, caster, nullptr, my)); + } + } + } + } + } + else + { + if ( entity->behavior == &actDoor + || entity->behavior == &::actIronDoor + || entity->behavior == &actBell + || entity->behavior == &actGreasePuddleSpawner + || entity->behavior == &::actFurniture || entity->behavior == &::actChest + || (entity->isDamageableCollider() && entity->isColliderDamageableByMagic()) ) + { + if ( entity->SetEntityOnFire(caster) ) + { + ++numTargets; + } + } + } + } + } + + if ( numTargets > 0 ) + { + while ( numTargets > 0 ) + { + --numTargets; + magicOnSpellCastEvent(caster, caster, nullptr, SPELL_IGNITE, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + } + } + else + { + my->light = addLight(my->x / 16, my->y / 16, "campfire"); + } + } + else + { + PARTICLE_LIFE = 0; + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_VORTEX ) + { + my->removeLightField(); + if ( my->particleTimerVariable1 == 0 ) + { + //Entity* fx = createParticleCastingIndicator(my, my->x, my->y, my->z, PARTICLE_LIFE, 0); + //fx->skill[11] = my->getUID(); + + for ( int i = 0; i < 3; ++i ) + { + static ConsoleVariable cvar_magic_vortex_sprite("/magic_vortex_sprite", 1719); + static ConsoleVariable cvar_magic_vortex_scale("/magic_vortex_scale", 0.5); + Entity* fx = createParticleAestheticOrbit(my, *cvar_magic_vortex_sprite, PARTICLE_LIFE, PARTICLE_EFFECT_VORTEX_ORBIT); + fx->scalex = *cvar_magic_vortex_scale; + fx->scaley = *cvar_magic_vortex_scale; + fx->scalez = *cvar_magic_vortex_scale; + fx->yaw += i * 2 * PI / 3; + fx->z = 7.5; + fx->actmagicOrbitDist = 4; + } + + auto poof = spawnPoof(my->x, my->y, 6, 0.5); + my->particleTimerVariable1 = 1; + my->entity_sound = playSoundEntity(my, 757, 128); + } + +#ifdef USE_FMOD + bool isPlaying = false; + if ( my->entity_sound ) + { + my->entity_sound->isPlaying(&isPlaying); + if ( isPlaying ) + { + FMOD_VECTOR position; + position.x = (float)(my->x / (real_t)16.0); + position.y = (float)(0.0); + position.z = (float)(my->y / (real_t)16.0); + my->entity_sound->set3DAttributes(&position, nullptr); + } + } +#endif + + Entity* parent = nullptr; + if ( multiplayer != CLIENT ) + { + parent = uidToEntity(my->parent); + if ( PARTICLE_LIFE == 1 ) + { + auto poof = spawnPoof(my->x, my->y, 6, 0.5, true); + playSoundEntity(my, 512, 128); + } + + if ( parent && parent->behavior == &actLeafPile ) + { + my->x = parent->x; + my->y = parent->y; + } + else + { + my->vel_x *= .95; + my->vel_y *= .95; + my->flags[NOCLIP_CREATURES] = true; + if ( abs(my->vel_x) > 0.01 || abs(my->vel_y) > 0.01 ) + { + clipMove(&my->x, &my->y, my->vel_x, my->vel_y, my); + } + } + } + + static ConsoleVariable cvar_vortex_particle_interval("/vortex_particle_interval", 30); + static ConsoleVariable cvar_vortex_particle_decay("/vortex_particle_decay", 0.9); + if ( my->particleTimerVariable1 == 1 || my->particleTimerVariable1 % *cvar_vortex_particle_interval == 0 ) + { + real_t offset = PI * (local_rng.rand() % 360) / 180.0;// -((my->ticks % 50) / 50.0) * 2 * PI; + int lifetime = PARTICLE_LIFE / 10; + + constexpr auto color = makeColor(255, 255, 255, 255); + for ( int i = 0; i < 24; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, -7.5, TICKS_PER_SECOND * 5, 16 + (i / 2) * 2) ) + { + fx->yaw = my->yaw + PI / 2 - (i / 2) * PI / 3; + fx->pitch += PI / 32; + if ( i % 2 == 1 ) + { + fx->pitch += PI; + } + fx->z = 8.0; + fx->z -= (i / 2) * 0.5; + fx->vel_z -= 0.25; + fx->fskill[0] = 0.3; // rotate + fx->scalex = 0.5;// + (i / 2) * 0.25 / 12; + fx->scaley = 0.5;// + (i / 2) * 0.25 / 12; + fx->flags[ENTITY_SKIP_CULLING] = false; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->expireAlphaRate = *cvar_vortex_particle_decay; + indicator->cacheType = AOEIndicators_t::CACHE_VORTEX; + indicator->arc = PI / 4; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + } + } + } + } + ++my->particleTimerVariable1; + + if ( multiplayer != CLIENT ) + { + { + //my->x = parent->x; + //my->y = parent->y; + + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( auto it : entLists ) + { + if ( PARTICLE_LIFE <= 0 ) + { + break; + } + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( !(entity->behavior == &actPlayer || entity->behavior == &actMonster) ) + { + continue; + } + if ( !entity->monsterIsTargetable() ) + { + continue; + } + if ( entityDist(my, entity) > 8.0 ) + { + continue; + } + + Stat* stats = entity->getStats(); + if ( !stats ) { continue; } + + if ( PARTICLE_LIFE == 1 ) + { + if ( Uint8 effectStrength = stats->getEffectActive(EFF_LIFT) ) + { + if ( isLevitating(stats) && !(stats->type == MINOTAUR) ) + { + continue; + } + + // check for neighbouring vortexes + bool foundAnother = false; + for ( auto it : entLists ) + { + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity2 = (Entity*)node->element ) + { + if ( entity2->behavior == &actParticleTimer + && entity2->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_VORTEX ) + { + if ( entityDist(entity2, entity) <= 8.0 ) + { + if ( entity2->skill[0] > 1 ) // still has life + { + auto props = getParticleEmitterHitProps(entity2->getUID(), entity); + if ( props->hits > 0 ) + { + foundAnother = true; + break; + } + } + } + } + } + if ( foundAnother ) + { + break; + } + } + if ( foundAnother ) + { + break; + } + } + + if ( foundAnother ) + { + continue; + } + + stats->EFFECTS_TIMERS[EFF_LIFT] = 1; + if ( stats->getEffectActive(EFF_ROOTED) ) + { + stats->EFFECTS_TIMERS[EFF_ROOTED] = 1; + } + + auto poof = spawnPoof(entity->x, entity->y, 4, 1.0, true); + + if ( effectStrength >= 3 ) + { + createParticleRock(entity, 78); + playSoundEntity(entity, 181, 128); + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_ABILITY_ROCK, 78); + } + + int damage = getSpellDamageFromID(my->particleTimerVariable2, parent ? parent : my, nullptr, my, my->actmagicSpellbookBonus / 100.0); + int perStatDmg = getSpellDamageSecondaryFromID(my->particleTimerVariable2, parent ? parent : my, nullptr, my, my->actmagicSpellbookBonus / 100.0); + real_t perStatMult = getSpellEffectDurationSecondaryFromID(my->particleTimerVariable2, parent ? parent : my, nullptr, my, my->actmagicSpellbookBonus / 100.0) / 100.0; + perStatDmg *= (statGetSTR(stats, entity) + statGetCON(stats, entity)) * perStatMult; + damage += perStatDmg; + real_t mult = 0.1 * (std::min(10, effectStrength - 3)); + damage *= mult; + if ( applyGenericMagicDamage(parent, entity, *my, SPELL_SLAM, damage, true) ) + { + messagePlayerColor(entity->isEntityPlayer(), MESSAGE_STATUS, + makeColorRGB(255, 0, 0), Language::get(6758)); + if ( parent && parent->behavior == &actLeafPile ) + { + stats->killer = KilledBy::LEAVES; + entity->setObituary(Language::get(6760)); + } + } + } + } + continue; + } + + if ( parent && parent->behavior == &actMonster ) + { + if ( parent == entity || parent->checkFriend(entity) ) + { + continue; + } + } + if ( parent && parent->behavior == &actPlayer ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( parent->checkFriend(entity) && parent->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 && (ticks - props->tick) < 20 ) + { + continue; + } + Uint8 strength = std::min(13, 1 + stats->getEffectActive(EFF_LIFT)); + if ( entity->setEffect(EFF_LIFT, strength, std::max(5, PARTICLE_LIFE + 21), true) ) + { + my->vel_x = 0.0; + my->vel_y = 0.0; + + if ( parent && parent->behavior == &actLeafPile ) + { + parent->vel_x = 0.0; + parent->vel_y = 0.0; + } + + entity->setEffect(EFF_ROOTED, strength, std::max(5, PARTICLE_LIFE), false); + if ( strength == 1 ) + { + messagePlayer(entity->isEntityPlayer(), MESSAGE_STATUS, Language::get(6757)); + auto poof = spawnPoof(entity->x, entity->y, 4, 0.5, true); + playSoundEntity(entity, 178, 128); + } + } + props->hits++; + props->tick = ticks; + } + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SHATTER ) + { + my->removeLightField(); + if ( Entity* caster = uidToEntity(my->parent) ) + { + my->x = caster->x; + my->y = caster->y; + if ( PARTICLE_LIFE == 5 ) + { + my->light = addLight(my->x / 16, my->y / 16, "magic_purple"); + if ( PARTICLE_LIFE == 5 ) + { + for ( int i = 0; i < 16; ++i ) + { + if ( Entity* fx = createParticleAestheticOrbit(caster, 261, 25, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->fskill[2] = (i / 16.0) * 2 * PI; + fx->fskill[3] += (local_rng.rand() % 10) * PI / 10.0; + fx->z = 7.5; + fx->vel_z = -0.1 + (local_rng.rand() % 10) * -.025; + fx->actmagicOrbitDist = 30; + } + } + + if ( multiplayer != CLIENT ) + { + int numTargets = 0; + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(caster, 3); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entityDist(caster, entity) > 32.0 + 4.0 ) + { + continue; + } + Stat* stats = (entity->behavior == &actMonster || entity->behavior == &actPlayer) ? entity->getStats() : nullptr; + if ( stats ) + { + if ( !(stats->type == MIMIC || stats->type == MINIMIMIC) ) + { + continue; + } + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + if ( entity->behavior == &actDoor + || entity->behavior == &::actIronDoor + || entity->behavior == &::actChest + || entity->behavior == &actMonster + || (entity->isDamageableCollider() && entity->isColliderDamageableByMagic()) + || entity->behavior == &::actFurniture ) + { + int damage = getSpellDamageFromID(SPELL_SHATTER_OBJECTS, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0); + if ( applyGenericMagicDamage(caster, entity, *my, SPELL_SHATTER_OBJECTS, damage, true) ) + { + if ( entity->behavior != &::actIronDoor ) + { + ++numTargets; + + if ( caster && caster->behavior == &actPlayer ) + { + if ( my->actmagicFromSpellbook ) + { + Compendium_t::Events_t::eventUpdate(caster->skill[2], Compendium_t::CPDM_SPELL_TARGETS, SPELLBOOK_SHATTER_OBJECTS, 1); + } + else + { + Compendium_t::Events_t::eventUpdate(caster->skill[2], Compendium_t::CPDM_SPELL_TARGETS, SPELL_ITEM, 1, false, SPELL_SHATTER_OBJECTS); + } + } + } + } + } + } + } + + if ( numTargets > 0 ) + { + while ( numTargets > 0 ) + { + --numTargets; + magicOnSpellCastEvent(caster, my, nullptr, SPELL_SHATTER_OBJECTS, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + } + } + else + { + my->light = addLight(my->x / 16, my->y / 16, "magic_purple_flicker"); + } + } + else + { + PARTICLE_LIFE = 0; + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_DAMAGE_LOS_AREA ) + { + if ( PARTICLE_LIFE == 1 || PARTICLE_LIFE == 3 ) // double tap for enemies inside crates etc + { + Entity* caster = uidToEntity(my->parent); + doSpellExplosionArea(my->particleTimerVariable2, my, caster, my->x, my->y, my->z, my->particleTimerVariable3); + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SWEEP_ATTACK ) + { + Entity* parent = uidToEntity(my->parent); + Stat* parentStats = parent ? parent->getStats() : nullptr; + if ( parent && parentStats && ((parent->behavior == &actPlayer ? parent->skill[9] : parent->monsterAttack) != 0) ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadius(parent->x / 16, parent->y / 16, 2); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity == parent ) + { + continue; + } + /*if ( !entity->getStats() ) + { + continue; + }*/ + if ( !(entity->behavior == &actMonster + || entity->behavior == &actPlayer + || (entity->isDamageableCollider() && entity->isColliderDamageableByMelee()) + || entity->behavior == &actDoor + || entity->behavior == &actFurniture + || entity->behavior == &::actChest + || entity->behavior == &::actIronDoor) ) + { + continue; + } + + if ( parent && parent->getStats() ) + { + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( parent->checkFriend(entity) && parent->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + auto hitProps = getParticleEmitterHitProps(my->getUID(), entity); + if ( !hitProps ) + { + continue; + } + if ( hitProps->hits > 0 ) + { + continue; + } + if ( entity->getStats() ) + { + if ( !entity->monsterIsTargetable() ) { continue; } + } + + if ( entityDist(parent, entity) > STRIKERANGE + 8.0 ) { continue; } + + real_t tangent = atan2(entity->y - parent->y, entity->x - parent->x); + real_t angle = parent->yaw - tangent; + while ( angle >= PI ) + { + angle -= PI * 2; + } + while ( angle < -PI ) + { + angle += PI * 2; + } + if ( abs(angle) > 1 * PI / 3 ) { continue; } + bool oldPassable = entity->flags[PASSABLE]; + entity->flags[PASSABLE] = false; + real_t d = lineTraceTarget(parent, parent->x, parent->y, tangent, STRIKERANGE, LINETRACE_ATK_CHECK_FRIENDLYFIRE + | LINETRACE_TELEKINESIS, false, entity); + entity->flags[PASSABLE] = oldPassable; + if ( hit.entity != entity ) + { + continue; + } + + { + Sint32 prevAtk = parent->monsterAttack; + Sint32 prevAtkTime = parent->monsterAttackTime; + parent->attack(MONSTER_POSE_SWEEP_ATTACK_NO_UPDATE, Stat::getMaxAttackCharge(parent->getStats()), entity); + parent->monsterAttack = prevAtk; + parent->monsterAttackTime = prevAtkTime; + + ++hitProps->hits; + hitProps->tick = ticks; + } + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPIRIT_WEAPON_ATTACK ) + { + Entity* parent = uidToEntity(my->parent); + Stat* parentStats = parent ? parent->getStats() : nullptr; + if ( parent && parentStats && parent->monsterAttack != 0 && parentStats->getEffectActive(EFF_KNOCKBACK) ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadius(parent->x / 16, parent->y / 16, 1); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity == parent ) + { + continue; + } + /*if ( !entity->getStats() ) + { + continue; + }*/ + + if ( !(entity->behavior == &actMonster + || entity->behavior == &actPlayer + || (entity->isDamageableCollider() && entity->isColliderDamageableByMelee()) + || entity->behavior == &actDoor + || entity->behavior == &actFurniture + || entity->behavior == &::actChest + || entity->behavior == &::actIronDoor) ) + { + continue; + } + + if ( !entityInsideEntity(parent, entity) ) + { + continue; + } + if ( parent && parent->getStats() ) + { + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( parent->checkFriend(entity) && parent->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + auto hitProps = getParticleEmitterHitProps(my->getUID(), entity); + if ( !hitProps ) + { + continue; + } + if ( hitProps->hits > 0 ) + { + continue; + } + + if ( entity->getStats() ) + { + if ( !entity->monsterIsTargetable() ) { continue; } + } + + real_t tangent = atan2(entity->y - parent->y, entity->x - parent->x); + bool oldPassable = entity->flags[PASSABLE]; + entity->flags[PASSABLE] = false; + real_t d = lineTraceTarget(parent, parent->x, parent->y, tangent, 8.0, 0, false, entity); + entity->flags[PASSABLE] = oldPassable; + if ( hit.entity != entity ) + { + continue; + } + + { + Sint32 prevAtk = parent->monsterAttack; + Sint32 prevAtkTime = parent->monsterAttackTime; + parent->attack(0, 0, entity); + parent->monsterAttack = prevAtk; + parent->monsterAttackTime = prevAtkTime; + + ++hitProps->hits; + hitProps->tick = ticks; + } + } + } + } + else + { + PARTICLE_LIFE = 0; + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL_ROLL ) + { + Entity* parent = uidToEntity(my->parent); + Stat* parentStats = parent ? parent->getStats() : nullptr; + if ( parent && parentStats && parent->monsterAttack == MONSTER_POSE_EARTH_ELEMENTAL_ROLL && parentStats->getEffectActive(EFF_KNOCKBACK) ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadius(parent->x / 16, parent->y / 16, 1); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity == parent ) + { + continue; + } + if ( !entity->getStats() ) + { + continue; + } + if ( !entityInsideEntity(parent, entity) ) + { + continue; + } + if ( parent && parent->getStats() ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( parent->checkFriend(entity) && parent->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + auto hitProps = getParticleEmitterHitProps(my->getUID(), entity); + if ( !hitProps ) + { + continue; + } + if ( hitProps->hits > 0 ) + { + continue; + } + if ( !entity->monsterIsTargetable() ) { continue; } + + real_t tangent = atan2(entity->y - parent->y, entity->x - parent->x); + bool oldPassable = entity->flags[PASSABLE]; + entity->flags[PASSABLE] = false; + real_t d = lineTraceTarget(parent, parent->x, parent->y, tangent, 8.0, 0, false, entity); + entity->flags[PASSABLE] = oldPassable; + if ( hit.entity != entity ) + { + continue; + } + + { + int damage = std::max(10, statGetCON(parentStats, parent)); + if ( Stat* entityStats = entity->getStats() ) + { + Sint32 oldHP = entityStats->HP; + if ( applyGenericMagicDamage(parent, entity, *parent, SPELL_NONE, damage, true, true) ) + { + if ( entityStats->HP == 0 && oldHP > entityStats->HP ) + { + if ( Entity* leader = parent->monsterAllyGetPlayerLeader() ) + { + if ( leader->checkEnemy(entity) ) + { + steamAchievementClient(leader->skill[2], "BARONY_ACH_BOLDER_BOULDER"); + } + } + } + ++hitProps->hits; + hitProps->tick = ticks; + } + } + } + } + } + } + else + { + PARTICLE_LIFE = 0; + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_BOOBY_TRAP ) + { + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, "magic_purple"); + } + if ( PARTICLE_LIFE == 7 ) + { + if ( multiplayer != CLIENT ) + { + Entity* target = uidToEntity(my->particleTimerTarget); + if ( target ) + { + Entity* caster = uidToEntity(my->parent); + if ( !caster ) + { + caster = my; + } + + if ( target->behavior == &actDoor ) + { + target->doorHandleDamageMagic(target->doorHealth, *my, caster); + magicOnSpellCastEvent(caster, caster, target, SPELL_BOOBY_TRAP, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + else if ( target->behavior == &::actChest || target->getMonsterTypeFromSprite() == MIMIC ) + { + target->chestHandleDamageMagic(my->particleTimerVariable1, *my, caster); + magicOnSpellCastEvent(caster, caster, target, SPELL_BOOBY_TRAP, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + else if ( target->isDamageableCollider() ) + { + target->colliderHandleDamageMagic(target->colliderCurrentHP, *my, caster); + magicOnSpellCastEvent(caster, caster, target, SPELL_BOOBY_TRAP, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + else if ( target->behavior == &::actFurniture ) + { + target->furnitureHandleDamageMagic(target->furnitureHealth, *my, caster); + magicOnSpellCastEvent(caster, caster, target, SPELL_BOOBY_TRAP, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + } + if ( PARTICLE_LIFE == 5 ) + { + if ( multiplayer != CLIENT ) + { + spawnExplosion(my->x, my->y, my->z); + Entity* caster = uidToEntity(my->parent); + if ( !caster ) + { + caster = my; + } + + Entity* target = uidToEntity(my->particleTimerTarget); + + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entityDist(my, entity) > 32.0 + 4.0 ) + { + continue; + } + if ( entity == target || entity == my || entity == caster ) + { + continue; + } + bool mimic = entity->isInertMimic(); + if ( caster && caster->getStats() ) + { + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + bool oldPassable = entity->flags[PASSABLE]; + if ( entity->behavior == &actGreasePuddleSpawner ) + { + entity->flags[PASSABLE] = false; + } + real_t d = lineTraceTarget(my, my->x, my->y, tangent, 32.0, 0, false, entity); + entity->flags[PASSABLE] = oldPassable; + if ( hit.entity != entity ) + { + continue; + } + + int damage = my->particleTimerVariable1; + if ( (entity->behavior == &actMonster && !mimic) || entity->behavior == &actPlayer ) + { + if ( !entity->monsterIsTargetable() ) { continue; } + if ( Stat* stats = entity->getStats() ) + { + if ( stats->getEffectActive(EFF_MAGIC_GREASE) ) + { + damage *= 2; + } + applyGenericMagicDamage(caster, entity, *my, SPELL_BOOBY_TRAP, damage, true); + if ( entity->SetEntityOnFire(caster) ) + { + if ( caster ) + { + entity->char_fire = std::min(entity->char_fire, getSpellEffectDurationFromID(SPELL_BOOBY_TRAP, caster, nullptr, my)); + } + } + if ( caster ) + { + stats->burningInflictedBy = caster->getUID(); + } + } + } + else if ( entity->behavior == &actGreasePuddleSpawner ) + { + entity->SetEntityOnFire(caster); + } + else + { + if ( applyGenericMagicDamage(caster, entity, *my, SPELL_BOOBY_TRAP, damage, true) ) + { + entity->SetEntityOnFire(caster); + } + } + } + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_LIGHTNING ) + { + if ( my->ticks == 1 ) + { + //createParticleCastingIndicator(my, my->x, my->y, my->z, PARTICLE_LIFE, my->getUID()); + for ( int i = 0; i < 3; ++i ) + { + Entity* fx = createParticleAestheticOrbit(my, 1758, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_STATIC_ORBIT); + //fx->x = my->x + 40.0/*dist * (data.dist)*/ * cos(my->yaw); + //fx->y = my->y + 40.0/*dist * (data.dist)*/ * sin(my->yaw); + fx->z = 7.5 - 2.0 * i; + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += i * 2 * PI / 3; + if ( i != 0 ) + { + fx->actmagicNoLight = 1; + } + } + + if ( multiplayer != CLIENT ) + { + playSoundEntity(my, 806, 128); + } + } + + if ( multiplayer != CLIENT ) + { + Entity* parent = uidToEntity(my->parent); + Entity* closestEntity = nullptr; + if ( my->actmagicOrbitHitTargetUID1 != 0 ) + { + closestEntity = uidToEntity(my->actmagicOrbitHitTargetUID1); + if ( !closestEntity ) + { + my->actmagicOrbitHitTargetUID1 = 0; + } + } + if ( my->actmagicOrbitHitTargetUID1 == 0 && my->ticks < 100 ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + real_t dist = 10000.0; + Uint8 hadStaticEffect = 0; + for ( auto it : entLists ) + { + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( parent && entity == parent ) + { + continue; + } + if ( entity->behavior == &actMonster || entity->behavior == &actPlayer ) + { + if ( parent && parent->checkFriend(entity) ) + { + continue; + } + if ( !entity->monsterIsTargetable() ) { continue; } + real_t newDist = entityDist(my, entity); + Uint8 effectStrength = entity->getStats() ? entity->getStats()->getEffectActive(EFF_STATIC) : 0; + if ( ((newDist < dist) || (effectStrength >= hadStaticEffect)) && newDist < 64.0 ) + { + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + real_t d = lineTraceTarget(my, my->x, my->y, tangent, 64.0, 0, false, entity); + if ( hit.entity == entity ) + { + if ( effectStrength > hadStaticEffect ) + { + if ( effectStrength > hadStaticEffect ) + { + closestEntity = entity; + dist = newDist; + hadStaticEffect = effectStrength; + } + else if ( effectStrength == hadStaticEffect ) + { + if ( newDist < dist ) + { + closestEntity = entity; + dist = newDist; + hadStaticEffect = effectStrength; + } + } + } + else if ( effectStrength == hadStaticEffect ) + { + if ( newDist < dist ) + { + closestEntity = entity; + dist = newDist; + } + } + } + } + } + } + } + + if ( closestEntity ) + { + if ( dist < 8.0 ) + { + my->actmagicOrbitHitTargetUID1 = closestEntity->getUID(); + } + } + } + + my->vel_x = 0.0; + my->vel_y = 0.0; + if ( closestEntity ) + { + real_t dist = entityDist(my, closestEntity); + real_t tangent = atan2(closestEntity->y - my->y, closestEntity->x - my->x); + real_t speedMult = 100.0 - 0.5 * std::min((Uint32)100, my->ticks); // build up faster to impact + real_t staticEffectSpeedMult = 0.0; + if ( Stat* targetStats = closestEntity->getStats() ) + { + staticEffectSpeedMult += (0.1 + std::min(1.6, targetStats->getEffectActive(EFF_STATIC) * 0.3)); + } + if ( my->ticks >= 100 ) // first impact + { + if ( my->ticks >= (100 - 5 * staticEffectSpeedMult) ) + { + if ( my->actmagicOrbitHitTargetUID2 != 0 ) + { + if ( dist < 16.0 ) + { + my->actmagicOrbitHitTargetUID2 = closestEntity->getUID(); // follow this target + } + } + } + + Entity* followTarget = my->actmagicOrbitHitTargetUID2 != 0 ? uidToEntity(my->actmagicOrbitHitTargetUID2) : nullptr; + if ( followTarget ) + { + my->x = followTarget->x; + my->y = followTarget->y; + } + else + { + speedMult = 100.0; + real_t speed = std::min(dist, std::max(16.0, 64.0 - dist) / speedMult); + my->vel_x = speed * cos(tangent) * staticEffectSpeedMult; + my->vel_y = speed * sin(tangent) * staticEffectSpeedMult; + } + } + else + { + real_t speed = std::min(dist, std::max(16.0, 64.0 - dist) / speedMult); + my->vel_x = speed * cos(tangent) * staticEffectSpeedMult; + my->vel_y = speed * sin(tangent) * staticEffectSpeedMult; + } + } + + my->x += my->vel_x; + my->y += my->vel_y; + } + auto findEffects = particleTimerEffects.find(my->getUID()); + if ( findEffects != particleTimerEffects.end() ) + { + auto findEffect = findEffects->second.effectMap.find(my->ticks); + if ( findEffect != findEffects->second.effectMap.end() ) + { + auto& data = findEffect->second; + if ( data.effectType == ParticleTimerEffect_t::EFFECT_LIGHTNING_BOLT ) + { + if ( data.firstEffect ) + { + serverSpawnMiscParticles(my, PARTICLE_EFFECT_LIGHTNING_SEQ, 0); + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, 0.0, TICKS_PER_SECOND, 32.0) ) + { + //fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(252, 224, 0); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + } + } + } + Entity* fx = createFloorMagic(data.effectType, 1757, my->x + data.x, my->y + data.y, -8.5, data.yaw, TICKS_PER_SECOND / 8); + fx->parent = my->getUID(); + if ( my->actmagicSpellbookBonus > 0 ) + { + fx->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + } + fx->actmagicFromSpellbook = my->actmagicFromSpellbook; + + fx->actmagicOrbitHitTargetUID1 = my->actmagicOrbitHitTargetUID2; + if ( data.sfx ) + { + playSoundEntityLocal(fx, data.sfx, 128); + } + floorMagicSetLightningParticle(fx); + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_ETERNALS_GAZE2 ) + { + if ( my->ticks == 1 ) + { + static ConsoleVariable cvar_magic_egaze7("/magic_egaze7", -8.25); + static ConsoleVariable cvar_magic_egaze9("/magic_egaze9", 0.05); + for ( int i = 0; i < 5; ++i ) + { + Entity* fx = createParticleAestheticOrbit(my, 1865, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_ETERNALS_GAZE2); + fx->fskill[2] = *cvar_magic_egaze7; + fx->fskill[3] = -24.25; + fx->z = *cvar_magic_egaze7; + fx->scaley = 1.0; + fx->actmagicOrbitDist = 12.0; + fx->fskill[6] = *cvar_magic_egaze9; // yaw + if ( i <= 2 ) + { + fx->yaw = i * 2 * PI / 3 + 3 * PI / 4; + } + else + { + fx->yaw = (i - 2) * PI + 3 * PI / 4; + fx->actmagicOrbitDist = 1.0; + fx->fskill[6] *= -2.0; + fx->flags[GENIUS] = true; + } + fx->sizex = 4; + fx->sizey = 4; + } + + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, 0.0, TICKS_PER_SECOND, 32.0) ) + { + //fx->actSpriteFollowUID = 0; + //fx->actSpriteCheckParentExists = 0; + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(236, 240, 144); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + } + } + } + + if ( multiplayer != CLIENT ) + { + if ( my->ticks == 1 + || my->ticks == 1 + 10 + || my->ticks == 1 + 20 ) + { + Entity* caster = uidToEntity(my->parent); + if ( Entity* aoe = createSpellExplosionArea(SPELL_ETERNALS_GAZE, caster, my->x, my->y, my->z, 12.0, 0, nullptr) ) + { + aoe->actmagicFromSpellbook = my->actmagicFromSpellbook; + aoe->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + } + } + + Entity* target = uidToEntity(my->actmagicOrbitHitTargetUID1); + my->vel_x = 0.0; + my->vel_y = 0.0; + if ( target ) + { + real_t dist = entityDist(my, target); + real_t tangent = atan2(target->y - my->y, target->x - my->x); + real_t speed = std::min(dist, std::max(16.0, 64.0 - dist) / 100.0); + my->vel_x = speed * cos(tangent); + my->vel_y = speed * sin(tangent); + } + + my->x += my->vel_x; + my->y += my->vel_y; + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_ETERNALS_GAZE ) + { + if ( multiplayer != CLIENT ) + { + Entity* parent = uidToEntity(my->parent); + Entity* closestEntity = nullptr; + if ( my->actmagicOrbitHitTargetUID1 != 0 ) + { + closestEntity = uidToEntity(my->actmagicOrbitHitTargetUID1); + if ( !closestEntity ) + { + my->actmagicOrbitHitTargetUID1 = 0; + } + } + if ( my->actmagicOrbitHitTargetUID1 == 0 ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + real_t dist = 10000.0; + for ( auto it : entLists ) + { + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( parent && entity == parent ) + { + continue; + } + if ( entity->behavior == &actMonster || entity->behavior == &actPlayer ) + { + if ( parent && parent->checkFriend(entity) ) + { + continue; + } + if ( !entity->monsterIsTargetable() ) { continue; } + real_t newDist = entityDist(my, entity); + if ( newDist < dist && newDist < 64.0 ) + { + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + real_t d = lineTraceTarget(my, my->x, my->y, tangent, 64.0, 0, false, entity); + if ( hit.entity == entity ) + { + closestEntity = entity; + dist = newDist; + } + } + } + } + } + + if ( closestEntity ) + { + if ( dist < 8.0 ) + { + my->actmagicOrbitHitTargetUID1 = closestEntity->getUID(); + } + } + } + + my->vel_x = 0.0; + my->vel_y = 0.0; + if ( closestEntity ) + { + real_t dist = entityDist(my, closestEntity); + real_t tangent = atan2(closestEntity->y - my->y, closestEntity->x - my->x); + real_t speed = std::min(dist, std::max(16.0, 64.0 - dist) / 100.0); + my->vel_x = speed * cos(tangent); + my->vel_y = speed * sin(tangent); + } + + my->x += my->vel_x; + my->y += my->vel_y; + + if ( my->ticks == 3 * TICKS_PER_SECOND ) + { + Entity* parent = uidToEntity(my->parent); + Entity* target = uidToEntity(my->actmagicOrbitHitTargetUID1); + Entity* spellTimer = createParticleTimer(target, 2 * TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_ETERNALS_GAZE2; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = my->yaw; + if ( !target ) + { + spellTimer->x = my->x; + spellTimer->y = my->y; + } + spellTimer->parent = parent ? parent->getUID() : 0; + spellTimer->actmagicOrbitHitTargetUID1 = target ? my->actmagicOrbitHitTargetUID1 : 0; + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + } + if ( my->ticks == 1 ) + { + static ConsoleVariable cvar_magic_egaze1("/magic_egaze1", -16.25); + static ConsoleVariable cvar_magic_egaze2("/magic_egaze2", 7.75); + for ( int i = 0; i < 4; ++i ) + { + Entity* fx = createParticleAestheticOrbit(my, 1865, 4 * TICKS_PER_SECOND, PARTICLE_EFFECT_ETERNALS_GAZE1); + fx->yaw = i * PI / 2 + 3 * PI / 4; + fx->fskill[2] = *cvar_magic_egaze2; + fx->fskill[3] = *cvar_magic_egaze1; + fx->fskill[6] = 0.15; // yaw + fx->fskill[7] = 0.1; + fx->z = *cvar_magic_egaze2; + fx->scaley = 1.0; + //fx->ditheringOverride = 6; + } + + //for ( int i = 0; i < 3; ++i ) + //{ + // Entity* fx = createParticleAestheticOrbit(my, 1867, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_ETERNALS_GAZE_STATIC); + // //fx->x = my->x + 40.0/*dist * (data.dist)*/ * cos(my->yaw); + // //fx->y = my->y + 40.0/*dist * (data.dist)*/ * sin(my->yaw); + // fx->z = 7.5 - 2.0 * i; + // fx->scalex = 0.25; + // fx->scaley = 0.25; + // fx->scalez = 0.25; + // fx->actmagicOrbitDist = 20; + // fx->yaw += i * 2 * PI / 3; + // if ( i != 0 ) + // { + // fx->actmagicNoLight = 1; + // } + //} + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SHATTER_EARTH + || my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL ) + { + int x = static_cast(my->x / 16); + int y = static_cast(my->y / 16); + if ( x > 0 && x < map.width - 2 && y > 0 && y < map.height - 2 ) + { + if ( my->ticks == 1 ) + { + createParticleShatterEarth(my, uidToEntity(my->parent), my->x, my->y, + my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SHATTER_EARTH ? SPELL_SHATTER_EARTH + : SPELL_EARTH_ELEMENTAL); + } + if ( my->ticks < TICKS_PER_SECOND ) + { + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + bool tallCeiling = false; + if ( !map.tiles[(MAPLAYERS - 1) + mapIndex] ) + { + tallCeiling = true; + } + if ( my->ticks % 4 == 0 ) + { + //for ( int i = 0; i < 6; ++i ) + int i = local_rng.rand() % 8; + Entity* fx = createParticleAestheticOrbit(my, 1870, TICKS_PER_SECOND, PARTICLE_EFFECT_SHATTER_EARTH_ORBIT); + fx->parent = 0; + fx->z = -8.0 + (tallCeiling ? -16.0 : 0.0); + fx->actmagicOrbitDist = 4; + fx->actmagicOrbitStationaryX = x * 16.0 + 8.0; + fx->actmagicOrbitStationaryY = y * 16.0 + 8.0; + fx->yaw = i * PI / 8; + fx->x = fx->actmagicOrbitStationaryX + fx->actmagicOrbitDist * cos(fx->yaw); + fx->y = fx->actmagicOrbitStationaryY + fx->actmagicOrbitDist * sin(fx->yaw); + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPLINTER_GEAR ) + { + if ( my->ticks % 4 == 0 ) + { + //for ( int i = 0; i < 6; ++i ) + int i = (my->ticks / 4) % 3; + Entity* fx = createParticleAestheticOrbit(my, 2205, TICKS_PER_SECOND, PARTICLE_EFFECT_SHATTER_EARTH_ORBIT); + fx->parent = 0; + fx->z = my->z; + fx->yaw = my->yaw + 2 * i * PI / 3; + fx->vel_x = 0.5 * cos(fx->yaw); + fx->vel_y = 0.5 * sin(fx->yaw); + fx->actmagicOrbitDist = 2; + fx->actmagicOrbitStationaryX = my->x; + fx->actmagicOrbitStationaryY = my->y; + fx->x = fx->actmagicOrbitStationaryX + fx->actmagicOrbitDist * cos(fx->yaw); + fx->y = fx->actmagicOrbitStationaryY + fx->actmagicOrbitDist * sin(fx->yaw); + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_BASTION_MUSHROOM ) + { + if ( multiplayer != CLIENT ) + { + Entity* parent = uidToEntity(my->parent); + if ( parent ) + { + my->x = parent->x; + my->y = parent->y; + } + if ( my->ticks == 1 ) + { + createMushroomSpellEffect(parent, my->x, my->y); + serverSpawnMiscParticlesAtLocation(my->x, my->y, 0, PARTICLE_EFFECT_BASTION_MUSHROOM, 0, 0, 0, my->parent); + + if ( my->particleTimerVariable3 == SPELL_SPORE_BOMB ) + { + floorMagicCreateSpores(parent ? parent : my, my->x, my->y, parent, 0, SPELL_SPORES); + } + } + + if ( my->ticks <= 50 && parent ) + { + real_t range = 32.0; + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1 + (range / 16)); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( !(entity->behavior == &actPlayer || entity->behavior == &actMonster) ) + { + continue; + } + if ( !entity->monsterIsTargetable() ) + { + continue; + } + if ( entityDist(my, entity) > (real_t)(range + 4.0) ) + { + continue; + } + + Stat* stats = entity->getStats(); + if ( !stats ) { continue; } + + if ( parent == entity ) + { + continue; + } + if ( parent && parent->behavior == &actMonster ) + { + if ( parent->checkFriend(entity) ) + { + continue; + } + } + if ( parent && parent->behavior == &actPlayer ) + { + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( parent->checkFriend(entity) && parent->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 ) + { + continue; + } + + real_t tangent = atan2(entity->y - parent->y, entity->x - parent->x); + bool oldPassable = entity->flags[PASSABLE]; + entity->flags[PASSABLE] = false; + real_t d = lineTraceTarget(parent, parent->x, parent->y, tangent, (real_t)(range + 4.0), 0, false, entity); + entity->flags[PASSABLE] = oldPassable; + if ( hit.entity != entity ) + { + continue; + } + + int damage = getSpellDamageFromID(SPELL_BASTION_MUSHROOM, parent, nullptr, my); + if ( applyGenericMagicDamage(parent, entity, *parent, SPELL_BASTION_MUSHROOM, damage, true, true) ) + { + props->hits++; + props->tick = ticks; + + bool wasEffected = stats->getEffectActive(EFF_POISONED); + if ( entity->setEffect(EFF_POISONED, true, 3 * TICKS_PER_SECOND + 10, false) ) + { + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 944); + stats->poisonKiller = parent->getUID(); + } + + if ( entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + real_t pushbackMultiplier = 0.9; + real_t tangent = atan2(entity->y - parent->y, entity->x - parent->x); + if ( entity->behavior == &actPlayer ) + { + if ( !players[entity->skill[2]]->isLocalPlayer() ) + { + entity->monsterKnockbackVelocity = pushbackMultiplier; + entity->monsterKnockbackTangentDir = tangent; + serverUpdateEntityFSkill(entity, 11); + serverUpdateEntityFSkill(entity, 9); + } + else + { + entity->monsterKnockbackVelocity = pushbackMultiplier; + entity->monsterKnockbackTangentDir = tangent; + } + } + else if ( entity->behavior == &actMonster ) + { + entity->vel_x = cos(tangent) * pushbackMultiplier; + entity->vel_y = sin(tangent) * pushbackMultiplier; + entity->monsterKnockbackVelocity = 0.01; + entity->monsterKnockbackUID = parent->colliderCreatedParent; + entity->monsterKnockbackTangentDir = tangent; + } + } + } + } + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_ROOTS1 ) + { + if ( my->ticks == 1 && multiplayer != CLIENT ) + { + Entity* fx = createFloorMagic(ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF, + 1765, my->x, my->y, 7.5, my->yaw, PARTICLE_LIFE); + //playSoundEntity(fx, data.sfx, 64); + fx->parent = my->getUID(); + fx->actmagicNoParticle = 1; + fx->sizex = 6; + fx->sizey = 6; + floorMagicParticleSetUID(*fx, true); + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_ROOTS_SUSTAIN ) + { + if ( my->ticks == 1 && multiplayer != CLIENT ) + { + Entity* fx = createFloorMagic(ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF_SUSTAIN, + 1765, my->x, my->y, 7.5, my->yaw, PARTICLE_LIFE); + //playSoundEntity(fx, data.sfx, 64); + fx->parent = my->getUID(); + fx->actmagicNoParticle = 1; + fx->sizex = 6; + fx->sizey = 6; + floorMagicParticleSetUID(*fx, true); + } + + if ( multiplayer != CLIENT ) + { + Entity* parent = uidToEntity(my->parent); + bool sustain = false; + if ( parent && parent->behavior == &actPlayer ) + { + if ( Stat* stats = parent->getStats() ) + { + for ( node_t* node = stats->magic_effects.first; node; node = node->next ) + { + if ( spell_t* spell = (spell_t*)node->element ) + { + if ( spell->ID == SPELL_BASTION_ROOTS && spell->sustain ) + { + sustain = true; + break; + } + } + } + } + } + + if ( !sustain ) + { + PARTICLE_LIFE = -1; + } + else + { + PARTICLE_LIFE = std::max(PARTICLE_LIFE, TICKS_PER_SECOND); + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE ) + { + if ( my->ticks == 1 && multiplayer != CLIENT ) + { + Entity* fx = createFloorMagic(ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE, + 1765, my->x, my->y, 7.5, my->yaw, PARTICLE_LIFE); + //playSoundEntity(fx, data.sfx, 64); + fx->parent = my->getUID(); + fx->actmagicNoParticle = 1; + fx->sizex = 6; + fx->sizey = 6; + floorMagicParticleSetUID(*fx, true); + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE_VOID ) + { + if ( my->ticks == 1 && multiplayer != CLIENT ) + { + Entity* fx = createFloorMagic(ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE_VOID, + 2199, my->x, my->y, 7.5, my->yaw, PARTICLE_LIFE); + //playSoundEntity(fx, data.sfx, 64); + fx->parent = my->getUID(); + fx->actmagicNoParticle = 1; + fx->sizex = 6; + fx->sizey = 6; + floorMagicParticleSetUID(*fx, true); + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_ROOTS_PATH ) + { + if ( my->ticks == 1 && multiplayer != CLIENT ) + { + Entity* fx = createFloorMagic(ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_PATH, + 1765, my->x, my->y, 7.5, my->yaw, PARTICLE_LIFE); + //playSoundEntity(fx, data.sfx, 64); + fx->parent = my->getUID(); + fx->actmagicNoParticle = 1; + fx->sizex = 6; + fx->sizey = 6; + floorMagicParticleSetUID(*fx, true); + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL_DIE ) + { + if ( my->ticks == 1 ) + { + int x = static_cast(my->x) >> 4; + int y = static_cast(my->y) >> 4; + int mapIndex = (y)*MAPLAYERS + (x) * MAPLAYERS * map.height; + if ( x > 0 && y > 0 && x < map.width - 1 && y < map.height - 1 ) + { + if ( map.tiles[mapIndex] ) + { + Entity* entity = newEntity(1869, 1, map.entities, nullptr); //Gib entity. + entity->x = my->x; + entity->y = my->y; + entity->z = 7.0; + entity->sizex = 2; + entity->sizey = 2; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->behavior = &actEarthElementalDeathGib; + entity->ditheringDisabled = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[UNCLICKABLE] = true; + entity->scalex = 0.05; + entity->scaley = 0.05; + entity->scalez = 0.05; + entity->skill[0] = 5 * TICKS_PER_SECOND; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + + } + } + } + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + my->vel_z = -0.05; + my->z += my->vel_z; + if ( my->ticks % 4 == 0 ) + { + int i = local_rng.rand() % 8; + Entity* fx = createParticleAestheticOrbit(my, 1870, 30, PARTICLE_EFFECT_SHATTER_EARTH_ORBIT); + fx->parent = 0; + fx->z = my->z; + fx->actmagicOrbitDist = 2; + fx->actmagicOrbitStationaryX = my->x; + fx->actmagicOrbitStationaryY = my->y; + fx->yaw = i * PI / 8; + fx->x = fx->actmagicOrbitStationaryX + fx->actmagicOrbitDist * cos(fx->yaw); + fx->y = fx->actmagicOrbitStationaryY + fx->actmagicOrbitDist * sin(fx->yaw); + } + + if ( my->scalex <= 0.0 ) + { + list_RemoveNode(my->mynode); + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_TRAP_SABOTAGED ) + { + if ( my->ticks == 1 ) + { + int x = static_cast(my->x) >> 4; + int y = static_cast(my->y) >> 4; + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( x > 0 && y > 0 && x < map.width - 1 && y < map.height - 1 ) + { + if ( map.tiles[mapIndex] ) + { + Entity* entity = newEntity(1869, 1, map.entities, nullptr); //Gib entity. + entity->x = my->x; + entity->y = my->y; + entity->z = 7.0; + entity->sizex = 2; + entity->sizey = 2; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->behavior = &actEarthElementalDeathGib; + entity->ditheringDisabled = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[UNCLICKABLE] = true; + entity->scalex = 0.05; + entity->scaley = 0.05; + entity->scalez = 0.05; + entity->skill[0] = -1; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + + } + } + } + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + my->vel_z = -0.05; + my->z += my->vel_z; + if ( my->ticks % 4 == 0 ) + { + int i = local_rng.rand() % 8; + Entity* fx = createParticleAestheticOrbit(my, 1870, TICKS_PER_SECOND, PARTICLE_EFFECT_SHATTER_EARTH_ORBIT); + fx->parent = 0; + fx->z = my->z; + fx->actmagicOrbitDist = 2; + fx->actmagicOrbitStationaryX = my->x; + fx->actmagicOrbitStationaryY = my->y; + fx->yaw = i * PI / 8; + fx->x = fx->actmagicOrbitStationaryX + fx->actmagicOrbitDist * cos(fx->yaw); + fx->y = fx->actmagicOrbitStationaryY + fx->actmagicOrbitDist * sin(fx->yaw); + } + + if ( my->scalex <= 0.0 ) + { + list_RemoveNode(my->mynode); + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPORES_TRAIL ) + { + auto findEffects = particleTimerEffects.find(my->getUID()); + if ( findEffects != particleTimerEffects.end() ) + { + // moved to actThrown + //if ( auto projectile = uidToEntity(my->particleTimerVariable4) ) + //{ + // int x = static_cast(projectile->x) / 16; + // int y = static_cast(projectile->y) / 16; + // bool freeSpot = true; + // Uint32 lastTick = 1; + // for ( auto& eff : findEffects->second.effectMap ) + // { + // if ( static_cast(eff.second.x) / 16 == x + // && static_cast(eff.second.x) / 16 == y ) + // { + // freeSpot = false; + // } + // lastTick = std::max(eff.first, lastTick); + // } + // if ( freeSpot ) + // { + // auto& effect = findEffects->second.effectMap[std::max(my->ticks + 1, lastTick + 2)]; // insert x ticks beyond last effect + // if ( findEffects->second.effectMap.size() == 1 ) + // { + // effect.firstEffect = true; + // } + // int spellID = my->particleTimerVariable2; + // auto particleEffectType = (spellID == SPELL_SPORES || spellID == SPELL_MYCELIUM_SPORES) + // ? ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM + // : ParticleTimerEffect_t::EffectType::EFFECT_SPORES; + // effect.effectType = particleEffectType; + // effect.x = x * 16.0 + 8.0; + // effect.y = y * 16.0 + 8.0; + // effect.yaw = 0.0; + // } + //} + + auto findEffect = findEffects->second.effectMap.find(my->ticks); + if ( findEffect != findEffects->second.effectMap.end() ) + { + auto& data = findEffect->second; + int x = static_cast(data.x) >> 4; + int y = static_cast(data.y) >> 4; + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( x > 0 && y > 0 && x < map.width - 1 && y < map.height - 1 + && !map.tiles[OBSTACLELAYER + mapIndex] ) + { + auto entLists = TileEntityList.getEntitiesWithinRadius(x, y, 0); + bool freeSpot = true; + std::vector toDelete; + for ( auto it : entLists ) + { + if ( !freeSpot ) + { + break; + } + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->behavior == &actParticleFloorMagic && + (entity->actfloorMagicType == data.effectType + || entity->actfloorMagicType == ParticleTimerEffect_t::EFFECT_SPORES + || entity->actfloorMagicType == ParticleTimerEffect_t::EFFECT_MYCELIUM) ) + { + if ( my->particleTimerVariable2 == SPELL_SPORE_BOMB + || my->particleTimerVariable2 == SPELL_MYCELIUM_BOMB ) + { + toDelete.push_back(entity); + } + else + { + freeSpot = false; + break; + } + } + } + } + } + for ( auto ent : toDelete ) + { + ent->removeLightField(); + list_RemoveNode(ent->mynode); + } + if ( freeSpot ) + { + Entity* fx = createFloorMagic(data.effectType, my->particleTimerCountdownSprite, data.x, data.y, 4.0, data.yaw, PARTICLE_LIFE); + fx->sizex = 8; + fx->sizey = 8; + fx->parent = my->getUID(); + floorMagicParticleSetUID(*fx, true); + } + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPORES ) + { + auto findEffects = particleTimerEffects.find(my->getUID()); + if ( findEffects != particleTimerEffects.end() ) + { + auto findEffect = findEffects->second.effectMap.find(my->ticks); + if ( findEffect != findEffects->second.effectMap.end() ) + { + auto& data = findEffect->second; + int x = static_cast(data.x) >> 4; + int y = static_cast(data.y) >> 4; + int mapIndex = (y)*MAPLAYERS + (x) * MAPLAYERS * map.height; + if ( x > 0 && y > 0 && x < map.width - 1 && y < map.height - 1 + && !map.tiles[OBSTACLELAYER + mapIndex] ) + { + auto entLists = TileEntityList.getEntitiesWithinRadius(x, y, 0); + bool freeSpot = true; + std::vector toDelete; + for ( auto it : entLists ) + { + if ( !freeSpot ) + { + break; + } + for ( node_t* node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->behavior == &actParticleFloorMagic && + (entity->actfloorMagicType == data.effectType + || entity->actfloorMagicType == ParticleTimerEffect_t::EFFECT_SPORES + || entity->actfloorMagicType == ParticleTimerEffect_t::EFFECT_MYCELIUM) ) + { + if ( my->particleTimerVariable2 == SPELL_SPORE_BOMB + || my->particleTimerVariable2 == SPELL_MYCELIUM_BOMB ) + { + toDelete.push_back(entity); + } + else + { + freeSpot = false; + break; + } + } + } + } + } + for ( auto ent : toDelete ) + { + ent->removeLightField(); + list_RemoveNode(ent->mynode); + } + if ( freeSpot ) + { + Entity* fx = createFloorMagic(data.effectType, my->particleTimerCountdownSprite, data.x, data.y, 4.0, data.yaw, PARTICLE_LIFE); + fx->sizex = 8; + fx->sizey = 8; + fx->parent = my->getUID(); + floorMagicParticleSetUID(*fx, true); + } + } + } + } + } + else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_MAGIC_WAVE ) + { + Entity* parent = uidToEntity(my->parent); + if ( my->ticks == 1 ) + { + //createParticleCastingIndicator(my, my->x, my->y, my->z, PARTICLE_LIFE, 0); + } + + auto findEffects = particleTimerEffects.find(my->getUID()); + if ( findEffects != particleTimerEffects.end() ) + { + auto findEffect = findEffects->second.effectMap.find(my->ticks); + if ( findEffect != findEffects->second.effectMap.end() ) + { + auto& data = findEffect->second; + if ( data.effectType == ParticleTimerEffect_t::EFFECT_TEST_1 ) + { + createParticleErupt(data.x, data.y, 592); + } + else if ( data.effectType == ParticleTimerEffect_t::EFFECT_ICE_WAVE ) + { + Entity* fx = createFloorMagic(data.effectType, my->particleTimerCountdownSprite, data.x, data.y, 4.0, data.yaw, PARTICLE_LIFE + 3 * TICKS_PER_SECOND); + fx->sizex = 4; + fx->sizey = 4; + fx->parent = my->getUID(); + if ( my->actmagicSpellbookBonus > 0 ) + { + fx->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + } + fx->actmagicFromSpellbook = my->actmagicFromSpellbook; + if ( data.sfx ) + { + playSoundEntity(fx, data.sfx, 64); + } + + floorMagicParticleSetUID(*fx, true); + } + else if ( data.effectType == ParticleTimerEffect_t::EFFECT_DISRUPT_EARTH ) + { + int mapx = static_cast(data.x) >> 4; + int mapy = static_cast(data.y) >> 4; + + int mapIndex = (mapy)*MAPLAYERS + (mapx)*MAPLAYERS * map.height; + if ( mapx > 0 && mapy > 0 && mapx < map.width - 1 && mapy < map.height - 1 ) + { + if ( !map.tiles[OBSTACLELAYER + mapIndex] && map.tiles[mapIndex] + && !swimmingtiles[map.tiles[mapIndex] && !lavatiles[map.tiles[mapIndex]]] ) + { + Entity* fx = createFloorMagic(data.effectType, my->particleTimerCountdownSprite, data.x, data.y, 7.8, data.yaw, PARTICLE_LIFE + 3 * TICKS_PER_SECOND); + fx->parent = my->getUID(); + if ( my->actmagicSpellbookBonus > 0 ) + { + fx->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + } + fx->actmagicFromSpellbook = my->actmagicFromSpellbook; + fx->sizex = 6; + fx->sizey = 6; + Uint32 nextTickEffect = std::numeric_limits::max(); + bool foundNext = false; + for ( auto& eff : findEffects->second.effectMap ) + { + if ( eff.first > my->ticks ) + { + if ( eff.first < nextTickEffect ) + { + nextTickEffect = eff.first; + foundNext = true; + } + } + } + if ( foundNext ) + { + if ( findEffects->second.effectMap.find(nextTickEffect) != findEffects->second.effectMap.end() ) + { + auto& data = findEffects->second.effectMap[nextTickEffect]; + real_t tangent = atan2(fx->y - data.y, fx->x - data.x); + fx->monsterKnockbackTangentDir = tangent; + } + } + if ( data.sfx ) + { + playSoundEntity(fx, data.sfx, 64); + } + fx->actmagicNoParticle = 1; + floorMagicParticleSetUID(*fx, true); + } + } + } + else if ( data.effectType == ParticleTimerEffect_t::EFFECT_LIGHTNING_BOLT ) + { + Entity* fx = createFloorMagic(data.effectType, my->particleTimerCountdownSprite, data.x, data.y, -8.5, data.yaw, TICKS_PER_SECOND / 8); + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->ditheringDisabled = true; + fx->actmagicNoParticle = 1; + fx->parent = my->getUID(); + if ( my->actmagicSpellbookBonus > 0 ) + { + fx->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + } + fx->actmagicFromSpellbook = my->actmagicFromSpellbook; + if ( flickerLights ) + { + fx->light = addLight(fx->x / 16, fx->y / 16, "explosion"); + } + else + { + if ( !my->light ) + { + my->light = addLight(fx->x / 16, fx->y / 16, "explosion"); + } + } + if ( data.sfx ) + { + playSoundEntity(fx, data.sfx, 64); + } + if ( data.firstEffect ) + { + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, 0.0, TICKS_PER_SECOND, 32.0) ) + { + //fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + fx->scalex = 0.8; + fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + Uint32 color = makeColorRGB(252, 224, 0); + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; + } + } + } + /*if ( Entity* gib = spawnGib(fx) ) + { + gib->sprite = 670; + }*/ + + //spawnExplosion(fx->x, fx->y, 7.5); + + //createParticleErupt(my, 670); + //fx->flags[INVISIBLE] = true; + } + else if ( data.effectType == ParticleTimerEffect_t::EFFECT_TEST_3 ) + { + Entity* fx = createFloorMagic(data.effectType, my->particleTimerCountdownSprite, data.x, data.y, 7.5, my->yaw, PARTICLE_LIFE + 3 * TICKS_PER_SECOND); + if ( data.sfx ) + { + playSoundEntity(fx, data.sfx, 64); + } + fx->parent = my->getUID(); + fx->actmagicNoParticle = 1; + } + else if ( data.effectType == ParticleTimerEffect_t::EFFECT_ROOTS_SELF + || data.effectType == ParticleTimerEffect_t::EFFECT_ROOTS_SELF_SUSTAIN + || data.effectType == ParticleTimerEffect_t::EFFECT_ROOTS_TILE + || data.effectType == ParticleTimerEffect_t::EFFECT_ROOTS_PATH ) + { + Entity* fx = createFloorMagic(data.effectType, my->particleTimerCountdownSprite, data.x, data.y, 7.5, my->yaw, PARTICLE_LIFE); + if ( data.sfx ) + { + playSoundEntity(fx, data.sfx, 64); + } + fx->sizex = 6; + fx->sizey = 6; + fx->parent = my->getUID(); + fx->actmagicNoParticle = 1; + } + else if ( data.effectType == ParticleTimerEffect_t::EFFECT_TEST_2 ) + { + real_t offset = PI * (local_rng.rand() % 360) / 180.0;// -((my->ticks % 50) / 50.0) * 2 * PI; + int lifetime = PARTICLE_LIFE / 10; + createVortexMagic(my->particleTimerCountdownSprite, my->x, my->y, 7.5, offset + 0.0, lifetime + 1 * TICKS_PER_SECOND); + createVortexMagic(my->particleTimerCountdownSprite, my->x, my->y, 7.5, offset + 2 * PI / 3, lifetime + 1 * TICKS_PER_SECOND); + createVortexMagic(my->particleTimerCountdownSprite, my->x, my->y, 7.5, offset + 4 * PI / 3, lifetime + 1 * TICKS_PER_SECOND); + } + } + } + } + } + else + { + --my->particleTimerPreDelay; + } + } +} + +void actParticleSap(Entity* my) +{ + real_t decel = 0.9; + real_t accel = 0.9; + real_t z_accel = accel; + real_t z_decel = decel; + real_t minSpeed = 0.05; + + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + return; + } + else + { + if ( my->sprite == 977 ) // boomerang + { + if ( my->skill[3] == 1 ) + { + // specific for the animation I want... + // magic numbers that take approximately 75 frames (50% of travel time) to go outward or inward. + // acceleration is a little faster to overshoot into the right hand side. + decel = 0.9718; + accel = 0.9710; + z_decel = decel; + z_accel = z_decel; + } + else + { + decel = 0.95; + accel = 0.949; + z_decel = 0.9935; + z_accel = z_decel; + } + Entity* particle = spawnMagicParticleCustom(my, (local_rng.rand() % 2) ? 943 : 979, 1, 10); + if ( particle ) + { + particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); + particle->focalx = 2; + particle->focaly = -2; + particle->focalz = 2.5; + particle->ditheringDisabled = true; + } + if ( PARTICLE_LIFE < 100 && my->ticks % 6 == 0 ) + { + if ( PARTICLE_LIFE < 70 ) + { + playSoundEntityLocal(my, 434 + local_rng.rand() % 10, 64); + } + else + { + playSoundEntityLocal(my, 434 + local_rng.rand() % 10, 32); + } + } + //particle->flags[SPRITE] = true; + } + else + { + if ( *cvar_magic_fx_use_vismap && !intro ) + { + int x = my->x / 16.0; + int y = my->y / 16.0; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) + { + spawnMagicParticle(my); + break; + } + } + } + } + else + { + spawnMagicParticle(my); + } + } + Entity* parent = uidToEntity(my->parent); + if ( parent ) + { + my->x = parent->x + my->fskill[0]; + my->y = parent->y + my->fskill[1]; + } + else + { + list_RemoveNode(my->mynode); + return; + } + + if ( my->skill[1] == 0 ) + { + // move outwards diagonally. + if ( abs(my->vel_z) > minSpeed ) + { + my->fskill[0] += my->vel_x; + my->fskill[1] += my->vel_y; + my->vel_x *= decel; + my->vel_y *= decel; + + my->z += my->vel_z; + my->vel_z *= z_decel; + + my->yaw += my->fskill[2]; + my->pitch += my->fskill[3]; + } + else + { + my->skill[1] = 1; + my->vel_x *= -1; + my->vel_y *= -1; + my->vel_z *= -1; + } + } + else if ( my->skill[1] == 1 ) + { + // move inwards diagonally. + if ( (abs(my->vel_z) < 0.08 && my->skill[3] == 0) || (abs(my->vel_z) < 0.4 && my->skill[3] == 1) ) + { + my->fskill[0] += my->vel_x; + my->fskill[1] += my->vel_y; + my->vel_x /= accel; + my->vel_y /= accel; + + my->z += my->vel_z; + my->vel_z /= z_accel; + + my->yaw += my->fskill[2]; + my->pitch += my->fskill[3]; + } + else + { + // movement completed. + my->skill[1] = 2; + } + } + + my->scalex *= 0.99; + my->scaley *= 0.99; + my->scalez *= 0.99; + if ( my->sprite == 977 ) + { + my->scalex = 1.f; + my->scaley = 1.f; + my->scalez = 1.f; + my->roll -= 0.5; + my->pitch = std::max(my->pitch - 0.015, 0.0); + } + --PARTICLE_LIFE; + } +} + +void actParticleSapCenter(Entity* my) +{ + // init + if ( my->skill[3] == 0 ) + { + // for clients and server spawn the visible arcing particles. + my->skill[3] = 1; + createParticleSap(my); + } + + if ( multiplayer == CLIENT ) + { + return; + } + + Entity* parent = uidToEntity(my->parent); + if ( parent ) + { + // if reached the caster, delete self and spawn some particles. + if ( my->sprite == 977 && PARTICLE_LIFE > 1 ) + { + // store these in case parent dies. + // boomerang doesn't check for collision until end of life. + my->fskill[4] = parent->x; + my->fskill[5] = parent->y; + } + else if ( entityInsideEntity(my, parent) || (my->sprite == 977 && PARTICLE_LIFE == 0) ) + { + if ( my->skill[6] == SPELL_STEAL_WEAPON ) + { + if ( my->skill[7] == 1 ) + { + // found stolen item. + Item* item = newItemFromEntity(my); + if ( parent->behavior == &actPlayer ) + { + itemPickup(parent->skill[2], item); + } + else if ( parent->behavior == &actMonster ) + { + Stat* myStats = parent->getStats(); + if ( myStats && myStats->type == INCUBUS && myStats->getAttribute("special_npc") == "johann" ) + { + if ( dropItemMonster(item, parent, parent->getStats(), item->count) ) + { + parent->monsterSpecialState = INCUBUS_TELEPORT_STEAL; + parent->monsterSpecialTimer = 100 + local_rng.rand() % MONSTER_SPECIAL_COOLDOWN_INCUBUS_TELEPORT_RANDOM; + item = nullptr; + } + } + + if ( item ) + { + parent->addItemToMonsterInventory(item); + if ( myStats ) + { + node_t* weaponNode = itemNodeInInventory(myStats, -1, WEAPON); + if ( weaponNode ) + { + swapMonsterWeaponWithInventoryItem(parent, myStats, weaponNode, false, true); + if ( myStats->type == INCUBUS ) + { + parent->monsterSpecialState = INCUBUS_TELEPORT_STEAL; + parent->monsterSpecialTimer = 100 + local_rng.rand() % MONSTER_SPECIAL_COOLDOWN_INCUBUS_TELEPORT_RANDOM; + } + } + } + } + } + item = nullptr; + } + playSoundEntity(parent, 168, 128); + spawnMagicEffectParticles(parent->x, parent->y, parent->z, my->skill[5]); + } + else if ( my->skill[6] == SPELL_DRAIN_SOUL ) + { + parent->modHP(my->skill[7]); + parent->modMP(my->skill[8]); + if ( parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(2445)); + } + playSoundEntity(parent, 168, 128); + spawnMagicEffectParticles(parent->x, parent->y, parent->z, 169); + } + else if ( my->skill[6] == SHADOW_SPELLCAST ) + { + parent->shadowSpecialAbility(parent->monsterShadowInitialMimic); + playSoundEntity(parent, 166, 128); + spawnMagicEffectParticles(parent->x, parent->y, parent->z, my->skill[5]); + } + else if ( my->skill[6] == SPELL_SUMMON ) + { + parent->modMP(my->skill[7]); + /*if ( parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerColor(parent->skill[2], MESSAGE_STATUS, color, Language::get(774)); + }*/ + playSoundEntity(parent, 168, 128); + spawnMagicEffectParticles(parent->x, parent->y, parent->z, 169); + } + else if ( my->skill[6] == SPELL_FEAR ) + { + playSoundEntity(parent, 168, 128); + spawnMagicEffectParticles(parent->x, parent->y, parent->z, 174); + Entity* caster = uidToEntity(my->skill[7]); + if ( caster ) + { + spellEffectFear(nullptr, spellElement_fear, caster, parent, 0); + } + } + else if ( my->sprite == 977 ) // boomerang + { + Item* item = newItemFromEntity(my); + if ( parent->behavior == &actPlayer ) + { + item->ownerUid = parent->getUID(); + Item* pickedUp = itemPickup(parent->skill[2], item); + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerColor(parent->skill[2], MESSAGE_EQUIPMENT, color, Language::get(3746), items[item->type].getUnidentifiedName()); + achievementObserver.awardAchievementIfActive(parent->skill[2], parent, AchievementObserver::BARONY_ACH_IF_YOU_LOVE_SOMETHING); + if ( pickedUp ) + { + if ( parent->skill[2] == 0 || (parent->skill[2] > 0 && splitscreen) ) + { + // pickedUp is the new inventory stack for server, free the original items + free(item); + item = nullptr; + if ( multiplayer != CLIENT && !stats[parent->skill[2]]->weapon ) + { + useItem(pickedUp, parent->skill[2]); + } + auto& hotbar_t = players[parent->skill[2]]->hotbar; + if ( hotbar_t.magicBoomerangHotbarSlot >= 0 ) + { + auto& hotbar = hotbar_t.slots(); + hotbar[hotbar_t.magicBoomerangHotbarSlot].item = pickedUp->uid; + for ( int i = 0; i < NUM_HOTBAR_SLOTS; ++i ) + { + if ( i != hotbar_t.magicBoomerangHotbarSlot + && hotbar[i].item == pickedUp->uid ) + { + hotbar[i].item = 0; + hotbar[i].resetLastItem(); + } + } + } + } + else + { + free(pickedUp); // item is the picked up items (item == pickedUp) + } + } + } + else if ( parent->behavior == &actMonster ) + { + parent->addItemToMonsterInventory(item); + Stat *myStats = parent->getStats(); + if ( myStats ) + { + node_t* weaponNode = itemNodeInInventory(myStats, -1, WEAPON); + if ( weaponNode ) + { + swapMonsterWeaponWithInventoryItem(parent, myStats, weaponNode, false, true); + } + } + } + playSoundEntity(parent, 431 + local_rng.rand() % 3, 92); + item = nullptr; + } + list_RemoveNode(my->mynode); + return; + } + + // calculate direction to caster and move. + real_t tangent = atan2(parent->y - my->y, parent->x - my->x); + real_t dist = sqrt(pow(my->x - parent->x, 2) + pow(my->y - parent->y, 2)); + real_t speed = dist / std::max(PARTICLE_LIFE, 1); + my->vel_x = speed * cos(tangent); + my->vel_y = speed * sin(tangent); + my->x += my->vel_x; + my->y += my->vel_y; + } + else + { + if ( my->skill[6] == SPELL_SUMMON ) + { + real_t dist = sqrt(pow(my->x - my->skill[8], 2) + pow(my->y - my->skill[9], 2)); + if ( dist < 4 ) + { + spawnMagicEffectParticles(my->skill[8], my->skill[9], 0, my->skill[5]); + Entity* caster = uidToEntity(my->skill[7]); + if ( caster && caster->behavior == &actPlayer && stats[caster->skill[2]] ) + { + // kill old summons. + for ( node_t* node = stats[caster->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + { + Entity* follower = nullptr; + if ( (Uint32*)(node)->element ) + { + follower = uidToEntity(*((Uint32*)(node)->element)); + } + if ( follower && follower->monsterAllySummonRank != 0 ) + { + Stat* followerStats = follower->getStats(); + if ( followerStats && followerStats->HP > 0 ) + { + follower->setMP(followerStats->MAXMP * (followerStats->HP / static_cast(followerStats->MAXHP))); + follower->setHP(0); + } + } + } + + Monster creature = SKELETON; + Entity* monster = summonMonster(creature, my->skill[8], my->skill[9]); + if ( monster ) + { + Stat* monsterStats = monster->getStats(); + monster->yaw = my->yaw - PI; + if ( monsterStats ) + { + int magicLevel = 1; + magicLevel = std::min(7, 1 + (stats[caster->skill[2]]->playerSummonLVLHP >> 16) / 5); + + monster->monsterAllySummonRank = magicLevel; + monsterStats->setAttribute("special_npc", "skeleton knight"); + strcpy(monsterStats->name, MonsterData_t::getSpecialNPCName(*monsterStats).c_str()); + forceFollower(*caster, *monster); + + monster->setEffect(EFF_STUNNED, true, 20, false); + bool spawnSecondAlly = false; + if ( auto spell = getSpellFromID(SPELL_SUMMON) ) + { + if ( (caster->getINT() + stats[caster->skill[2]]->getModifiedProficiency(spell->skillID)) >= SKILL_LEVEL_EXPERT ) + { + spawnSecondAlly = true; + } + } + //parent->increaseSkill(PRO_LEADERSHIP); + monster->monsterAllyIndex = caster->skill[2]; + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. + } + + // change the color of the hit entity. + monster->flags[USERFLAG2] = true; + serverUpdateEntityFlag(monster, USERFLAG2); + if ( monsterChangesColorWhenAlly(monsterStats) ) + { + int bodypart = 0; + for ( node_t* node = (monster)->children.first; node != nullptr; node = node->next ) + { + if ( bodypart >= LIMB_HUMANOID_TORSO ) + { + Entity* tmp = (Entity*)node->element; + if ( tmp ) + { + tmp->flags[USERFLAG2] = true; + serverUpdateEntityFlag(tmp, USERFLAG2); + } + } + ++bodypart; + } + } + + if ( spawnSecondAlly ) + { + Entity* monster = summonMonster(creature, my->skill[8], my->skill[9]); + if ( monster ) + { + if ( multiplayer != CLIENT ) + { + spawnExplosion(monster->x, monster->y, -1); + playSoundEntity(monster, 171, 128); + + createParticleErupt(monster, 791); + serverSpawnMiscParticles(monster, PARTICLE_EFFECT_ERUPT, 791); + } + + Stat* monsterStats = monster->getStats(); + monster->yaw = my->yaw - PI; + if ( monsterStats ) + { + monsterStats->setAttribute("special_npc", "skeleton sentinel"); + strcpy(monsterStats->name, MonsterData_t::getSpecialNPCName(*monsterStats).c_str()); + magicLevel = 1; + if ( stats[caster->skill[2]] ) + { + magicLevel = std::min(7, 1 + (stats[caster->skill[2]]->playerSummon2LVLHP >> 16) / 5); + } + monster->monsterAllySummonRank = magicLevel; + + forceFollower(*caster, *monster); + monster->setEffect(EFF_STUNNED, true, 20, false); + + monster->monsterAllyIndex = caster->skill[2]; + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. + } + + if ( caster && caster->behavior == &actPlayer ) + { + steamAchievementClient(caster->skill[2], "BARONY_ACH_SKELETON_CREW"); + } + + // change the color of the hit entity. + monster->flags[USERFLAG2] = true; + serverUpdateEntityFlag(monster, USERFLAG2); + if ( monsterChangesColorWhenAlly(monsterStats) ) + { + int bodypart = 0; + for ( node_t* node = (monster)->children.first; node != nullptr; node = node->next ) + { + if ( bodypart >= LIMB_HUMANOID_TORSO ) + { + Entity* tmp = (Entity*)node->element; + if ( tmp ) + { + tmp->flags[USERFLAG2] = true; + serverUpdateEntityFlag(tmp, USERFLAG2); + } + } + ++bodypart; + } + } + } + } + } + } + } + } + list_RemoveNode(my->mynode); + return; + } + + // calculate direction to caster and move. + real_t tangent = atan2(my->skill[9] - my->y, my->skill[8] - my->x); + real_t speed = dist / PARTICLE_LIFE; + my->vel_x = speed * cos(tangent); + my->vel_y = speed * sin(tangent); + my->x += my->vel_x; + my->y += my->vel_y; + } + else if ( my->skill[6] == SPELL_STEAL_WEAPON ) + { + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. + entity->flags[INVISIBLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->x = my->x; + entity->y = my->y; + entity->sizex = 4; + entity->sizey = 4; + entity->yaw = my->yaw; + entity->vel_x = (local_rng.rand() % 20 - 10) / 10.0; + entity->vel_y = (local_rng.rand() % 20 - 10) / 10.0; + entity->vel_z = -.5; + entity->flags[PASSABLE] = true; + entity->flags[USERFLAG1] = true; // speeds up game when many items are dropped + entity->behavior = &actItem; + entity->skill[10] = my->skill[10]; + entity->skill[11] = my->skill[11]; + entity->skill[12] = my->skill[12]; + entity->skill[13] = my->skill[13]; + entity->skill[14] = my->skill[14]; + entity->skill[15] = my->skill[15]; + entity->itemOriginalOwner = my->itemOriginalOwner; + entity->parent = 0; + + // no parent, no target to travel to. + list_RemoveNode(my->mynode); + return; + } + else if ( my->sprite == 977 ) + { + // calculate direction to caster and move. + real_t tangent = atan2(my->fskill[5] - my->y, my->fskill[4] - my->x); + real_t dist = sqrt(pow(my->x - my->fskill[4], 2) + pow(my->y - my->fskill[5], 2)); + real_t speed = dist / std::max(PARTICLE_LIFE, 1); + + if ( dist < 4 || (abs(my->fskill[5]) < 0.001 && abs(my->fskill[4]) < 0.001) ) + { + // reached goal, or goal not set then spawn the item. + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. + entity->flags[INVISIBLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->x = my->x; + entity->y = my->y; + entity->sizex = 4; + entity->sizey = 4; + entity->yaw = my->yaw; + entity->vel_x = (local_rng.rand() % 20 - 10) / 10.0; + entity->vel_y = (local_rng.rand() % 20 - 10) / 10.0; + entity->vel_z = -.5; + entity->flags[PASSABLE] = true; + entity->flags[USERFLAG1] = true; // speeds up game when many items are dropped + entity->behavior = &actItem; + entity->skill[10] = my->skill[10]; + entity->skill[11] = my->skill[11]; + entity->skill[12] = my->skill[12]; + entity->skill[13] = my->skill[13]; + entity->skill[14] = my->skill[14]; + entity->skill[15] = my->skill[15]; + entity->itemOriginalOwner = 0; + entity->parent = 0; + + list_RemoveNode(my->mynode); return; } + my->vel_x = speed * cos(tangent); + my->vel_y = speed * sin(tangent); + my->x += my->vel_x; + my->y += my->vel_y; + } + else + { + // no parent, no target to travel to. + list_RemoveNode(my->mynode); + return; + } + } + + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + return; + } + else + { + --PARTICLE_LIFE; + } +} + +void createParticleExplosionCharge(Entity* parent, int sprite, int particleCount, double scale) +{ + if ( !parent ) + { + return; + } + + for ( int c = 0; c < particleCount; c++ ) + { + // shoot drops to the sky + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = parent->x - 3 + local_rng.rand() % 7; + entity->y = parent->y - 3 + local_rng.rand() % 7; + entity->z = 0 + local_rng.rand() % 190; + if ( parent && parent->behavior == &actPlayer ) + { + entity->z /= 2; + } + entity->vel_z = -1; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->particleDuration = entity->z + 10; + /*if ( local_rng.rand() % 5 > 0 ) + { + entity->vel_x = 0.5*cos(entity->yaw); + entity->vel_y = 0.5*sin(entity->yaw); + entity->particleDuration = 6; + entity->z = 0; + entity->vel_z = 0.5 *(-1 + local_rng.rand() % 3); + }*/ + entity->scalex *= scale; + entity->scaley *= scale; + entity->scalez *= scale; + entity->behavior = &actParticleExplosionCharge; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->parent = parent->getUID(); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } + + int radius = STRIKERANGE * 2 / 3; + real_t arc = PI / 16; + int randScale = 1; + for ( int c = 0; c < 128; c++ ) + { + // shoot drops to the sky + Entity* entity = newEntity(670, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = 0 + c * arc; + + entity->x = parent->x + (radius * cos(entity->yaw));// - 2 + local_rng.rand() % 5; + entity->y = parent->y + (radius * sin(entity->yaw));// - 2 + local_rng.rand() % 5; + entity->z = radius + 150; + entity->particleDuration = entity->z + local_rng.rand() % 3; + entity->vel_z = -1; + if ( parent && parent->behavior == &actPlayer ) + { + entity->z /= 2; + } + randScale = 1 + local_rng.rand() % 3; + + entity->scalex *= (scale / randScale); + entity->scaley *= (scale / randScale); + entity->scalez *= (scale / randScale); + entity->behavior = &actParticleExplosionCharge; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->parent = parent->getUID(); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + if ( c > 0 && c % 16 == 0 ) + { + radius -= 2; + } + } +} + +void actParticleExplosionCharge(Entity* my) +{ + if ( PARTICLE_LIFE < 0 || (my->z < -4 && local_rng.rand() % 4 == 0) || (ticks % 14 == 0 && uidToEntity(my->parent) == nullptr) ) + { + list_RemoveNode(my->mynode); + } + else + { + --PARTICLE_LIFE; + my->yaw += 0.1; + my->x += my->vel_x; + my->y += my->vel_y; + my->z += my->vel_z; + my->scalex /= 0.99; + my->scaley /= 0.99; + my->scalez /= 0.99; + //my->z -= 0.01; + } + return; +} + +bool Entity::magicFallingCollision() +{ + hit.entity = nullptr; + real_t zLimitHigh = -5; + if ( sprite == 2209 || sprite == 233 ) // meteor + { + zLimitHigh = -5; + } + if ( z <= zLimitHigh || fabs(vel_z) < 0.01 ) + { + // check if particle stopped or too high. + return false; + } + + if ( z >= 7.5 ) + { + return true; + } + + if ( actmagicIsVertical == MAGIC_ISVERTICAL_Z ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 1); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + node_t* node; + for ( node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity ) + { + if ( entity == this ) + { + continue; + } + if ( entityInsideEntity(this, entity) && !entity->flags[PASSABLE] && (entity->getUID() != this->parent) ) + { + if ( collisionProjectileMiss(uidToEntity(this->parent), this) ) + { + continue; + } + hit.entity = entity; + //hit.side = HORIZONTAL; + return true; + } + } + } + } + } + + return false; +} + +bool Entity::magicOrbitingCollision() +{ + hit.entity = nullptr; + + if ( this->actmagicIsOrbiting == 2 ) + { + if ( this->ticks == 5 && this->actmagicOrbitHitTargetUID4 != 0 ) + { + // hit this target automatically + Entity* tmp = uidToEntity(actmagicOrbitHitTargetUID4); + if ( tmp ) + { + hit.entity = tmp; + return true; + } + } + if ( this->z < -8 || this->z > 3 ) + { + return false; + } + else if ( this->ticks >= 12 && this->ticks % 4 != 0 ) // check once every 4 ticks, after the missile is alive for a bit + { + return false; + } + } + else if ( this->z < -10 ) + { + return false; + } + + if ( this->actmagicIsOrbiting == 2 ) + { + if ( this->actmagicOrbitStationaryHitTarget >= 3 ) + { + return false; + } + } + + Entity* caster = uidToEntity(parent); + + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 1); + + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + node_t* node; + for ( node = currentList->first; node != NULL; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity == this ) + { + continue; + } + if ( entity->behavior != &actMonster + && entity->behavior != &actPlayer + && entity->behavior != &actDoor + && entity->behavior != &::actIronDoor + && !(entity->isDamageableCollider() && entity->isColliderDamageableByMagic()) + && !entity->isInertMimic() + && entity->behavior != &::actChest + && entity->behavior != &::actFurniture ) + { + continue; + } + if ( caster && !(svFlags & SV_FLAG_FRIENDLYFIRE) && caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + if ( actmagicIsOrbiting == 2 ) + { + if ( static_cast(actmagicOrbitHitTargetUID1) == entity->getUID() + || static_cast(actmagicOrbitHitTargetUID2) == entity->getUID() + || static_cast(actmagicOrbitHitTargetUID3) == entity->getUID() + || static_cast(actmagicOrbitHitTargetUID4) == entity->getUID() ) + { + // we already hit these guys. + continue; + } + } + if ( entityInsideEntity(this, entity) && !entity->flags[PASSABLE] && (entity->getUID() != this->parent) ) + { + hit.entity = entity; + if ( hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer ) + { + if ( actmagicIsOrbiting == 2 ) + { + if ( actmagicOrbitHitTargetUID4 != 0 && caster && caster->behavior == &actPlayer ) + { + if ( actmagicOrbitHitTargetUID1 == 0 + && actmagicOrbitHitTargetUID2 == 0 + && actmagicOrbitHitTargetUID3 == 0 + && hit.entity->behavior == &actMonster ) + { + steamStatisticUpdateClient(caster->skill[2], STEAM_STAT_VOLATILE, STEAM_STAT_INT, 1); + } + } + ++actmagicOrbitStationaryHitTarget; + if ( actmagicOrbitHitTargetUID1 == 0 ) + { + actmagicOrbitHitTargetUID1 = entity->getUID(); + } + else if ( actmagicOrbitHitTargetUID2 == 0 ) + { + actmagicOrbitHitTargetUID2 = entity->getUID(); + } + else if ( actmagicOrbitHitTargetUID3 == 0 ) + { + actmagicOrbitHitTargetUID3 = entity->getUID(); + } + } + } + return true; + } + } + } + + return false; +} + +void Entity::castFallingMagicMissile(int spellID, real_t distFromCaster, real_t angleFromCasterDirection, int heightDelay) +{ + spell_t* spell = getSpellFromID(spellID); + Entity* entity = castSpell(getUID(), spell, false, true); + if ( entity ) + { + entity->x = x + distFromCaster * cos(yaw + angleFromCasterDirection); + entity->y = y + distFromCaster * sin(yaw + angleFromCasterDirection); + entity->z = -25 - heightDelay; + double missile_speed = 4; + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->vel_z = 0.5 * (missile_speed); + entity->pitch = PI / 2; + entity->actmagicIsVertical = MAGIC_ISVERTICAL_Z; + spawnMagicEffectParticles(entity->x, entity->y, 0, 174); + playSoundEntity(entity, spellGetCastSound(spell), 128); + } +} + +Entity* Entity::castOrbitingMagicMissile(int spellID, real_t distFromCaster, real_t angleFromCasterDirection, int duration) +{ + spell_t* spell = getSpellFromID(spellID); + Entity* entity = castSpell(getUID(), spell, false, true); + if ( entity ) + { + if ( spellID == SPELL_FIREBALL ) + { + entity->sprite = 671; + } + else if ( spellID == SPELL_MAGICMISSILE ) + { + entity->sprite = 679; + } + entity->yaw = angleFromCasterDirection; + entity->x = x + distFromCaster * cos(yaw + entity->yaw); + entity->y = y + distFromCaster * sin(yaw + entity->yaw); + entity->z = -2.5; + double missile_speed = 4; + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->actmagicIsOrbiting = 1; + entity->actmagicOrbitDist = distFromCaster; + entity->actmagicOrbitStartZ = entity->z; + entity->z += 4 * sin(angleFromCasterDirection); + entity->roll += (PI / 8) * (1 - abs(sin(angleFromCasterDirection))); + entity->actmagicOrbitVerticalSpeed = 0.1; + entity->actmagicOrbitVerticalDirection = 1; + entity->actmagicOrbitLifetime = duration; + entity->vel_z = entity->actmagicOrbitVerticalSpeed; + playSoundEntity(entity, spellGetCastSound(spell), 128); + //spawnMagicEffectParticles(entity->x, entity->y, 0, 174); + } + return entity; +} + +Entity* castStationaryOrbitingMagicMissile(Entity* parent, int spellID, real_t centerx, real_t centery, + real_t distFromCenter, real_t angleFromCenterDirection, int duration) +{ + spell_t* spell = getSpellFromID(spellID); + if ( !parent ) + { + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = centerx; + entity->y = centery; + entity->z = 15; + entity->vel_z = 0; + //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = 100; + entity->skill[1] = 10; + entity->behavior = &actParticleDot; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[INVISIBLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + parent = entity; + } + Stat* stats = parent->getStats(); + Uint8 amplify = 0; + if ( stats ) + { + amplify = stats->getEffectActive(EFF_MAGICAMPLIFY); + stats->clearEffect(EFF_MAGICAMPLIFY); // temporary skip amplify effects otherwise recursion. + } + Entity* entity = castSpell(parent->getUID(), spell, false, true); + if ( stats ) + { + stats->setEffectValueUnsafe(EFF_MAGICAMPLIFY, amplify); + } + if ( entity ) + { + if ( spellID == SPELL_FIREBALL ) + { + entity->sprite = 671; + } + else if ( spellID == SPELL_COLD ) + { + entity->sprite = 797; + } + else if ( spellID == SPELL_LIGHTNING ) + { + entity->sprite = 798; + } + else if ( spellID == SPELL_MAGICMISSILE ) + { + entity->sprite = 679; + } + entity->yaw = angleFromCenterDirection; + entity->x = centerx; + entity->y = centery; + entity->z = 4; + double missile_speed = 4; + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->actmagicIsOrbiting = 2; + entity->actmagicOrbitDist = distFromCenter; + entity->actmagicOrbitStationaryCurrentDist = 0.0; + entity->actmagicOrbitStartZ = entity->z; + //entity->roll -= (PI / 8); + entity->actmagicOrbitVerticalSpeed = -0.3; + entity->actmagicOrbitVerticalDirection = 1; + entity->actmagicOrbitLifetime = duration; + entity->actmagicOrbitStationaryX = centerx; + entity->actmagicOrbitStationaryY = centery; + entity->vel_z = -0.1; + playSoundEntity(entity, spellGetCastSound(spell), 128); + + //spawnMagicEffectParticles(entity->x, entity->y, 0, 174); + } + return entity; +} + +void createParticleFollowerCommand(real_t x, real_t y, real_t z, int sprite, Uint32 uid) +{ + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + //entity->sizex = 1; + //entity->sizey = 1; + entity->x = x; + entity->y = y; + entity->z = 0; + entity->vel_z = -0.8; + //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = 50; + entity->skill[1] = 0; + entity->parent = uid; + entity->fskill[1] = entity->z; // start z + entity->behavior = &actParticleFollowerCommand; + entity->scalex = 0.8; + entity->scaley = 0.8; + entity->scalez = 0.8; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + + // boosty boost + for ( int c = 0; c < 10; c++ ) + { + entity = newEntity(174, 1, map.entities, nullptr); //Particle entity. + entity->x = x - 4 + local_rng.rand() % 9; + entity->y = y - 4 + local_rng.rand() % 9; + entity->z = z - 0 + local_rng.rand() % 11; + entity->scalex = 0.5; + entity->scaley = 0.5; + entity->scalez = 0.5; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = (local_rng.rand() % 360) * PI / 180.f; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->behavior = &actMagicParticle; + entity->vel_z = -1; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } + +} + +static ConsoleVariable cvar_follower_particle_speed("/follower_particle_speed", 2.0); +void actParticleFollowerCommand(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + return; + } + else + { + --PARTICLE_LIFE; + if ( my->parent != 0 ) + { + if ( Entity* parent = uidToEntity(my->parent) ) + { + my->x = parent->x; + my->y = parent->y; + } + } + my->z += my->vel_z; + my->yaw += (std::min(my->vel_z * 2, -0.1)) / *cvar_follower_particle_speed; + if ( my->z < (my->fskill[1] - 3) ) + { + my->skill[1] = 1; + my->vel_z *= 0.9; + } + if ( my->skill[1] != 0 && abs(my->vel_z) < 0.1 ) + { + my->z += .1 * sin(my->fskill[0]); + my->fskill[0] += 1.0 * PI / 32; + /*if ( my->fskill[0] > PI / 2 ) + { + my->scalex *= .9; + my->scaley *= .9; + my->scalez *= .9; + }*/ + } + } +} + +void actParticleShadowTag(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + // once off, fire some erupt dot particles at end of life. + real_t yaw = 0; + int numParticles = 8; + for ( int c = 0; c < 8; c++ ) + { + Entity* entity = newEntity(871, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = my->x; + entity->y = my->y; + entity->z = -10 + my->fskill[0]; + entity->yaw = yaw; + entity->vel_x = 0.2; + entity->vel_y = 0.2; + entity->vel_z = -0.02; + entity->skill[0] = 100; + entity->skill[1] = 0; // direction. + entity->fskill[0] = 0.1; + entity->behavior = &actParticleErupt; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + yaw += 2 * PI / numParticles; + } - if ( my->actmagicSpray == 1 ) + if ( multiplayer != CLIENT ) + { + Uint32 casterUid = static_cast(my->skill[2]); + Entity* caster = uidToEntity(casterUid); + Entity* parent = uidToEntity(my->parent); + if ( caster && caster->behavior == &actPlayer + && parent ) { - my->vel_x = my->vel_x * .95; - my->vel_y = my->vel_y * .95; + // caster is alive, notify they lost their mark + Uint32 color = makeColorRGB(255, 255, 255); + if ( parent->getStats() ) + { + messagePlayerMonsterEvent(caster->skill[2], color, *(parent->getStats()), Language::get(3466), Language::get(3467), MSG_COMBAT); + parent->setEffect(EFF_SHADOW_TAGGED, false, 0, true); + } } } + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else + { + --PARTICLE_LIFE; + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, true)); - //Go down two levels to the next element. This will need to get re-written shortly. - node = spell->elements.first; - element = (spellElement_t*)node->element; - //element = (spellElement_t *)spell->elements->first->element; - //element = (spellElement_t *)element->elements->first->element; //Go down two levels to the second element. - node = element->elements.first; - element = (spellElement_t*)node->element; - //if (!strcmp(element->element_internal_name, spellElement_fire.element_internal_name) - // || !strcmp(element->element_internal_name, spellElement_lightning.element_internal_name)) - if (1) + Entity* parent = uidToEntity(my->parent); + if ( parent ) { - //Make the ball light up stuff as it travels. - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, false)); + my->x = parent->x; + my->y = parent->y; + } - if ( flickerLights ) + if ( my->skill[1] >= 50 ) // stop changing size + { + real_t maxspeed = .03; + real_t acceleration = 0.95; + if ( my->skill[3] == 0 ) { - //Magic light ball will never flicker if this setting is disabled. - lightball_flicker++; + // once off, store the normal height of the particle. + my->skill[3] = 1; + my->vel_z = -maxspeed; } - my->skill[2] = -11; // so clients know to create a light field - - if (lightball_flicker > 5) + if ( my->skill[1] % 5 == 0 ) { - lightball_lighting = (lightball_lighting == 1) + 1; - - if (lightball_lighting == 1) + Uint32 casterUid = static_cast(my->skill[2]); + Entity* caster = uidToEntity(casterUid); + if ( caster && caster->creatureShadowTaggedThisUid == my->parent && parent ) { - my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, false)); + // caster is alive, and they have still marked the parent this particle is following. } else { - my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, true)); + PARTICLE_LIFE = 0; } - lightball_flicker = 0; } - } - else - { - my->skill[2] = -12; // so clients know to simply spawn particles - } - // spawn particles - if ( my->actmagicSpray == 1 ) - { - } - else if ( *cvar_magic_fx_use_vismap && !intro ) - { - int x = my->x / 16.0; - int y = my->y / 16.0; - if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + if ( PARTICLE_LIFE > 0 && PARTICLE_LIFE < TICKS_PER_SECOND ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( parent && parent->getStats() && parent->getStats()->getEffectActive(EFF_SHADOW_TAGGED) ) { - if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) - { - spawnMagicParticle(my); - break; - } + ++PARTICLE_LIFE; } } + // bob up and down movement. + if ( my->skill[3] == 1 ) + { + my->vel_z *= acceleration; + if ( my->vel_z > -0.005 ) + { + my->skill[3] = 2; + my->vel_z = -0.005; + } + my->z += my->vel_z; + } + else if ( my->skill[3] == 2 ) + { + my->vel_z /= acceleration; + if ( my->vel_z < -maxspeed ) + { + my->skill[3] = 3; + my->vel_z = -maxspeed; + } + my->z -= my->vel_z; + } + else if ( my->skill[3] == 3 ) + { + my->vel_z *= acceleration; + if ( my->vel_z > -0.005 ) + { + my->skill[3] = 4; + my->vel_z = -0.005; + } + my->z -= my->vel_z; + } + else if ( my->skill[3] == 4 ) + { + my->vel_z /= acceleration; + if ( my->vel_z < -maxspeed ) + { + my->skill[3] = 1; + my->vel_z = -maxspeed; + } + my->z += my->vel_z; + } + my->yaw += 0.01; } else { - spawnMagicParticle(my); - } - } - else - { - //Any init stuff that needs to happen goes here. - magic_init = 1; - my->skill[2] = -7; // ordinarily the client won't do anything with this entity - if ( my->actmagicIsOrbiting == 1 || my->actmagicIsOrbiting == 2 ) - { - MAGIC_MAXLIFE = my->actmagicOrbitLifetime; - } - else if ( my->actmagicIsVertical != MAGIC_ISVERTICAL_NONE ) - { - MAGIC_MAXLIFE = 512; - } - } -} - -void actMagicClient(Entity* my) -{ - my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, false)); - - if ( flickerLights ) - { - //Magic light ball will never flicker if this setting is disabled. - lightball_flicker++; - } - my->skill[2] = -11; // so clients know to create a light field - - if (lightball_flicker > 5) - { - lightball_lighting = (lightball_lighting == 1) + 1; - - if (lightball_lighting == 1) - { - my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, false)); - } - else - { - my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, true)); - } - lightball_flicker = 0; - } - - // spawn particles - if ( *cvar_magic_fx_use_vismap && !intro ) - { - int x = my->x / 16.0; - int y = my->y / 16.0; - if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) - { - for ( int i = 0; i < MAXPLAYERS; ++i ) + my->z += my->vel_z; + my->yaw += my->vel_z * 2; + if ( my->scalex < 0.5 ) { - if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) - { - spawnMagicParticle(my); - break; - } + my->scalex += 0.02; + } + else + { + my->scalex = 0.5; + } + my->scaley = my->scalex; + my->scalez = my->scalex; + if ( my->z < -3 + my->fskill[0] ) + { + my->vel_z *= 0.9; } } - } - else - { - spawnMagicParticle(my); - } -} -void actMagicClientNoLight(Entity* my) -{ - // simply spawn particles - if ( *cvar_magic_fx_use_vismap && !intro ) - { - int x = my->x / 16.0; - int y = my->y / 16.0; - if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + // once off, fire some erupt dot particles at start. + if ( my->skill[1] == 0 ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + real_t yaw = 0; + int numParticles = 8; + for ( int c = 0; c < 8; c++ ) { - if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) + Entity* entity = newEntity(871, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = my->x; + entity->y = my->y; + entity->z = -10 + my->fskill[0]; + entity->yaw = yaw; + entity->vel_x = 0.2; + entity->vel_y = 0.2; + entity->vel_z = -0.02; + entity->skill[0] = 100; + entity->skill[1] = 0; // direction. + entity->fskill[0] = 0.1; + entity->behavior = &actParticleErupt; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) { - spawnMagicParticle(my); - break; + entity_uids--; } + entity->setUID(-3); + yaw += 2 * PI / numParticles; } } - } - else - { - spawnMagicParticle(my); - } -} - -void actMagicParticle(Entity* my) -{ - my->x += my->vel_x; - my->y += my->vel_y; - my->z += my->vel_z; - if ( my->sprite == 943 || my->sprite == 979 ) - { - my->scalex -= 0.05; - my->scaley -= 0.05; - my->scalez -= 0.05; - } - - if ( my->sprite == 1479 ) - { - my->scalex -= 0.025; - my->scaley -= 0.025; - my->scalez -= 0.025; - my->pitch += 0.25; - my->yaw += 0.25; - //if ( my->parent == 0 && local_rng.rand() % 10 == 0 ) - //{ - // if ( Entity* particle = spawnMagicParticle(my) ) - // { - // particle->parent = my->getUID(); - // particle->x = my->x; - // particle->y = my->y; - // //particle->z = my->z; - // } - //} - } - else - { - my->scalex -= 0.05; - my->scaley -= 0.05; - my->scalez -= 0.05; - } - if ( my->scalex <= 0 ) - { - my->scalex = 0; - my->scaley = 0; - my->scalez = 0; - list_RemoveNode(my->mynode); - return; - } -} - -static ConsoleVariable cvar_magic_fx_light_bonus("/magic_fx_light_bonus", 0.25f); - -void actHUDMagicParticle(Entity* my) -{ - my->x += my->vel_x; - my->y += my->vel_y; - my->z += my->vel_z; - my->scalex -= 0.05; - my->scaley -= 0.05; - my->scalez -= 0.05; - if ( my->scalex <= 0 ) - { - my->scalex = 0; - my->scaley = 0; - my->scalez = 0; - list_RemoveNode(my->mynode); - return; + ++my->skill[1]; } } -void actHUDMagicParticleCircling(Entity* my) +void createParticleShadowTag(Entity* parent, Uint32 casterUid, int duration) { - int turnRate = 4; - my->yaw += 0.2; - turnRate = 4; - my->x = my->actmagicOrbitStationaryX + my->actmagicOrbitStationaryCurrentDist * cos(my->yaw); - my->y = my->actmagicOrbitStationaryY + my->actmagicOrbitStationaryCurrentDist * sin(my->yaw); - my->actmagicOrbitStationaryCurrentDist = - std::min(my->actmagicOrbitStationaryCurrentDist + 0.5, static_cast(my->actmagicOrbitDist)); - my->z += my->vel_z * my->actmagicOrbitVerticalDirection; - - my->vel_z = std::min(my->actmagicOrbitVerticalSpeed, my->vel_z / 0.95); - my->roll += (PI / 8) / (turnRate / my->vel_z) * my->actmagicOrbitVerticalDirection; - my->roll = std::max(my->roll, -PI / 4); - - --my->actmagicOrbitLifetime; - if ( my->actmagicOrbitLifetime <= 0 ) + if ( !parent ) { - list_RemoveNode(my->mynode); return; } - - { - Entity* entity; - - entity = newEntity(my->sprite, 1, map.entities, nullptr); //Particle entity. - - entity->x = my->x + (local_rng.rand() % 50 - 25) / 200.f; - entity->y = my->y + (local_rng.rand() % 50 - 25) / 200.f; - entity->z = my->z + (local_rng.rand() % 50 - 25) / 200.f; - entity->scalex = 0.7; - entity->scaley = 0.7; - entity->scalez = 0.7; - entity->sizex = 1; - entity->sizey = 1; - entity->yaw = my->yaw; - entity->pitch = my->pitch; - entity->roll = my->roll; - entity->flags[NOUPDATE] = true; - entity->flags[PASSABLE] = true; - entity->flags[UNCLICKABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UPDATENEEDED] = false; - entity->flags[OVERDRAW] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->behavior = &actHUDMagicParticle; - entity->skill[11] = my->skill[11]; - if ( multiplayer != CLIENT ) - { - entity_uids--; - } - entity->setUID(-3); - } -} - -Entity* spawnMagicParticle(Entity* parentent) -{ - if ( !parentent ) - { - return nullptr; - } - Entity* entity; - - entity = newEntity(parentent->sprite, 1, map.entities, nullptr); //Particle entity. - - entity->x = parentent->x + (local_rng.rand() % 50 - 25) / 20.f; - entity->y = parentent->y + (local_rng.rand() % 50 - 25) / 20.f; - entity->z = parentent->z + (local_rng.rand() % 50 - 25) / 20.f; - entity->parent = 0; - entity->scalex = 0.7; - entity->scaley = 0.7; - entity->scalez = 0.7; - entity->sizex = 1; - entity->sizey = 1; - entity->yaw = parentent->yaw; - entity->pitch = parentent->pitch; - entity->roll = parentent->roll; - entity->flags[NOUPDATE] = true; + Entity* entity = newEntity(870, 1, map.entities, nullptr); //Particle entity. + entity->parent = parent->getUID(); + entity->x = parent->x; + entity->y = parent->y; + entity->z = 7.5; + entity->fskill[0] = parent->z; + entity->vel_z = -0.8; + entity->scalex = 0.1; + entity->scaley = 0.1; + entity->scalez = 0.1; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = duration; + entity->skill[1] = 0; + entity->skill[2] = static_cast(casterUid); + entity->skill[3] = 0; + entity->behavior = &actParticleShadowTag; entity->flags[PASSABLE] = true; - entity->flags[UNCLICKABLE] = true; entity->flags[NOUPDATE] = true; - entity->flags[UPDATENEEDED] = false; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->behavior = &actMagicParticle; + entity->flags[UNCLICKABLE] = true; + /*entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f);*/ if ( multiplayer != CLIENT ) { entity_uids--; } entity->setUID(-3); - - return entity; } -Entity* spawnMagicParticleCustom(Entity* parentent, int sprite, real_t scale, real_t spreadReduce) +void actParticlePinpointTarget(Entity* my) { - if ( !parentent ) + Sint32& spellID = my->skill[4]; + + if ( PARTICLE_LIFE < 0 ) { - return nullptr; - } - Entity* entity; + // once off, fire some erupt dot particles at end of life. + real_t yaw = 0; - entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + for ( int i = 0; i < 4; ++i ) + { + Entity* fx = spawnMagicParticle(my); + fx->vel_x = 0.5 * cos(my->yaw + i * PI / 2); + fx->vel_y = 0.5 * sin(my->yaw + i * PI / 2); + fx->fskill[1] = 0.05; + fx->yaw = my->yaw + i * PI / 2; + fx->scalex = 0.7; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + } - int size = 50 / spreadReduce; - entity->x = parentent->x + (local_rng.rand() % size - size / 2) / 20.f; - entity->y = parentent->y + (local_rng.rand() % size - size / 2) / 20.f; - entity->z = parentent->z + (local_rng.rand() % size - size / 2) / 20.f; - entity->scalex = scale; - entity->scaley = scale; - entity->scalez = scale; - entity->sizex = 1; - entity->sizey = 1; - entity->yaw = parentent->yaw; - entity->pitch = parentent->pitch; - entity->roll = parentent->roll; - entity->flags[NOUPDATE] = true; - entity->flags[PASSABLE] = true; - entity->flags[UNCLICKABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UPDATENEEDED] = false; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->behavior = &actMagicParticle; - if ( multiplayer != CLIENT ) - { - entity_uids--; - } - entity->setUID(-3); + /*Entity* fx = createParticleAestheticOrbit(my, my->sprite, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE_NOSOUND); + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->yaw = my->yaw; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 1;*/ - return entity; -} + //int numParticles = 8; + //for ( int c = 0; c < 8; c++ ) + //{ + // Entity* entity = newEntity(my->skill[5], 1, map.entities, nullptr); //Particle entity. + // entity->sizex = 1; + // entity->sizey = 1; + // entity->x = my->x; + // entity->y = my->y; + // entity->z = -10 + my->fskill[0]; + // entity->yaw = yaw; + // entity->vel_x = 0.2; + // entity->vel_y = 0.2; + // entity->vel_z = -0.02; + // entity->skill[0] = 100; + // entity->skill[1] = 0; // direction. + // entity->fskill[0] = 0.1; + // entity->scalex = 0.1; + // entity->scaley = 0.1; + // entity->scalez = 0.1; + // entity->behavior = &actParticleErupt; + // entity->flags[PASSABLE] = true; + // entity->flags[NOUPDATE] = true; + // entity->flags[UNCLICKABLE] = true; + // entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + // *cvar_magic_fx_light_bonus, 0.f); + // if ( multiplayer != CLIENT ) + // { + // entity_uids--; + // } + // entity->setUID(-3); + // yaw += 2 * PI / numParticles; + //} -void spawnMagicEffectParticles(Sint16 x, Sint16 y, Sint16 z, Uint32 sprite) -{ - int c; - if ( multiplayer == SERVER ) + if ( multiplayer != CLIENT ) + { + Uint32 casterUid = static_cast(my->skill[2]); + Entity* caster = uidToEntity(casterUid); + Entity* parent = uidToEntity(my->parent); + if ( caster && caster->behavior == &actPlayer + && parent ) + { + // caster is alive, notify they lost their mark + //Uint32 color = makeColorRGB(255, 255, 255); + //messagePlayerMonsterEvent(caster->skill[2], color, *(parent->getStats()), Language::get(3466), Language::get(3467), MSG_COMBAT); + } + if ( parent && parent->getStats() ) + { + if ( spellID == SPELL_PINPOINT ) + { + parent->setEffect(EFF_PINPOINT, false, 0, true); + } + else if ( spellID == SPELL_PENANCE ) + { + parent->setEffect(EFF_PENANCE, false, 0, true); + } + else if ( spellID == SPELL_TABOO ) + { + parent->setEffect(EFF_TABOO, false, 0, true); + } + else if ( spellID == SPELL_DETECT_ENEMY || spellID == SPELL_DETECT_ENEMIES ) + { + parent->setEffect(EFF_DETECT_ENEMY, false, 0, true); + } + } + } + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else { - for ( c = 1; c < MAXPLAYERS; c++ ) + Entity* parent = uidToEntity(my->parent); + if ( spellID == SPELL_DONATION ) { - if ( client_disconnected[c] || players[c]->isLocalPlayer() ) + // don't decay + if ( parent && parent->behavior == &actColliderDecoration ) { - continue; + Entity* donation = uidToEntity(parent->colliderContainedEntity); + if ( donation ) + { + my->parent = parent->colliderContainedEntity; + parent = donation; + } } - strcpy((char*)net_packet->data, "MAGE"); - SDLNet_Write16(x, &net_packet->data[4]); - SDLNet_Write16(y, &net_packet->data[6]); - SDLNet_Write16(z, &net_packet->data[8]); - SDLNet_Write32(sprite, &net_packet->data[10]); - net_packet->address.host = net_clients[c - 1].host; - net_packet->address.port = net_clients[c - 1].port; - net_packet->len = 14; - sendPacketSafe(net_sock, -1, net_packet, c - 1); } - } - - // boosty boost - for ( c = 0; c < 10; c++ ) - { - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->x = x - 5 + local_rng.rand() % 11; - entity->y = y - 5 + local_rng.rand() % 11; - entity->z = z - 10 + local_rng.rand() % 21; - entity->scalex = 0.7; - entity->scaley = 0.7; - entity->scalez = 0.7; - entity->sizex = 1; - entity->sizey = 1; - entity->yaw = (local_rng.rand() % 360) * PI / 180.f; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->behavior = &actMagicParticle; - entity->vel_z = -1; - if ( multiplayer != CLIENT ) + else { - entity_uids--; + --PARTICLE_LIFE; } - entity->setUID(-3); - } -} + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, true)); -void createParticleCircling(Entity* parent, int duration, int sprite) -{ - if ( !parent ) - { - return; - } + if ( parent ) + { + my->x = parent->x; + my->y = parent->y; + Entity::EntityShowMapSource mapSource = Entity::SHOW_MAP_SCRY; + if ( spellID == SPELL_DONATION ) + { + mapSource = Entity::SHOW_MAP_DONATION; + } + else if ( spellID == SPELL_PINPOINT ) + { + mapSource = Entity::SHOW_MAP_PINPOINT; + } + else if ( spellID == SPELL_DETECT_ENEMY || spellID == SPELL_DETECT_ENEMIES ) + { + mapSource = Entity::SHOW_MAP_DETECT_MONSTER; + } - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x; - entity->y = parent->y; - entity->focalx = 8; - entity->z = -7; - entity->vel_z = 0.15; - entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->skill[0] = duration; - entity->skill[1] = -1; - //entity->scalex = 0.01; - //entity->scaley = 0.01; - entity->fskill[0] = -0.1; - entity->behavior = &actParticleCircle; - entity->flags[PASSABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->setUID(-3); + if ( parent->getEntityShowOnMapDuration() == 0 + || (parent->getEntityShowOnMapSource() == mapSource) ) + { + parent->setEntityShowOnMap(mapSource, std::max(parent->getEntityShowOnMapDuration(), 5)); + } - real_t tmp = entity->yaw; + if ( multiplayer != CLIENT ) + { + if ( spellID == SPELL_SCRY_TREASURES ) + { + if ( parent->behavior == &actChest ) + { + if ( parent->chestVoidState != 0 ) + { + PARTICLE_LIFE = -1; + } + else if ( list_t* inventory = parent->getChestInventoryList() ) + { + if ( !inventory->first ) + { + PARTICLE_LIFE = -1; + } + } + } + } + else if ( spellID == SPELL_SCRY_ALLIES ) + { + if ( parent->monsterAllyGetPlayerLeader() ) + { + PARTICLE_LIFE = -1; + Uint32 casterUid = static_cast(my->skill[2]); + if ( Entity* caster = uidToEntity(casterUid) ) + { + magicOnSpellCastEvent(caster, my, nullptr, SPELL_SCRY_ALLIES, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + } + } + } - entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x; - entity->y = parent->y; - entity->focalx = 8; - entity->z = -7; - entity->vel_z = 0.15; - entity->yaw = tmp + (2 * PI / 3); - entity->particleDuration = duration; - entity->skill[1] = -1; - //entity->scalex = 0.01; - //entity->scaley = 0.01; - entity->fskill[0] = -0.1; - entity->behavior = &actParticleCircle; - entity->flags[PASSABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->setUID(-3); + if ( my->skill[1] >= 50 ) // stop changing size + { + real_t maxspeed = .03; + real_t acceleration = 0.95; + if ( my->skill[3] == 0 ) + { + // once off, store the normal height of the particle. + my->skill[3] = 1; + my->vel_z = -maxspeed; + } + if ( my->skill[1] % 5 == 0 ) + { + Uint32 casterUid = static_cast(my->skill[2]); + Entity* caster = uidToEntity(casterUid); + if ( caster && parent ) + { + // caster is alive, and they have still marked the parent this particle is following. + if ( spellID == SPELL_PENANCE ) + { + if ( parent->monsterAllyGetPlayerLeader() ) + { + PARTICLE_LIFE = -1; + if ( multiplayer != CLIENT ) + { + parent->setEffect(SPELL_PENANCE, false, 0, true); + } + } + if ( Stat* parentStats = parent->getStats() ) + { + if ( !parentStats->getEffectActive(EFF_PENANCE) ) + { + PARTICLE_LIFE = -1; + } + } + } + else if ( spellID == SPELL_DETECT_ENEMY || spellID == SPELL_DETECT_ENEMIES ) + { + if ( Stat* parentStats = parent->getStats() ) + { + if ( !parentStats->getEffectActive(EFF_DETECT_ENEMY) ) + { + PARTICLE_LIFE = -1; + } + } + } + else if ( spellID == SPELL_TABOO ) + { + if ( Stat* parentStats = parent->getStats() ) + { + if ( !parentStats->getEffectActive(EFF_TABOO) ) + { + PARTICLE_LIFE = -1; + } + } + } + } + else + { + PARTICLE_LIFE = -1; + } + } + + if ( PARTICLE_LIFE > 0 && PARTICLE_LIFE < TICKS_PER_SECOND ) + { + if ( spellID == SPELL_PINPOINT ) + { + if ( parent && parent->getStats() && parent->getStats()->getEffectActive(EFF_PINPOINT) ) + { + ++PARTICLE_LIFE; + } + } + else if ( spellID == SPELL_PENANCE ) + { + if ( parent && parent->getStats() && parent->getStats()->getEffectActive(EFF_PENANCE) ) + { + ++PARTICLE_LIFE; + } + } + else if ( spellID == SPELL_TABOO ) + { + if ( parent && parent->getStats() && parent->getStats()->getEffectActive(EFF_TABOO) ) + { + ++PARTICLE_LIFE; + } + } + else if ( spellID == SPELL_DETECT_ENEMY || spellID == SPELL_DETECT_ENEMIES ) + { + if ( parent && parent->getStats() && parent->getStats()->getEffectActive(EFF_DETECT_ENEMY) ) + { + ++PARTICLE_LIFE; + } + } + else + { + /*if ( parent && parent->getStats() && parent->getStats()->getEffectActive(EFF_SHADOW_TAGGED) ) + { + ++PARTICLE_LIFE; + }*/ + } + } + // bob up and down movement. + if ( my->skill[3] == 1 ) + { + my->vel_z *= acceleration; + if ( my->vel_z > -0.005 ) + { + my->skill[3] = 2; + my->vel_z = -0.005; + } + my->z += my->vel_z; + } + else if ( my->skill[3] == 2 ) + { + my->vel_z /= acceleration; + if ( my->vel_z < -maxspeed ) + { + my->skill[3] = 3; + my->vel_z = -maxspeed; + } + my->z -= my->vel_z; + } + else if ( my->skill[3] == 3 ) + { + my->vel_z *= acceleration; + if ( my->vel_z > -0.005 ) + { + my->skill[3] = 4; + my->vel_z = -0.005; + } + my->z -= my->vel_z; + } + else if ( my->skill[3] == 4 ) + { + my->vel_z /= acceleration; + if ( my->vel_z < -maxspeed ) + { + my->skill[3] = 1; + my->vel_z = -maxspeed; + } + my->z += my->vel_z; + } + my->yaw += 0.01; + } + else + { + my->z += my->vel_z; + my->yaw += my->vel_z * 2; + if ( my->scalex < 0.5 ) + { + my->scalex += 0.02; + } + else + { + my->scalex = 0.5; + } + my->scaley = my->scalex; + my->scalez = my->scalex; + if ( my->z < -3 + my->fskill[0] ) + { + my->vel_z *= 0.9; + } + } + + // once off, fire some erupt dot particles at start. + if ( my->skill[1] == 0 ) + { + for ( int i = 0; i < 4; ++i ) + { + Entity* fx = spawnMagicParticle(my); + fx->vel_x = 0.5 * cos(my->yaw + i * PI / 2); + fx->vel_y = 0.5 * sin(my->yaw + i * PI / 2); + fx->fskill[1] = 0.05; + fx->yaw = my->yaw + i * PI / 2; + fx->scalex = 0.7; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + } + //real_t yaw = 0; + //int numParticles = 8; + //for ( int c = 0; c < 8; c++ ) + //{ + // Entity* entity = newEntity(my->skill[5], 1, map.entities, nullptr); //Particle entity. + // entity->sizex = 1; + // entity->sizey = 1; + // entity->x = my->x; + // entity->y = my->y; + // entity->z = -10 + my->fskill[0]; + // entity->yaw = yaw; + // entity->vel_x = 0.2; + // entity->vel_y = 0.2; + // entity->vel_z = -0.02; + // entity->skill[0] = 100; + // entity->skill[1] = 0; // direction. + // entity->fskill[0] = 0.1; + // entity->scalex = 0.1; + // entity->scaley = 0.1; + // entity->scalez = 0.1; + // entity->behavior = &actParticleErupt; + // entity->flags[PASSABLE] = true; + // entity->flags[NOUPDATE] = true; + // entity->flags[UNCLICKABLE] = true; + // entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + // *cvar_magic_fx_light_bonus, 0.f); + // if ( multiplayer != CLIENT ) + // { + // entity_uids--; + // } + // entity->setUID(-3); + // yaw += 2 * PI / numParticles; + //} + } + ++my->skill[1]; + } +} - entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; +Entity* createParticleSpellPinpointTarget(Entity* parent, Uint32 casterUid, int sprite, int duration, int spellID) +{ + if ( !parent ) + { + return nullptr; + } + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. + entity->parent = parent->getUID(); entity->x = parent->x; entity->y = parent->y; - entity->focalx = 8; - entity->z = -7; - entity->vel_z = 0.15; - entity->yaw = tmp - (2 * PI / 3); - entity->particleDuration = duration; - entity->skill[1] = -1; - //entity->scalex = 0.01; - //entity->scaley = 0.01; - entity->fskill[0] = -0.1; - entity->behavior = &actParticleCircle; + entity->z = 7.5; + static ConsoleVariable cvar_pinpoint_z("/pinpoint_z", 0.0); + entity->fskill[0] = parent->z + *cvar_pinpoint_z; + if ( parent->behavior == &actBoulderTrapHole ) + { + entity->fskill[0] += 15.0; + } + entity->vel_z = -0.8; + entity->scalex = 0.1; + entity->scaley = 0.1; + entity->scalez = 0.1; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = duration; + entity->skill[1] = 0; + entity->skill[2] = static_cast(casterUid); + entity->skill[3] = 0; + entity->skill[4] = spellID; + entity->skill[5] = sprite;// +(PINPOINT_PARTICLE_END - PINPOINT_PARTICLE_START); // start/end particle drops + entity->behavior = &actParticlePinpointTarget; + entity->ditheringOverride = 6; entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[ENTITY_SKIP_CULLING] = true; entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, 0.f); + TileEntityList.addEntity(*entity); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } entity->setUID(-3); - entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x; - entity->y = parent->y; - entity->focalx = 16; - entity->z = -12; - entity->vel_z = 0.2; - entity->yaw = tmp; - entity->particleDuration = duration; - entity->skill[1] = -1; - //entity->scalex = 0.01; - //entity->scaley = 0.01; - entity->fskill[0] = 0.1; - entity->behavior = &actParticleCircle; - entity->flags[PASSABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->setUID(-3); + for ( auto node = map.entities->first; node; node = node->next ) + { + if ( Entity* entity2 = (Entity*)node->element ) + { + if ( entity2->behavior == &actParticlePinpointTarget + && entity2 != entity + && entity2->parent == entity->parent ) + { + //if ( entity2->skill[4] == spellID ) + { + entity2->skill[0] = -1; // kill off existing particle + } + } + } + } - entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x; - entity->y = parent->y; - entity->focalx = 16; - entity->z = -12; - entity->vel_z = 0.2; - entity->yaw = tmp + (2 * PI / 3); - entity->particleDuration = duration; - entity->skill[1] = -1; - //entity->scalex = 0.01; - //entity->scaley = 0.01; - entity->fskill[0] = 0.1; - entity->behavior = &actParticleCircle; - entity->flags[PASSABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->setUID(-3); + return entity; +} - entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; +void createParticleCharmMonster(Entity* parent) +{ + if ( !parent ) + { + return; + } + Entity* entity = newEntity(685, 1, map.entities, nullptr); //Particle entity. + //entity->sizex = 1; + //entity->sizey = 1; + entity->parent = parent->getUID(); entity->x = parent->x; entity->y = parent->y; - entity->focalx = 16; - entity->z = -12; - entity->vel_z = 0.2; - entity->yaw = tmp - (2 * PI / 3); - entity->particleDuration = duration; - entity->skill[1] = -1; - //entity->scalex = 0.01; - //entity->scaley = 0.01; - entity->fskill[0] = 0.1; - entity->behavior = &actParticleCircle; + entity->z = 7.5; + entity->vel_z = -0.8; + entity->scalex = 0.1; + entity->scaley = 0.1; + entity->scalez = 0.1; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->skill[0] = 45; + entity->behavior = &actParticleCharmMonster; entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } entity->setUID(-3); } -#define PARTICLE_LIFE my->skill[0] - -void actParticleCircle(Entity* my) +void actParticleCharmMonster(Entity* my) { if ( PARTICLE_LIFE < 0 ) { + real_t yaw = 0; + int numParticles = 8; + for ( int c = 0; c < 8; c++ ) + { + Entity* entity = newEntity(576, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = my->x; + entity->y = my->y; + entity->z = -10; + entity->yaw = yaw; + entity->vel_x = 0.2; + entity->vel_y = 0.2; + entity->vel_z = -0.02; + entity->skill[0] = 100; + entity->skill[1] = 0; // direction. + entity->fskill[0] = 0.1; + entity->behavior = &actParticleErupt; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + yaw += 2 * PI / numParticles; + } list_RemoveNode(my->mynode); return; } else { --PARTICLE_LIFE; - my->yaw += my->fskill[0]; - if ( my->fskill[0] < 0.4 && my->fskill[0] > (-0.4) ) + Entity* parent = uidToEntity(my->parent); + if ( parent ) { - my->fskill[0] = my->fskill[0] * 1.05; + my->x = parent->x; + my->y = parent->y; } my->z += my->vel_z; - if ( my->focalx > 0.05 ) + my->yaw += my->vel_z * 2; + if ( my->scalex < 0.8 ) { - if ( my->vel_z == 0.15 ) - { - my->focalx = my->focalx * 0.97; - } - else - { - my->focalx = my->focalx * 0.97; - } + my->scalex += 0.02; + } + else + { + my->scalex = 0.8; + } + my->scaley = my->scalex; + my->scalez = my->scalex; + if ( my->z < -3 ) + { + my->vel_z *= 0.9; } - my->scalex *= 0.995; - my->scaley *= 0.995; - my->scalez *= 0.995; } } -void createParticleDot(Entity* parent) +void spawnMagicTower(Entity* parent, real_t x, real_t y, int spellID, Entity* autoHitTarget, bool castedSpell) { - if ( !parent ) + bool autoHit = false; + if ( autoHitTarget && (autoHitTarget->behavior == &actPlayer || autoHitTarget->behavior == &actMonster) ) { - return; + autoHit = true; + if ( parent ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) && parent->checkFriend(autoHitTarget) && parent->friendlyFireProtection(autoHitTarget) ) + { + autoHit = false; // don't hit friendlies + } + } } - for ( int c = 0; c < 50; c++ ) + Entity* orbit = castStationaryOrbitingMagicMissile(parent, spellID, x, y, 16.0, 0.0, 40); + if ( orbit ) { - Entity* entity = newEntity(576, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x + (-4 + local_rng.rand() % 9); - entity->y = parent->y + (-4 + local_rng.rand() % 9); - entity->z = 7.5 + local_rng.rand()%50; - entity->vel_z = -1; - //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->skill[0] = 10 + local_rng.rand()% 50; - entity->behavior = &actParticleDot; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) + orbit->actmagicUpdateOLDHPOnHit = 1; + if ( castedSpell ) + { + orbit->actmagicOrbitCastFromSpell = 1; + } + if ( autoHit ) + { + orbit->actmagicOrbitHitTargetUID4 = autoHitTarget->getUID(); + } + } + orbit = castStationaryOrbitingMagicMissile(parent, spellID, x, y, 16.0, 2 * PI / 3, 40); + if ( orbit ) + { + orbit->actmagicUpdateOLDHPOnHit = 1; + if ( castedSpell ) + { + orbit->actmagicOrbitCastFromSpell = 1; + } + if ( autoHit ) + { + orbit->actmagicOrbitHitTargetUID4 = autoHitTarget->getUID(); + } + } + orbit = castStationaryOrbitingMagicMissile(parent, spellID, x, y, 16.0, 4 * PI / 3, 40); + if ( orbit ) + { + orbit->actmagicUpdateOLDHPOnHit = 1; + if ( castedSpell ) + { + orbit->actmagicOrbitCastFromSpell = 1; + } + if ( autoHit ) { - entity_uids--; + orbit->actmagicOrbitHitTargetUID4 = autoHitTarget->getUID(); } - entity->setUID(-3); } + spawnMagicEffectParticles(x, y, 0, 174); + spawnExplosion(x, y, -4 + local_rng.rand() % 8); } -Entity* createParticleAestheticOrbit(Entity* parent, int sprite, int duration, int effectType) +bool magicDig(Entity* parent, Entity* projectile, int numRocks, int randRocks) { - if ( !parent ) - { - return nullptr; - } - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->actmagicOrbitDist = 6; - entity->yaw = parent->yaw; - entity->x = parent->x + entity->actmagicOrbitDist * cos(entity->yaw); - entity->y = parent->y + entity->actmagicOrbitDist * sin(entity->yaw); - entity->z = parent->z; - entity->skill[1] = effectType; - entity->parent = parent->getUID(); - //entity->vel_z = -1; - //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->skill[0] = duration; - entity->fskill[0] = entity->x; - entity->fskill[1] = entity->y; - entity->behavior = &actParticleAestheticOrbit; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) + if ( !hit.entity ) { - entity_uids--; - } - entity->setUID(-3); - return entity; -} + if ( map.tiles[(int)(OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height)] != 0 ) + { + if ( MFLAG_DISABLEDIGGING ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Uint32 color = makeColorRGB(255, 0, 255); + messagePlayerColor(parent->skill[2], MESSAGE_HINT, color, Language::get(2380)); // disabled digging. + } + playSoundPos(hit.x, hit.y, 66, 128); // strike wall + } + else if ( swimmingtiles[map.tiles[OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height]] + || lavatiles[map.tiles[OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height]] ) + { + // no effect for lava/water tiles. + } + else if ( !mapTileDiggable(hit.mapx, hit.mapy) ) + { + if ( parent && parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_HINT, Language::get(706)); + } + playSoundPos(hit.x, hit.y, 66, 128); // strike wall + } + else + { + if ( projectile ) + { + playSoundEntity(projectile, 66, 128); + playSoundEntity(projectile, 67, 128); + } -void createParticleRock(Entity* parent, int sprite, bool light) -{ - if ( !parent ) - { - return; + // spawn several rock items + if ( randRocks <= 0 ) + { + randRocks = 1; + } + int i = numRocks + local_rng.rand() % randRocks; + for ( int c = 0; c < i; c++ ) + { + Entity* rock = newEntity(-1, 1, map.entities, nullptr); //Rock entity. + rock->flags[INVISIBLE] = true; + rock->flags[UPDATENEEDED] = true; + rock->x = hit.mapx * 16 + 4 + local_rng.rand() % 8; + rock->y = hit.mapy * 16 + 4 + local_rng.rand() % 8; + rock->z = -6 + local_rng.rand() % 12; + rock->sizex = 4; + rock->sizey = 4; + rock->yaw = local_rng.rand() % 360 * PI / 180; + rock->vel_x = (local_rng.rand() % 20 - 10) / 10.0; + rock->vel_y = (local_rng.rand() % 20 - 10) / 10.0; + rock->vel_z = -.25 - (local_rng.rand() % 5) / 10.0; + rock->flags[PASSABLE] = true; + rock->behavior = &actItem; + rock->flags[USERFLAG1] = true; // no collision: helps performance + rock->skill[10] = GEM_ROCK; // type + rock->skill[11] = WORN; // status + rock->skill[12] = 0; // beatitude + rock->skill[13] = 1; // count + rock->skill[14] = 0; // appearance + rock->skill[15] = 1; // identified + } + + if ( map.tiles[(int)(OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height)] >= 41 + && map.tiles[(int)(OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height)] <= 49 ) + { + steamAchievementEntity(parent, "BARONY_ACH_BAD_REVIEW"); + } + + map.tiles[(int)(OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height)] = 0; + + // send wall destroy info to clients + if ( multiplayer == SERVER ) + { + for ( int c = 1; c < MAXPLAYERS; c++ ) + { + if ( players[c]->isLocalPlayer() || client_disconnected[c] == true ) + { + continue; + } + strcpy((char*)net_packet->data, "WALD"); + SDLNet_Write16((Uint16)hit.mapx, &net_packet->data[4]); + SDLNet_Write16((Uint16)hit.mapy, &net_packet->data[6]); + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } + } + + if ( projectile && projectile->behavior == &actMagicMissile ) + { + magicOnSpellCastEvent(parent, projectile, nullptr, SPELL_DIG, spell_t::SpellOnCastEventTypes::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + + generatePathMaps(); + return true; + } + } + return false; } - for ( int c = 0; c < 5; c++ ) + else if ( hit.entity->behavior == &actColliderDecoration && hit.entity->colliderDiggable != 0 ) { - Entity* entity = newEntity(sprite != -1 ? sprite : 78, 1, map.entities, nullptr); //Particle entity. - if ( entity->sprite == 1336 ) + int sprite = EditorEntityData_t::colliderData[hit.entity->colliderDamageTypes].gib; + if ( sprite > 0 ) { - entity->sprite = 1336 + local_rng.rand() % 3; + createParticleRock(hit.entity, sprite); + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_ABILITY_ROCK, sprite); + } } - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x + (-4 + local_rng.rand() % 9); - entity->y = parent->y + (-4 + local_rng.rand() % 9); - entity->z = 7.5; - entity->yaw = c * 2 * PI / 5;//(local_rng.rand() % 360) * PI / 180.0; - entity->roll = (local_rng.rand() % 360) * PI / 180.0; - - entity->vel_x = 0.2 * cos(entity->yaw); - entity->vel_y = 0.2 * sin(entity->yaw); - entity->vel_z = 3;// 0.25 - (local_rng.rand() % 5) / 10.0; - - entity->skill[0] = 50; // particle life - entity->skill[1] = 0; // particle direction, 0 = upwards, 1 = downwards. - entity->behavior = &actParticleRock; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - if ( !light ) + hit.entity->colliderOnDestroy(); + if ( parent ) { - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); + if ( parent->behavior == &actPlayer && hit.entity->isDamageableCollider() ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(4337), + Language::get(hit.entity->getColliderLangName())); // you destroy the %s! + if ( hit.entity->isColliderWall() ) + { + Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_BARRIER_DESTROYED, "breakable barriers", 1); + } + } } - if ( multiplayer != CLIENT ) + + if ( projectile && projectile->behavior == &actMagicMissile ) { - entity_uids--; + magicOnEntityHit(parent, projectile, hit.entity, nullptr, 0, 0, 0, SPELL_DIG); } - entity->setUID(-3); - } -} -void createParticleShatteredGem(real_t x, real_t y, real_t z, int sprite, Entity* parent) -{ - for ( int c = 0; c < 5; c++ ) + // destroy the object + playSoundEntity(hit.entity, 67, 128); + list_RemoveNode(hit.entity->mynode); + return true; + } + else if ( hit.entity->behavior == &::actDaedalusShrine ) { - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - if ( parent ) + createParticleRock(hit.entity); + if ( multiplayer == SERVER ) { - entity->x = parent->x + (-4 + local_rng.rand() % 9); - entity->y = parent->y + (-4 + local_rng.rand() % 9); + serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_ABILITY_ROCK, 78); } - else + + if ( projectile && projectile->behavior == &actMagicMissile ) { - entity->x = x + (-4 + local_rng.rand() % 9); - entity->y = y + (-4 + local_rng.rand() % 9); + magicOnEntityHit(parent, projectile, hit.entity, nullptr, 0, 0, 0, SPELL_DIG); } - entity->z = z; - entity->yaw = c * 2 * PI / 5;//(local_rng.rand() % 360) * PI / 180.0; - entity->roll = (local_rng.rand() % 360) * PI / 180.0; - - entity->vel_x = 0.2 * cos(entity->yaw); - entity->vel_y = 0.2 * sin(entity->yaw); - entity->vel_z = 3;// 0.25 - (local_rng.rand() % 5) / 10.0; - - real_t scale = .4; - entity->scalex = scale; - entity->scaley = scale; - entity->scalez = scale; - entity->skill[0] = 50; // particle life - entity->skill[1] = 0; // particle direction, 0 = upwards, 1 = downwards. + playSoundEntity(hit.entity, 67, 128); + list_RemoveNode(hit.entity->mynode); + } + else if ( hit.entity->behavior == &actBoulder ) + { + int i = numRocks + local_rng.rand() % 4; - entity->behavior = &actParticleRock; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) + // spawn several rock items //TODO: This should really be its own function. + for ( int c = 0; c < i; c++ ) { - entity_uids--; + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Rock entity. + entity->flags[INVISIBLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->x = hit.entity->x - 4 + local_rng.rand() % 8; + entity->y = hit.entity->y - 4 + local_rng.rand() % 8; + entity->z = -6 + local_rng.rand() % 12; + entity->sizex = 4; + entity->sizey = 4; + entity->yaw = local_rng.rand() % 360 * PI / 180; + entity->vel_x = (local_rng.rand() % 20 - 10) / 10.0; + entity->vel_y = (local_rng.rand() % 20 - 10) / 10.0; + entity->vel_z = -.25 - (local_rng.rand() % 5) / 10.0; + entity->flags[PASSABLE] = true; + entity->behavior = &actItem; + entity->flags[USERFLAG1] = true; // no collision: helps performance + entity->skill[10] = GEM_ROCK; // type + entity->skill[11] = WORN; // status + entity->skill[12] = 0; // beatitude + entity->skill[13] = 1; // count + entity->skill[14] = 0; // appearance + entity->skill[15] = false; // identified } - entity->setUID(-3); - } -} -void actParticleRock(Entity* my) -{ - if ( PARTICLE_LIFE < 0 || my->z > 10 ) - { - list_RemoveNode(my->mynode); - } - else - { - --PARTICLE_LIFE; - my->x += my->vel_x; - my->y += my->vel_y; + double ox = hit.entity->x; + double oy = hit.entity->y; - my->roll += 0.1; + boulderLavaOrArcaneOnDestroy(hit.entity, hit.entity->sprite, nullptr); - if ( my->vel_z < 0.01 ) + auto& rng = hit.entity->entity_rng ? *hit.entity->entity_rng : local_rng; + Uint32 monsterSpawnSeed = rng.getU32(); + + if ( projectile && projectile->behavior == &actMagicMissile ) { - my->skill[1] = 1; // start moving downwards - my->vel_z = 0.1; + magicOnEntityHit(parent, projectile, hit.entity, nullptr, 0, 0, 0, SPELL_DIG); } - if ( my->skill[1] == 0 ) // upwards motion + // destroy the boulder + playSoundEntity(hit.entity, 67, 128); + list_RemoveNode(hit.entity->mynode); + if ( parent ) { - my->z -= my->vel_z; - my->vel_z *= 0.7; + if ( parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(405)); + } } - else // downwards motion + + // on sokoban, destroying boulders spawns scorpions + if ( !strcmp(map.name, "Sokoban") ) { - my->z += my->vel_z; - my->vel_z *= 1.1; + Entity* monster = nullptr; + if ( local_rng.rand() % 2 == 0 ) + { + monster = summonMonster(INSECTOID, ox, oy); + } + else + { + monster = summonMonster(SCORPION, ox, oy); + } + if ( monster ) + { + monster->seedEntityRNG(monsterSpawnSeed); + for ( int c = 0; c < MAXPLAYERS; c++ ) + { + Uint32 color = makeColorRGB(255, 128, 0); + messagePlayerColor(c, MESSAGE_HINT, color, Language::get(406)); + } + } + boulderSokobanOnDestroy(false); } + return true; } - return; + return false; } -void actParticleDot(Entity* my) +Entity* createParticleCastingIndicator(Entity* parent, real_t x, real_t y, real_t z, Uint32 lifetime, Uint32 followUid) { - if ( PARTICLE_LIFE < 0 ) + Uint32 uid = 0; + if ( parent ) { - list_RemoveNode(my->mynode); + uid = parent->getUID(); } - else + + Entity* entity = newEntity(222, 1, map.entities, nullptr); //Sprite entity. + entity->x = x; + entity->y = y; + entity->z = 7.470; + static ConsoleVariable cvar_sprite_cast_indicator_scale("/sprite_cast_indicator_scale", 0.025); + static ConsoleVariable cvar_sprite_cast_indicator_rotate("/sprite_cast_indicator_rotate", 0.025); + static ConsoleVariable cvar_sprite_cast_indicator_alpha("/sprite_cast_indicator_alpha", 0.5); + static ConsoleVariable cvar_sprite_cast_indicator_alpha_glow("/sprite_cast_indicator_alpha_glow", 0.0625); + entity->ditheringDisabled = true; + entity->flags[SPRITE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + entity->scalex = *cvar_sprite_cast_indicator_scale; + entity->scaley = *cvar_sprite_cast_indicator_scale; + entity->behavior = &actSprite; + entity->yaw = local_rng.rand() % 360 * PI / 180; + entity->pitch = 0; + entity->roll = -PI / 2; + entity->skill[0] = 1; + entity->skill[1] = 1; + entity->skill[2] = lifetime; + entity->fskill[0] = *cvar_sprite_cast_indicator_rotate; + entity->fskill[2] = *cvar_sprite_cast_indicator_alpha; // alpha + entity->fskill[3] = *cvar_sprite_cast_indicator_alpha_glow; + entity->actSpriteUseAlpha = 1; // use alpha + entity->actSpriteNoBillboard = 1; // no billboard + entity->actSpriteCheckParentExists = uid; + entity->flags[ENTITY_SKIP_CULLING] = true; + if ( followUid ) { - --PARTICLE_LIFE; - my->z += my->vel_z; - //my->z -= 0.01; + entity->actSpriteFollowUID = followUid; } - return; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + return entity; } -void actParticleAestheticOrbit(Entity* my) +void AOEIndicators_t::cleanup() { - if ( PARTICLE_LIFE < 0 ) + indicators.clear(); + + for ( auto& m1 : surfaceCache ) { - list_RemoveNode(my->mynode); + for ( auto& m2 : m1.second ) + { + SDL_FreeSurface(m2.second); + m2.second = nullptr; + } } - else + surfaceCache.clear(); +} +std::map AOEIndicators_t::indicators; +Uint32 AOEIndicators_t::uids = 1; +void AOEIndicators_t::update() +{ + std::vector toDelete; + for ( auto& i : indicators ) { - Entity* parent = uidToEntity(my->parent); - if ( !parent ) + i.second.lifetime--; + + if ( i.second.castingTarget ) { i.second.lifetime = 1; } // never expire + + if ( i.second.lifetime <= 0 ) { - list_RemoveNode(my->mynode); - return; + toDelete.push_back(i.second.uid); } - Stat* stats = parent->getStats(); - if ( my->skill[1] == PARTICLE_EFFECT_SPELLBOT_ORBIT ) + else if ( i.second.expired ) { - my->yaw = parent->yaw; - my->x = parent->x + 2 * cos(parent->yaw); - my->y = parent->y + 2 * sin(parent->yaw); - my->z = parent->z - 1.5; - Entity* particle = spawnMagicParticle(my); - if ( particle ) + Uint8 alpha = 0; + getColor(i.second.indicatorColor, nullptr, nullptr, nullptr, &alpha); + if ( alpha == 0 ) { - particle->x = my->x + (-10 + local_rng.rand() % 21) / (50.f); - particle->y = my->y + (-10 + local_rng.rand() % 21) / (50.f); - particle->z = my->z + (-10 + local_rng.rand() % 21) / (50.f); - particle->scalex = my->scalex; - particle->scaley = my->scaley; - particle->scalez = my->scalez; + toDelete.push_back(i.second.uid); } - //spawnMagicParticle(my); - } - else if ( my->skill[1] == PARTICLE_EFFECT_SPELL_WEB_ORBIT ) - { - if ( my->sprite == 863 && (!stats || !stats->EFFECTS[EFF_WEBBED]) ) + else { - list_RemoveNode(my->mynode); - return; + i.second.updateIndicator(); } - my->yaw += 0.2; - spawnMagicParticle(my); - my->x = parent->x + my->actmagicOrbitDist * cos(my->yaw); - my->y = parent->y + my->actmagicOrbitDist * sin(my->yaw); } - --PARTICLE_LIFE; + else + { + i.second.updateIndicator(); + } + } + + for ( auto& uid : toDelete ) + { + indicators.erase(uid); } - return; } -void actParticleTest(Entity* my) +Uint32 AOEIndicators_t::createIndicator(int _radiusMin, int _radiusMax, int _size, int _lifetime) { - if ( PARTICLE_LIFE < 0 ) + Uint32 uid = uids; + uids++; + indicators.insert(std::make_pair(uid, Indicator_t(_radiusMin, _radiusMax, _size, _lifetime, uid))); + return uid; +} + +std::map, SDL_Surface*>> AOEIndicators_t::surfaceCache; +void AOEIndicators_t::Indicator_t::updateIndicator() +{ + if ( !(!gamePaused || (multiplayer && !client_disconnected[0])) ) { - list_RemoveNode(my->mynode); + // paused game return; } - else + + if ( delayTicks > 0 ) { - --PARTICLE_LIFE; - my->x += my->vel_x; - my->y += my->vel_y; - my->z += my->vel_z; - //my->z -= 0.01; + --delayTicks; + return; } -} -void createParticleErupt(Entity* parent, int sprite) -{ - if ( !parent ) + //auto t1 = std::chrono::high_resolution_clock::now(); + + const int ringSize = 1; + const int gradientSize = gradient; + int ring = radius - ringSize; + int center = size / 2; + //for ( int r = std::max(0, radius - gradientSize); r <= radius; r += 1 ) + //{ + // /*real_t minAngle = 0.9 * acos(1.0 - 1.0 / r); + // for ( real_t i = 0; i < 360; i += minAngle ) + // { + // x = (size / 2) + r * std::min(0.9999, cos(i)); + // y = (size / 2) + r * std::min(0.9999, sin(i)); + // assert(x >= 0 && x < size); + // assert(y >= 0 && y < size); + + // if ( r < ring ) + // { + // real_t alphaRatio = std::min(1.0, std::max(0.0, 1.0 + (ringSize + r - radius) / (real_t)gradientSize)); + // putPixel(surfaceNew, x, y, makeColor(255, 255, 255, std::min(255.0, 255 * alphaRatio))); + // } + // else if ( r == ring ) + // { + // putPixel(surfaceNew, x, y, makeColor(255, 255, 255, 255)); + // } + // else + // { + // putPixel(surfaceNew, x, y, makeColor(212, 212, 212, 255)); + // } + // }*/ + + // /*x = r; + // y = 0; + // int error = 3 - 2 * radius; + // while ( x >= y ) + // { + // putPixel(surfaceNew, center + x, center + y, makeColor(255, 255, 255, 255)); + // putPixel(surfaceNew, center + x, center - y, makeColor(255, 255, 255, 255)); + // putPixel(surfaceNew, center -x, center + y, makeColor(255, 255, 255, 255)); + // putPixel(surfaceNew, center -x, center - y, makeColor(255, 255, 255, 255)); + + // putPixel(surfaceNew, center + y, center + x, makeColor(255, 255, 255, 255)); + // putPixel(surfaceNew, center + y, center - x, makeColor(255, 255, 255, 255)); + // putPixel(surfaceNew, center - y, center + x, makeColor(255, 255, 255, 255)); + // putPixel(surfaceNew, center - y, center - x, makeColor(255, 255, 255, 255)); + + // if (error > 0) + // { + // error -= 4 * (--x); + // } + // error += 4 * (++y) + 2; + // }*/ + + //} + + Uint8 red, green, blue, alpha; + getColor(indicatorColor, &red, &green, &blue, &alpha); + if ( expired ) { - return; + alpha *= expireAlphaRate; + indicatorColor = makeColor(red, green, blue, alpha); + } + if ( loopType == 1 ) + { + if ( radius >= radiusMax ) + { + if ( loopTimer > 0 ) + { + alpha *= (loopTimer - loopTicks) / (real_t)loopTimer; + } + } } - real_t yaw = 0; - int numParticles = 8; - for ( int c = 0; c < 8; c++ ) + //auto t5 = std::chrono::high_resolution_clock::now(); + bool circle = !castingTarget; + + static ConsoleVariable cvar_aoe_indicator_cache("/aoe_indicator_cache", true); + if ( !*cvar_aoe_indicator_cache ) { - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x; - entity->y = parent->y; - entity->z = 7.5; // start from the ground. - entity->yaw = yaw; - entity->vel_x = 0.2; - entity->vel_y = 0.2; - entity->vel_z = -2; - entity->skill[0] = 100; - entity->skill[1] = 0; // direction. - entity->fskill[0] = 0.1; - entity->behavior = &actParticleErupt; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) + cacheType = CACHE_NONE; + } + + bool needsUpdate = true; + if ( prevData.r == red && + prevData.g == green && + prevData.b == blue && + prevData.a == alpha && + prevData.radMax == radius && + prevData.radMin == std::max(0, radius - gradientSize) + 0.0 && + prevData.size == size ) + { + needsUpdate = false; + } + + SDL_Surface* surfaceNew = nullptr; + if ( needsUpdate ) + { + auto tup = std::make_tuple( + red, green, blue, alpha, radius, std::max(0, radius - gradientSize) + 0.0, size); + + if ( cacheType != CACHE_NONE ) { - entity_uids--; + auto& cache = AOEIndicators_t::surfaceCache[cacheType]; + auto find = cache.find(tup); + if ( find != cache.end() ) + { + surfaceNew = find->second; + needsUpdate = false; + } + } + + if ( needsUpdate ) + { + surfaceNew = SDL_CreateRGBSurface(0, size, size, 32, + 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); + SDL_LockSurface(surfaceNew); + + for ( real_t rad = std::max(0, radius - gradientSize) + 0.0; rad <= radius; rad += 0.5 ) + { + Uint32 color = 0; + Uint8 alphaUsed = alpha; + if ( rad < ring + 0.5 ) + { + real_t alphaRatio = std::min(1.0, std::max(0.0, 1.0 + (ringSize + rad - (radius + 0.5)) / (real_t)gradientSize)); + alphaUsed = std::min(255.0, alpha * alphaRatio); + color = makeColor(red, green, blue, alphaUsed); + } + else if ( rad == ring + 0.5 ) + { + color = makeColor(red, green, blue, alpha); + } + else + { + color = makeColor(red * .8, green * .8, blue * .8, alpha); + } + + if ( !circle ) + { + real_t radius = rad + .5; + real_t r2 = radius * radius; + const int dist = radius;// floor(radius * sqrt(0.5)); + const int d = radius;//floor(sqrt(r2 - r * r)); + for ( int r = 0; r <= dist; r++ ) { + if ( !(center - d >= 0 && center + d < size) ) + { + continue; + } + if ( !(center - r >= 0 && center + r < size) ) + { + continue; + } + if ( d < 8 ) + { + Uint8 red, green, blue, alpha; + getColor(color, &red, &green, &blue, &alpha); + Uint8 prevAlpha = alpha; + alpha *= d / 8.0; + if ( alpha == 0 ) + { + continue; + } + color = makeColor(red, green, blue, alpha); + + putPixel(surfaceNew, center + d, center + r, color); + putPixel(surfaceNew, center + d, center - r, color); + + putPixel(surfaceNew, center - d, center + r, color); + putPixel(surfaceNew, center - d, center - r, color); + + putPixel(surfaceNew, center + r, center + d, color); + putPixel(surfaceNew, center - r, center + d, color); + + putPixel(surfaceNew, center + r, center - d, color); + putPixel(surfaceNew, center - r, center - d, color); + + color = makeColor(red, green, blue, prevAlpha); + continue; + } + if ( r < 8 ) + { + Uint8 red, green, blue, alpha; + getColor(color, &red, &green, &blue, &alpha); + Uint8 prevAlpha = alpha; + alpha *= r / 8.0; + if ( alpha == 0 ) + { + continue; + } + color = makeColor(red, green, blue, alpha); + putPixel(surfaceNew, center + d, center + r, color); + putPixel(surfaceNew, center + d, center - r, color); + + putPixel(surfaceNew, center - d, center + r, color); + putPixel(surfaceNew, center - d, center - r, color); + + putPixel(surfaceNew, center + r, center + d, color); + putPixel(surfaceNew, center - r, center + d, color); + + putPixel(surfaceNew, center + r, center - d, color); + putPixel(surfaceNew, center - r, center - d, color); + + color = makeColor(red, green, blue, prevAlpha); + continue; + } + if ( alphaUsed == 0 ) + { + continue; + } + putPixel(surfaceNew, center + d, center + r, color); + putPixel(surfaceNew, center + d, center - r, color); + + putPixel(surfaceNew, center - d, center + r, color); + putPixel(surfaceNew, center - d, center - r, color); + + putPixel(surfaceNew, center + r, center + d, color); + putPixel(surfaceNew, center - r, center + d, color); + + putPixel(surfaceNew, center + r, center - d, color); + putPixel(surfaceNew, center - r, center - d, color); + } + } + else if ( circle ) + { + const real_t radius = rad + .5; + const real_t r2 = radius * radius; + const int dist = floor(radius * sqrt(0.5)); + + for ( int r = 0; r <= dist; r++ ) { + if ( alphaUsed == 0 ) + { + break; + } + int d = floor(sqrt(r2 - r * r)); + if ( !(center - d >= 0 && center + d < size) ) + { + continue; + } + if ( !(center - r >= 0 && center + r < size) ) + { + continue; + } + + if ( arc > 0.001 ) + { + real_t tangent = atan2(r, d); + if ( tangent > arc ) + { + continue; + } + + putPixel(surfaceNew, center + r, center - d, color); + putPixel(surfaceNew, center - r, center - d, color); + } + else + { + putPixel(surfaceNew, center + d, center + r, color); + putPixel(surfaceNew, center + d, center - r, color); + + putPixel(surfaceNew, center - d, center + r, color); + putPixel(surfaceNew, center - d, center - r, color); + + putPixel(surfaceNew, center + r, center + d, color); + putPixel(surfaceNew, center - r, center + d, color); + + putPixel(surfaceNew, center + r, center - d, color); + putPixel(surfaceNew, center - r, center - d, color); + } + } + } + } + + if ( cacheType > CACHE_NONE ) + { + surfaceCache[cacheType][tup] = surfaceNew; + } } - entity->setUID(-3); - yaw += 2 * PI / numParticles; } -} -Entity* createParticleSapCenter(Entity* parent, Entity* target, int spell, int sprite, int endSprite) -{ - if ( !parent || !target ) - { - return nullptr; - } - // spawns the invisible 'center' of the magic particle - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = target->x; - entity->y = target->y; - entity->parent = (parent->getUID()); - entity->yaw = parent->yaw + PI; // face towards the caster. - entity->skill[0] = 45; - entity->skill[2] = -13; // so clients know my behavior. - entity->skill[3] = 0; // init - entity->skill[4] = sprite; // visible sprites. - entity->skill[5] = endSprite; // sprite to spawn on return to caster. - entity->skill[6] = spell; - entity->behavior = &actParticleSapCenter; - if ( target->sprite == 977 ) + //auto t6 = std::chrono::high_resolution_clock::now(); + //std::chrono::steady_clock::time_point t2; + //std::chrono::steady_clock::time_point t3; + //std::chrono::steady_clock::time_point t4; + //t2 = std::chrono::high_resolution_clock::now(); + //t3 = t2; + //t4 = t2; + + //std::chrono::steady_clock::time_point new1 = t2; + //std::chrono::steady_clock::time_point new2 = t2; + //std::chrono::steady_clock::time_point new3 = t2; + //std::chrono::steady_clock::time_point new4 = t2; + //std::chrono::steady_clock::time_point new5 = t2; + //std::chrono::steady_clock::time_point new6 = t2; + + auto m1 = surfaceNew; + auto m2 = surfaceOld; + if ( surfaceNew ) { - // boomerang. - entity->yaw = target->yaw; - entity->roll = target->roll; - entity->pitch = target->pitch; - entity->z = target->z; - } - entity->flags[INVISIBLE] = true; - entity->flags[PASSABLE] = true; - entity->flags[UPDATENEEDED] = true; - entity->flags[UNCLICKABLE] = true; - return entity; -} + //new1 = std::chrono::high_resolution_clock::now(); + if ( surfaceOld ) { + SDL_LockSurface(surfaceOld); + } -void createParticleSap(Entity* parent) -{ - real_t speed = 0.4; - if ( !parent ) - { - return; - } - for ( int c = 0; c < 4; c++ ) - { - // 4 particles, in an 'x' pattern around parent sprite. - int sprite = parent->sprite; - if ( parent->sprite == 977 ) - { - if ( c > 0 ) - { - continue; + //new2 = std::chrono::high_resolution_clock::now(); + const auto size1 = m1->w * m1->h * m1->format->BytesPerPixel; + const auto size2 = m2 ? (m2->w * m2->h * m2->format->BytesPerPixel) : 0; + if ( size1 != size2 || memcmp(m1, m2, size2) ) { + if ( !texture ) { + texture = new TempTexture(); } - // boomerang return. - sprite = parent->sprite; - } - if ( parent->skill[6] == SPELL_STEAL_WEAPON || parent->skill[6] == SHADOW_SPELLCAST ) - { - sprite = parent->sprite; + //new3 = std::chrono::high_resolution_clock::now(); + texture->load(surfaceNew, false, true); + //new4 = std::chrono::high_resolution_clock::now(); + if ( surfaceOld ) { + SDL_UnlockSurface(surfaceOld); + if ( cacheType == CACHE_NONE ) + { + SDL_FreeSurface(surfaceOld); + } + } + //new5 = std::chrono::high_resolution_clock::now(); + SDL_UnlockSurface(surfaceNew); + surfaceOld = surfaceNew; + + //new6 = std::chrono::high_resolution_clock::now(); + + prevData.r = red; + prevData.g = green; + prevData.b = blue; + prevData.a = alpha; + prevData.radMax = radius; + prevData.radMin = std::max(0, radius - gradientSize) + 0.0; + prevData.size = size; + //t3 = std::chrono::high_resolution_clock::now(); } - else if ( parent->skill[6] == SPELL_DRAIN_SOUL ) - { - if ( c == 0 || c == 3 ) - { - sprite = parent->sprite; + else { + if ( surfaceOld ) { + SDL_UnlockSurface(surfaceOld); } - else + SDL_UnlockSurface(surfaceNew); + if ( cacheType == CACHE_NONE ) { - sprite = 599; + SDL_FreeSurface(surfaceNew); } + surfaceNew = nullptr; + //t4 = std::chrono::high_resolution_clock::now(); } - else if ( parent->skill[6] == SPELL_SUMMON ) - { - sprite = parent->sprite; - } - else if ( parent->skill[6] == SPELL_FEAR ) + } + //auto t7 = std::chrono::high_resolution_clock::now(); + + if ( ticks % ticksPerUpdate == 0 ) + { + if ( radius < radiusMax ) { - sprite = parent->sprite; + radius += framesPerTick; } - else if ( multiplayer == CLIENT ) + if ( radius >= radiusMax ) { - // client won't receive the sprite skill data in time, fix for this until a solution is found! - if ( sprite == 598 ) + if ( loop ) { - if ( c == 0 || c == 3 ) + if ( loopType == 1 ) // linger on max radius { - // drain HP particle - sprite = parent->sprite; + radius = radiusMax; + ++loopTicks; + if ( loopTicks >= loopTimer ) + { + radius = radiusMin; + loopTicks = 0; + } } else { - // drain MP particle - sprite = 599; + radius = radiusMin; } } + else + { + expired = true; + } } - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x; - entity->y = parent->y; - entity->z = 0; - entity->scalex = 0.9; - entity->scaley = 0.9; - entity->scalez = 0.9; - if ( sprite == 598 || sprite == 599 ) - { - entity->scalex = 0.5; - entity->scaley = 0.5; - entity->scalez = 0.5; - } - entity->parent = (parent->getUID()); - entity->yaw = parent->yaw; - if ( c == 0 ) - { - entity->vel_z = -speed; - entity->vel_x = speed * cos(entity->yaw + PI / 2); - entity->vel_y = speed * sin(entity->yaw + PI / 2); - entity->yaw += PI / 3; - entity->pitch -= PI / 6; - entity->fskill[2] = -(PI / 3) / 25; // yaw rate of change. - entity->fskill[3] = (PI / 6) / 25; // pitch rate of change. - } - else if ( c == 1 ) - { - entity->vel_z = -speed; - entity->vel_x = speed * cos(entity->yaw - PI / 2); - entity->vel_y = speed * sin(entity->yaw - PI / 2); - entity->yaw -= PI / 3; - entity->pitch -= PI / 6; - entity->fskill[2] = (PI / 3) / 25; // yaw rate of change. - entity->fskill[3] = (PI / 6) / 25; // pitch rate of change. - } - else if ( c == 2 ) - { - entity->vel_x = speed * cos(entity->yaw + PI / 2); - entity->vel_y = speed * sin(entity->yaw + PI / 2); - entity->vel_z = speed; - entity->yaw += PI / 3; - entity->pitch += PI / 6; - entity->fskill[2] = -(PI / 3) / 25; // yaw rate of change. - entity->fskill[3] = -(PI / 6) / 25; // pitch rate of change. - } - else if ( c == 3 ) - { - entity->vel_x = speed * cos(entity->yaw - PI / 2); - entity->vel_y = speed * sin(entity->yaw - PI / 2); - entity->vel_z = speed; - entity->yaw -= PI / 3; - entity->pitch += PI / 6; - entity->fskill[2] = (PI / 3) / 25; // yaw rate of change. - entity->fskill[3] = -(PI / 6) / 25; // pitch rate of change. - } + } - entity->skill[3] = c; // particle index - entity->fskill[0] = entity->vel_x; // stores the accumulated x offset from center - entity->fskill[1] = entity->vel_y; // stores the accumulated y offset from center - entity->skill[0] = 200; // lifetime - entity->skill[1] = 0; // direction outwards - entity->behavior = &actParticleSap; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - if ( multiplayer != CLIENT ) - { - entity_uids--; - } - entity->setUID(-3); + //auto t8 = std::chrono::high_resolution_clock::now(); + //static double maxTotal; + //if ( keystatus[SDLK_t] ) + //{ + // static double out1 = 0.0; + // static double out2 = 0.0; + // static double out3 = 0.0; + // static double out4 = 0.0; + // static double out5 = 0.0; + // static double out6 = 0.0; + // static double out7 = 0.0; + // out1 += 1000 * std::chrono::duration_cast>(new1 - t2).count(); + // out2 += 1000 * std::chrono::duration_cast>(new2 - new1).count(); + // out3 += 1000 * std::chrono::duration_cast>(new3 - new2).count(); + // out4 += 1000 * std::chrono::duration_cast>(new4 - new3).count(); + // out5 += 1000 * std::chrono::duration_cast>(new5 - new4).count(); + // out6 += 1000 * std::chrono::duration_cast>(new6 - new5).count(); + + // //out1 += 1000 * std::chrono::duration_cast>(t5 - t1).count(); + // //out2 += std::max(0.0, 1000 * std::chrono::duration_cast>(t3 - t2).count()); + // //out3 += std::max(0.0, 1000 * std::chrono::duration_cast>(t4 - t2).count()); + // //out4 += 1000 * std::chrono::duration_cast>(t5 - t5).count(); + // //out5 += 1000 * std::chrono::duration_cast>(t6 - t5).count(); + // //out6 += 1000 * std::chrono::duration_cast>(t7 - t6).count(); + // //out7 += 1000 * std::chrono::duration_cast>(t8 - t7).count(); + // double newMax = 1000 * std::chrono::duration_cast>(t8 - t1).count(); + // if ( newMax > maxTotal ) + // { + // printlog("new max: %4.5fms, old: %4.5fms | rad: %.2f", newMax, maxTotal, radius); + // } + // maxTotal = std::max(maxTotal, newMax); + // char debugOutput[1024]; + // snprintf(debugOutput, 1023, + // "t1: %4.5fms t2: %4.5fms t3: %4.5fms t4: %4.5fms t5: %4.5fms t6: %4.5fms t7: %4.5fms max: %4.5fms", + // out1, out2, out3, out4, out5, out6, out7, maxTotal); + // messagePlayer(0, MESSAGE_DEBUG, "%s", debugOutput); + //} + //else + //{ + // maxTotal = 0.0; + //} +} - if ( sprite == 977 ) // boomerang - { - entity->z = parent->z; - entity->scalex = 1.f; - entity->scaley = 1.f; - entity->scalez = 1.f; - entity->skill[0] = 175; - entity->fskill[2] = -((PI / 3) + (PI / 6)) / (150); // yaw rate of change over 3 seconds - entity->fskill[3] = 0.f; - entity->focalx = 2; - entity->focalz = 0.5; - entity->pitch = parent->pitch; - entity->yaw = parent->yaw; - entity->roll = parent->roll; +Entity* createParticleAOEIndicator(Entity* parent, real_t x, real_t y, real_t z, Uint32 lifetime, int size) +{ + Uint32 uid = 0; + if ( parent ) + { + uid = parent->getUID(); + } - entity->vel_x = 1 * cos(entity->yaw); - entity->vel_y = 1 * sin(entity->yaw); - int x = entity->x / 16; - int y = entity->y / 16; - if ( !map.tiles[(MAPLAYERS - 1) + y * MAPLAYERS + x * MAPLAYERS * map.height] ) - { - // no ceiling, bounce higher. - entity->vel_z = -0.4; - entity->skill[3] = 1; // high bounce. - } - else + Entity* entity = newEntity(222, 1, map.entities, nullptr); //Sprite entity. + entity->x = x; + entity->y = y; + entity->z = z + 7.49; + static ConsoleVariable cvar_sprite_aoe_indicator_scale("/sprite_aoe_indicator_scale", 2.0); + static ConsoleVariable cvar_sprite_aoe_indicator_rotate("/sprite_aoe_indicator_rotate", 0.0); + static ConsoleVariable cvar_sprite_aoe_indicator_alpha("/sprite_aoe_indicator_alpha", 0.5); + static ConsoleVariable cvar_sprite_aoe_indicator_alpha_glow("/sprite_aoe_indicator_alpha_glow", 0.0625); + entity->ditheringDisabled = true; + entity->flags[SPRITE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + //entity->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); + entity->scalex = *cvar_sprite_aoe_indicator_scale; + entity->scaley = *cvar_sprite_aoe_indicator_scale; + entity->behavior = &actSprite; + entity->yaw = parent ? floor(parent->yaw / (PI / 2)) * PI / 2 : 0.0; + entity->pitch = 0; + entity->roll = -PI / 2; + entity->skill[0] = 1; + entity->skill[1] = 1; + entity->skill[2] = lifetime; + entity->fskill[0] = *cvar_sprite_aoe_indicator_rotate; + entity->fskill[1] = *cvar_sprite_aoe_indicator_alpha; + entity->fskill[2] = *cvar_sprite_aoe_indicator_alpha; // alpha + entity->fskill[3] = *cvar_sprite_aoe_indicator_alpha_glow; + entity->actSpriteUseAlpha = 1; // use alpha + entity->actSpriteNoBillboard = 1; // no billboard + entity->actSpriteCheckParentExists = uid; + entity->flags[ENTITY_SKIP_CULLING] = true; // ignore LOS culling + entity->actSpriteUseCustomSurface = AOEIndicators_t::createIndicator(4, size, size * 2 + 4, lifetime); + entity->actSpriteFollowUID = uid; // follow parent + entity->z -= 2 * (entity->actSpriteUseCustomSurface % 50 / 10000.0); + entity->setEntityString("aoe_indicator"); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + return entity; +} + +Entity* createFloorMagic(ParticleTimerEffect_t::EffectType particleType, int sprite, real_t x, real_t y, real_t z, real_t dir, Uint32 lifetime) +{ + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Sprite entity. + entity->x = x; + entity->y = y; + entity->z = z; + entity->yaw = dir; + entity->scalex = 0.25; + entity->scaley = 0.25; + entity->scalez = 0.25; + entity->skill[0] = lifetime; + entity->actfloorMagicType = particleType; + entity->behavior = &actParticleFloorMagic; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->skill[2] = -18; + return entity; +} + +Entity* createParticleRoot(int sprite, real_t x, real_t y, real_t z, real_t dir, Uint32 lifetime) +{ + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Sprite entity. + entity->x = x; + entity->y = y; + entity->z = z; + entity->yaw = dir; + entity->scalex = 0.25; + entity->scaley = 0.25; + entity->scalez = 0.25; + entity->sizex = 2; + entity->sizey = 2; + entity->skill[0] = lifetime; + entity->behavior = &actParticleRoot; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[NOUPDATE] = true; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + TileEntityList.addEntity(*entity); + entity->setUID(-3); + return entity; +} + +void createMushroomSpellEffect(Entity* caster, real_t x, real_t y) +{ + for ( int i = 0; i < 8; ++i ) + { + Entity* fx = createParticleAestheticOrbit(caster, 1885, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_MUSHROOM_SPELL); + fx->x = caster ? caster->x : x; + fx->y = caster ? caster->y : y; + fx->actmagicOrbitDist = 32; + fx->yaw = (caster ? caster->yaw : 0.0) + (i * PI / 4.0); + fx->pitch = -PI; + fx->fskill[4] = fx->yaw; + + Entity* gib = nullptr; + if ( multiplayer == CLIENT ) + { + gib = spawnGibClient(x, y, caster ? caster->z : 0, 1885); + } + else + { + gib = spawnGib(caster, 1885); + } + + if ( gib ) + { + if ( caster ) { - entity->vel_z = -0.08; + gib->z = caster->z; } - entity->yaw += PI / 3; + gib->vel_x = 1.5 * cos(fx->yaw); + gib->vel_y = 1.5 * sin(fx->yaw); + gib->lightBonus = vec4{ 0.25f, 0.25f, 0.25f, 0.f }; } - else + } + + if ( Entity* fx = createParticleAOEIndicator(caster, caster ? caster->x : x, caster ? caster->y : y, 0.0, TICKS_PER_SECOND * 2, 32) ) + { + fx->actSpriteFollowUID = caster ? caster->getUID() : 0; + fx->actSpriteCheckParentExists = 0; + //fx->scalex = 0.8; + //fx->scaley = 0.8; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) { - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); + //indicator->arc = PI / 2; + indicator->indicatorColor = makeColorRGB(0, 145, 16); + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.95; } } + playSoundPosLocal(x, y, 169, 128); + playSoundPosLocal(x, y, 717 + local_rng.rand() % 3, 128); } -void createParticleDropRising(Entity* parent, int sprite, double scale) +Entity* createVortexMagic(int sprite, real_t x, real_t y, real_t z, real_t dir, Uint32 lifetime) { - if ( !parent ) + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Sprite entity. + entity->x = x; + entity->y = y; + entity->z = z; + entity->yaw = dir; + entity->scalex = 0.25; + entity->scaley = 0.25; + entity->scalez = 0.25; + entity->skill[0] = lifetime; + entity->behavior = &actParticleVortex; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + //entity->flags[UPDATENEEDED] = true; + return entity; +} + +void actParticleRoot(Entity* my) +{ + int x = my->x / 16; + int y = my->y / 16; + + if ( x <= 0 || x >= map.width - 1 || y <= 0 || y >= map.height - 1 ) { + my->flags[INVISIBLE] = true; + list_RemoveNode(my->mynode); return; } - for ( int c = 0; c < 50; c++ ) + if ( my->sprite != 2200 ) // void root { - // shoot drops to the sky - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x - 4 + local_rng.rand() % 9; - entity->y = parent->y - 4 + local_rng.rand() % 9; - entity->z = 7.5 + local_rng.rand() % 50; - entity->vel_z = -1; - //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->particleDuration = 10 + local_rng.rand() % 50; - entity->scalex *= scale; - entity->scaley *= scale; - entity->scalez *= scale; - entity->behavior = &actParticleDot; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( !map.tiles[mapIndex] || swimmingtiles[map.tiles[mapIndex]] || lavatiles[map.tiles[mapIndex]] || map.tiles[OBSTACLELAYER + mapIndex] ) { - entity_uids--; + my->flags[INVISIBLE] = true; + list_RemoveNode(my->mynode); + return; } - entity->setUID(-3); } -} -Entity* createParticleTimer(Entity* parent, int duration, int sprite) -{ - Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Timer entity. - entity->sizex = 1; - entity->sizey = 1; - if ( parent ) - { - entity->x = parent->x; - entity->y = parent->y; - entity->parent = (parent->getUID()); - } - entity->behavior = &actParticleTimer; - entity->particleTimerDuration = duration; - entity->flags[INVISIBLE] = true; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - /*if ( multiplayer != CLIENT ) + if ( my->actmagicDelayMove > 0 ) { - entity_uids--; + my->flags[INVISIBLE] = true; + --my->actmagicDelayMove; + return; } - entity->setUID(-3);*/ - return entity; -} + my->flags[INVISIBLE] = false; -void actParticleErupt(Entity* my) -{ if ( PARTICLE_LIFE < 0 ) { - list_RemoveNode(my->mynode); - return; + my->scalex -= 0.1; + my->scaley = my->scalex; + my->scalez = my->scalex; + if ( my->scalex < 0.0 ) + { + list_RemoveNode(my->mynode); + return; + } } else { - // particles jump up from the ground then back down again. - --PARTICLE_LIFE; - my->x += my->vel_x * cos(my->yaw); - my->y += my->vel_y * sin(my->yaw); - my->scalex *= 0.99; - my->scaley *= 0.99; - my->scalez *= 0.99; - if ( *cvar_magic_fx_use_vismap && !intro ) + if ( my->skill[6] == 1 ) // check parent exists instead of countdown { - int x = my->x / 16.0; - int y = my->y / 16.0; - if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + if ( !uidToEntity(my->parent) ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) - { - if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) - { - spawnMagicParticle(my); - break; - } - } + PARTICLE_LIFE = -1; } } else { - spawnMagicParticle(my); + --PARTICLE_LIFE; } - if ( my->skill[1] == 0 ) // rising + if ( my->fskill[1] < 1.0 ) { - my->z += my->vel_z; - my->vel_z *= 0.8; - my->pitch = std::min(my->pitch + my->fskill[0], PI / 2); - my->fskill[0] = std::max(my->fskill[0] * 0.85, 0.05); - if ( my->vel_z > -0.02 ) + my->fskill[1] += 0.05; + my->fskill[1] = std::min(my->fskill[1], 1.0); + if ( my->fskill[1] >= 1.0 ) + { + my->skill[5] = my->ticks; // animation tick start + } + } + my->roll = -PI + PI * sin(my->fskill[1] * PI / 2); + if ( my->skill[1] == 0 ) + { + my->scalex = std::min(my->scalex + 0.1, 1.0); + my->scaley = my->scalex; + my->scalez = my->scalex; + if ( my->scalex >= 1.0 ) { my->skill[1] = 1; } } - else // falling + else if ( my->skill[1] == 1 ) { - my->pitch = std::min(my->pitch + my->fskill[0], 15 * PI / 16); - my->fskill[0] = std::min(my->fskill[0] * (1 / 0.99), 0.1); - my->z -= my->vel_z; - my->vel_z *= (1 / 0.8); - my->vel_z = std::max(my->vel_z, -0.8); + my->fskill[0] += my->skill[4] == 0 ? 0.5 : 0.05; + if ( my->fskill[0] >= 2 * PI ) + { + my->fskill[0] = 0.0; + my->skill[3]++; + if ( my->skill[3] == 3 ) + { + my->skill[4] = my->skill[4] > 0 ? 0 : 1; + } + } + my->scalex = 1.0 + 0.05 * sin(my->fskill[0]); + my->scaley = my->scalex; + my->scalez = my->scalex; } - } -} -void actParticleTimer(Entity* my) -{ - if ( PARTICLE_LIFE < 0 ) - { - if ( multiplayer != CLIENT ) + if ( (my->ticks - my->skill[5]) % 50 == 0 ) { - if ( my->particleTimerEndAction == PARTICLE_EFFECT_INCUBUS_TELEPORT_STEAL ) + if ( true ) { - // teleport to random location spell. - Entity* parent = uidToEntity(my->parent); - if ( parent ) + int sprite = 226; + if ( my->sprite == 2200 ) // void root { - createParticleErupt(parent, my->particleTimerEndSprite); - if ( parent->teleportRandom() ) - { - // teleport success. - if ( multiplayer == SERVER ) - { - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - } - } + sprite = 261; } + Entity* particle = spawnMagicParticleCustom(my, sprite, .7, 1.0); + particle->flags[SPRITE] = true; + particle->vel_z = -0.25; + particle->z = 6.0; } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_INCUBUS_TELEPORT_TARGET ) + + if ( false ) { - // teleport to target spell. - Entity* parent = uidToEntity(my->parent); - Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); - if ( parent && target ) + Entity* entity = newEntity(227, 1, map.entities, nullptr); //Sprite entity. + entity->x = my->x; + entity->y = my->y; + entity->z = 6.0; + entity->ditheringDisabled = true; + entity->flags[SPRITE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + entity->behavior = &actSprite; + entity->skill[0] = 1; + entity->skill[1] = 6; + entity->skill[2] = 4; + entity->vel_z = -0.25; + if ( multiplayer != CLIENT ) { - createParticleErupt(parent, my->particleTimerEndSprite); - if ( parent->teleportAroundEntity(target, my->particleTimerVariable1) ) - { - // teleport success. - if ( multiplayer == SERVER ) - { - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - } - } + entity_uids--; } + entity->setUID(-3); } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_TELEPORT_PULL ) + + if ( false ) { - // teleport to target spell. - Entity* parent = uidToEntity(my->parent); - Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); - if ( parent && target ) + Entity* entity = newEntity(233, 1, map.entities, nullptr); //Sprite entity. + entity->x = my->x; + entity->y = my->y; + entity->z = 6.0; + entity->ditheringDisabled = true; + entity->flags[SPRITE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + entity->behavior = &actSprite; + entity->skill[0] = 1; + entity->skill[1] = 12; + entity->skill[2] = 4; + entity->vel_z = -0.25; + if ( multiplayer != CLIENT ) { - real_t oldx = target->x; - real_t oldy = target->y; - my->flags[PASSABLE] = true; - int tx = static_cast(std::floor(my->x)) >> 4; - int ty = static_cast(std::floor(my->y)) >> 4; - if ( !target->isBossMonster() && - target->teleport(tx, ty) ) - { - // teleport success. - if ( parent->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(0, 255, 0); - if ( target->getStats() ) - { - messagePlayerMonsterEvent(parent->skill[2], color, *(target->getStats()), Language::get(3450), Language::get(3451), MSG_COMBAT); - } - } - if ( target->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(255, 255, 255); - messagePlayerColor(target->skill[2], MESSAGE_COMBAT, color, Language::get(3461)); - } - real_t distance = sqrt((target->x - oldx) * (target->x - oldx) + (target->y - oldy) * (target->y - oldy)) / 16.f; - //real_t distance = (entityDist(parent, target)) / 16; - createParticleErupt(target, my->particleTimerEndSprite); - int durationToStun = 0; - if ( distance >= 2 ) - { - durationToStun = 25 + std::min((distance - 4) * 10, 50.0); - } - if ( target->behavior == &actMonster ) - { - if ( durationToStun > 0 && target->setEffect(EFF_DISORIENTED, true, durationToStun, false) ) - { - int numSprites = std::min(3, durationToStun / 25); - for ( int i = 0; i < numSprites; ++i ) - { - spawnFloatingSpriteMisc(134, target->x + (-4 + local_rng.rand() % 9) + cos(target->yaw) * 2, - target->y + (-4 + local_rng.rand() % 9) + sin(target->yaw) * 2, target->z + local_rng.rand() % 4); - } - } - target->monsterReleaseAttackTarget(); - target->lookAtEntity(*parent); - target->monsterLookDir += (PI - PI / 4 + (local_rng.rand() % 10) * PI / 40); - } - else if ( target->behavior == &actPlayer ) - { - durationToStun = std::max(50, durationToStun); - target->setEffect(EFF_DISORIENTED, true, durationToStun, false); - int numSprites = std::min(3, durationToStun / 50); - for ( int i = 0; i < numSprites; ++i ) - { - spawnFloatingSpriteMisc(134, target->x + (-4 + local_rng.rand() % 9) + cos(target->yaw) * 2, - target->y + (-4 + local_rng.rand() % 9) + sin(target->yaw) * 2, target->z + local_rng.rand() % 4); - } - Uint32 color = makeColorRGB(255, 255, 255); - messagePlayerColor(target->skill[2], MESSAGE_COMBAT, color, Language::get(3462)); - } - if ( multiplayer == SERVER ) - { - serverSpawnMiscParticles(target, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - } - } + entity_uids--; } + entity->setUID(-3); + } + } + } +} + +void actParticleVortex(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + return; + } + else + { + --PARTICLE_LIFE; + if ( my->skill[1] == 0 ) + { + my->skill[1] = 1; + my->fskill[0] = my->x; + my->fskill[1] = my->y; + } + + + static ConsoleVariable cvar_particle_speed("/particle_speed", 1.0); + static ConsoleVariable cvar_particle_z("/particle_z", 1.0); + static ConsoleVariable cvar_particle_yaw("/particle_yaw", 1.0); + static ConsoleVariable cvar_particle_radius("/particle_radius", 1.0); + static ConsoleVariable cvar_particle_scale("/particle_scale", 1.0); + real_t dist = std::max(0.25, (1.0 + cos(PI + PI * my->fskill[3])) / 2); + my->x = my->fskill[0] + 4.0 * *cvar_particle_radius * dist * cos(my->yaw); + my->y = my->fskill[1] + 4.0 * *cvar_particle_radius * dist * sin(my->yaw); + my->z = my->z - 0.05 * *cvar_particle_z; + my->yaw += 0.05 * *cvar_particle_yaw; + real_t scale = *cvar_particle_scale * (1.0 + cos(PI + PI * my->fskill[2])) / 2; + if ( my->z <= 0 ) //-8.0 ) + { + my->scalex -= 0.1; + my->scaley -= 0.1; + my->scalez -= 0.1; + if ( my->scalex <= 0.0 ) + { + list_RemoveNode(my->mynode); + return; } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_PORTAL_SPAWN ) + } + else + { + my->scalex = scale; + my->scaley = scale; + my->scalez = scale; + } + my->fskill[2] = std::min(1.0, 0.005 * *cvar_particle_speed + my->fskill[2]); // scale + my->fskill[3] = std::min(1.0, 0.01 * *cvar_particle_speed + my->fskill[3]); // dist from center + + Entity* particle = spawnMagicParticleCustom(my, my->sprite, scale * .7, 10.0); + particle->ditheringDisabled = true; + } +} + +void actParticleFloorMagic(Entity* my) +{ + Entity* parentTimer = uidToEntity(my->parent); + if ( PARTICLE_LIFE < 0 || (multiplayer != CLIENT && !parentTimer) ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + + bool doEffect = my->actmagicDelayMove == 0; + if ( my->actmagicDelayMove > 0 ) + { + --my->actmagicDelayMove; + my->flags[INVISIBLE] = true; + if ( my->actmagicDelayMove == 0 ) + { + spawnGib(my); + my->flags[INVISIBLE] = false; + my->flags[UPDATENEEDED] = true; + } + } + + if ( doEffect ) + { + if ( multiplayer != CLIENT ) + { + --PARTICLE_LIFE; + my->x += my->vel_x * cos(my->yaw); + my->y += my->vel_y * sin(my->yaw); + + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF_SUSTAIN ) { - Entity* parent = uidToEntity(my->parent); - if ( parent ) + PARTICLE_LIFE = std::max(TICKS_PER_SECOND, PARTICLE_LIFE); // wait for parent timer to expire + } + } + else + { + if ( my->actfloorMagicClientReceived == 1 ) + { + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT ) { - parent->flags[INVISIBLE] = false; - serverUpdateEntityFlag(parent, INVISIBLE); - playSoundEntity(parent, 164, 128); + --PARTICLE_LIFE; // client allowed decay } - spawnExplosion(my->x, my->y, 0); } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_SUMMON_MONSTER - || my->particleTimerEndAction == PARTICLE_EFFECT_DEVIL_SUMMON_MONSTER ) + } + + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE_VOID + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF_SUSTAIN + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_PATH ) + { + if ( my->scalex < 1.0 ) { - playSoundEntity(my, 164, 128); - spawnExplosion(my->x, my->y, -4.0); - bool forceLocation = false; - if ( my->particleTimerEndAction == PARTICLE_EFFECT_DEVIL_SUMMON_MONSTER && - !map.tiles[static_cast(my->y / 16) * MAPLAYERS + static_cast(my->x / 16) * MAPLAYERS * map.height] ) + my->scalex = std::min(my->scalex * 1.25, 1.0); + my->scaley = std::min(my->scaley * 1.25, 1.0); + my->scalez = std::min(my->scalez * 1.25, 1.0); + } + else + { + my->fskill[0] += 0.05; + my->scalex = 1.025 - 0.025 * sin(my->fskill[0] + PI / 2); + my->scaley = my->scalex; + my->scalez = my->scalex; + } + } + else + { + my->scalex = std::min(my->scalex * 1.25, 1.0); + my->scaley = std::min(my->scaley * 1.25, 1.0); + my->scalez = std::min(my->scalez * 1.25, 1.0); + } + } + + if ( my->ticks == 1 ) // lightning bolt + { + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT ) + { + if ( Entity* particle = spawnMagicParticleCustom(my, 1764, my->scalex * 1.0, 1) ) + { + particle->z = 6.0; + particle->x = my->x; + particle->y = my->y; + particle->yaw = local_rng.rand() % 360 * PI / 180; + particle->vel_x = 0.05 * cos(particle->yaw); + particle->vel_y = 0.05 * sin(particle->yaw); + particle->ditheringDisabled = true; + } + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ICE_WAVE ) + { + //if ( Entity* gib = multiplayer == CLIENT ? spawnGibClient(my,) : spawnGib(my) ) + //{ + // gib->sprite = 1374; + //} + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH ) + { + if ( Entity* gib = multiplayer == CLIENT ? spawnGibClient(my->x, my->y, my->z, -1) : spawnGib(my) ) + { + gib->sprite = 78; + } + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_SPORES + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM ) + { + my->flags[INVISIBLE] = true; + my->scalex = 1.0; + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE_VOID ) + { + BaronyRNG rng; + Uint32 rootSeed = my->getUID(); + rng.seedBytes(&rootSeed, sizeof(rootSeed)); + + std::vector locations = + { + 0 * PI / 4, + 1 * PI / 4, + 2 * PI / 4, + 3 * PI / 4, + 4 * PI / 4, + 5 * PI / 4, + 6 * PI / 4, + 7 * PI / 4 + }; + + int numLocations = locations.size(); + + real_t dist = 16.0; + while ( locations.size() >= 4 ) + { + int pick = rng.rand() % locations.size(); + float yaw = locations[pick]; + /*if ( i % 2 == 1 ) { - if ( my->particleTimerVariable1 == SHADOW || my->particleTimerVariable1 == CREATURE_IMP ) - { - forceLocation = true; - } - } - Entity* monster = summonMonster(static_cast(my->particleTimerVariable1), my->x, my->y, forceLocation); - if ( monster ) + yaw += PI / 8; + }*/ + int sprite = 1766; + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE_VOID ) { - if ( Stat* monsterStats = monster->getStats() ) - { - if ( my->parent != 0 ) - { - Entity* parent = uidToEntity(my->parent); - if ( parent ) - { - if ( parent->getRace() == LICH_ICE ) - { - //monsterStats->leader_uid = my->parent; - switch ( monsterStats->type ) - { - case AUTOMATON: - strcpy(monsterStats->name, "corrupted automaton"); - monsterStats->EFFECTS[EFF_CONFUSED] = true; - monsterStats->EFFECTS_TIMERS[EFF_CONFUSED] = -1; - break; - default: - break; - } - - if ( Stat* parentStats = parent->getStats() ) - { - monsterStats->monsterNoDropItems = 1; - monsterStats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS] = 1; - monsterStats->LVL = 5; - std::string lich_num_summons = parentStats->getAttribute("lich_num_summons"); - if ( lich_num_summons == "" ) - { - parentStats->setAttribute("lich_num_summons", "1"); - } - else - { - int numSummons = std::stoi(parentStats->getAttribute("lich_num_summons")); - if ( numSummons >= 25 ) - { - monsterStats->MISC_FLAGS[STAT_FLAG_XP_PERCENT_AWARD] = 1; - } - ++numSummons; - parentStats->setAttribute("lich_num_summons", std::to_string(numSummons)); - } - } - } - else if ( parent->getRace() == DEVIL ) - { - monsterStats->monsterNoDropItems = 1; - monsterStats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS] = 1; - monsterStats->LVL = 5; - if ( parent->monsterDevilNumSummons >= 25 ) - { - monsterStats->MISC_FLAGS[STAT_FLAG_XP_PERCENT_AWARD] = 1; - } - if ( my->particleTimerVariable2 >= 0 - && players[my->particleTimerVariable2] && players[my->particleTimerVariable2]->entity ) - { - monster->monsterAcquireAttackTarget(*(players[my->particleTimerVariable2]->entity), MONSTER_STATE_ATTACK); - } - } - } - } - } + sprite = 2200; } + Entity* root = createParticleRoot(sprite, my->x + dist * cos(yaw), my->y + dist * sin(yaw), + 7.5, rng.rand() % 360 * (PI / 180.0), PARTICLE_LIFE); + root->focalz = -0.5; + int roll = rng.rand() % 8; + real_t angle = (pick / (float)numLocations) * PI + ((roll) / 8.0) * PI; + real_t xoffset = 4.0 * sin(angle); + xoffset += -2.0 + 4.0 * (rng.rand() % 16) / 16.0; + root->x += xoffset * cos(root->yaw + PI / 2); + root->y += xoffset * sin(root->yaw + PI / 2); + root->actmagicDelayMove = (TICKS_PER_SECOND / 8) * (locations.size()); + root->skill[0] -= root->actmagicDelayMove; + root->parent = my->getUID(); + locations.erase(locations.begin() + pick); } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_SPELL_SUMMON ) + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_PATH ) + { + BaronyRNG rng; + Uint32 rootSeed = my->getUID(); + rng.seedBytes(&rootSeed, sizeof(rootSeed)); + + std::vector locations = { + 0 * PI / 8, + 1 * PI / 8, + 2 * PI / 8, + 3 * PI / 8, + -1 * PI / 8, + -2 * PI / 8, + -3 * PI / 8, + -4 * PI / 8, + 0 * PI / 8, + 1 * PI / 8, + 2 * PI / 8, + 3 * PI / 8, + -1 * PI / 8, + -2 * PI / 8, + -3 * PI / 8, + -4 * PI / 8 + }; + int numLocations = locations.size(); + + real_t dist = 8.0; + while ( locations.size() ) { - //my->removeLightField(); + if ( dist >= 80.0 ) + { + break; + } + int pick = rng.rand() % locations.size(); + float yaw = my->yaw + locations[pick] / 64; + dist += 8.0; + /*if ( i % 2 == 1 ) + { + yaw += PI / 8; + }*/ + Entity* root = createParticleRoot(1766, my->x + dist * cos(yaw), my->y + dist * sin(yaw), + 7.5, rng.rand() % 360 * (PI / 180.0), PARTICLE_LIFE); + root->focalz = -0.5; + int roll = rng.rand() % 8; + real_t angle = (pick / (float)numLocations) * PI + ((roll) / 8.0) * PI; + real_t xoffset = 4.0 * sin(angle); + xoffset += -2.0 + 4.0 * (rng.rand() % 16) / 16.0; + root->x += xoffset * cos(root->yaw + PI / 2); + root->y += xoffset * sin(root->yaw + PI / 2); + root->actmagicDelayMove = (TICKS_PER_SECOND / 8) * ((numLocations - locations.size())); + root->skill[0] -= root->actmagicDelayMove; + root->parent = my->getUID(); + locations.erase(locations.begin() + pick); } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_SHADOW_TELEPORT ) + /*int roll = local_rng.rand() % 8; + for ( int i = 0; i < 16; ++i ) { - // teleport to target spell. - Entity* parent = uidToEntity(my->parent); - if ( parent ) + real_t angle = (i / 8.0) * PI + ((roll) / 8.0) * PI; + real_t yawOffset = cos(angle) + ((local_rng.rand() % 4) / 4.0) * 2 * PI; + real_t dist = 40.0 * (0.25 + (0.75 * i / 16.0)); + Entity* root = createParticleRoot(1766, my->x + dist * cos(yawOffset), my->y + dist * sin(yawOffset), + 7.5, local_rng.rand() % 360 * (PI / 180.0), PARTICLE_LIFE); + root->focalz = -0.5; + real_t xoffset = 8.0 * sin(angle); + xoffset += 2.0 * (local_rng.rand() % 16) / 16.0; + root->x += xoffset * cos(my->yaw + PI / 2); + root->y += xoffset * sin(my->yaw + PI / 2); + + root->actmagicDelayMove = (TICKS_PER_SECOND / 4) * i; + root->skill[0] -= root->actmagicDelayMove; + }*/ + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF_SUSTAIN ) + { + BaronyRNG rng; + Uint32 rootSeed = my->getUID(); + rng.seedBytes(&rootSeed, sizeof(rootSeed)); + for ( int i = 0; i < 2; ++i ) + { + // circular + std::vector locations = { - if ( parent->monsterSpecialState == SHADOW_TELEPORT_ONLY ) - { - //messagePlayer(0, "Resetting shadow's monsterSpecialState!"); - parent->monsterSpecialState = 0; - serverUpdateEntitySkill(parent, 33); // for clients to keep track of animation - } - } - Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); - if ( parent ) + 0 * PI / 8, + 1 * PI / 8, + 2 * PI / 8, + 3 * PI / 8, + 4 * PI / 8, + 5 * PI / 8, + 6 * PI / 8, + 7 * PI / 8, + 8 * PI / 8, + 9 * PI / 8, + 10 * PI / 8, + 11 * PI / 8, + 12 * PI / 8, + 13 * PI / 8, + 14 * PI / 8, + 15 * PI / 8 + }; + if ( i == 1 ) { - bool teleported = false; - createParticleErupt(parent, my->particleTimerEndSprite); - if ( target ) - { - teleported = parent->teleportAroundEntity(target, my->particleTimerVariable1); - } - else + locations = { - teleported = parent->teleportRandom(); - } + 1 * PI / 8, + 3 * PI / 8, + 5 * PI / 8, + 7 * PI / 8, + 9 * PI / 8, + 11 * PI / 8, + 13 * PI / 8, + 15 * PI / 8 + }; + } + int numLocations = locations.size(); - if ( teleported ) + real_t dist = 24.0 - 8.0 * i; + while ( locations.size() ) + { + int pick = rng.rand() % locations.size(); + float yaw = locations[pick]; + Entity* root = createParticleRoot(1766, my->x + dist * cos(yaw), my->y + dist * sin(yaw), + 7.5, rng.rand() % 360 * (PI / 180.0), PARTICLE_LIFE); + root->focalz = -0.5; + int roll = rng.rand() % 8; + real_t angle = (pick / (float)numLocations) * PI + ((roll) / 8.0) * PI; + real_t xoffset = 4.0 * sin(angle); + xoffset += -2.0 + 4.0 * (rng.rand() % 16) / 16.0; + root->x += xoffset * cos(root->yaw + PI / 2); + root->y += xoffset * sin(root->yaw + PI / 2); + root->actmagicDelayMove = (TICKS_PER_SECOND / 8) * (locations.size() + i * 8); + root->skill[0] -= root->actmagicDelayMove; + root->parent = my->getUID(); + locations.erase(locations.begin() + pick); + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF_SUSTAIN ) { - // teleport success. - if ( multiplayer == SERVER ) - { - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - } + root->skill[6] = 1; // check parent exists instead of countdown } } } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_LICHFIRE_TELEPORT_STATIONARY ) + } + } + + bool doParticle = false; + if ( my->actmagicDelayMove == 0 && my->actmagicNoParticle == 0 ) + { + if ( *cvar_magic_fx_use_vismap && !intro ) + { + int x = my->x / 16.0; + int y = my->y / 16.0; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) { - // teleport to fixed location spell. - node_t* node; - int c = 0 + local_rng.rand() % 3; - Entity* target = nullptr; - for ( node = map.entities->first; node != nullptr; node = node->next ) + for ( int i = 0; i < MAXPLAYERS; ++i ) { - target = (Entity*)node->element; - if ( target->behavior == &actDevilTeleport ) + if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) { - if ( (c == 0 && target->sprite == 72) - || (c == 1 && target->sprite == 73) - || (c == 2 && target->sprite == 74) ) - { - break; - } + doParticle = true; + break; } } - Entity* parent = uidToEntity(my->parent); - if ( parent && target ) + } + } + else + { + doParticle = true; + } + } + + if ( doEffect ) + { + if ( multiplayer != CLIENT ) + { + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT ) + { + if ( Entity* target = uidToEntity(my->actmagicOrbitHitTargetUID1) ) { - createParticleErupt(parent, my->particleTimerEndSprite); - if ( parent->teleport(target->x / 16, target->y / 16) ) - { - // teleport success. - if ( multiplayer == SERVER ) - { - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - } - } + my->x = target->x; + my->y = target->y; } } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_LICH_TELEPORT_ROAMING ) + } + + if ( multiplayer != CLIENT && my->scalex >= .9 ) + { + my->skill[1]++; // active ticks + Entity* caster = uidToEntity(parentTimer->parent); + + int radius = 1; + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF_SUSTAIN + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE ) + { + radius = 2; + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_PATH ) + { + radius = 5; + } + + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM ) { - bool teleported = false; - // teleport to target spell. - node_t* node; - Entity* parent = uidToEntity(my->parent); - Entity* target = nullptr; - if ( parent ) + if ( local_rng.rand() % 200 == 0 || PARTICLE_LIFE == 1 ) { - for ( node = map.entities->first; node != nullptr; node = node->next ) + bool skip = false; + if ( parentTimer->particleTimerVariable4 != 0 ) { - target = (Entity*)node->element; - if ( target->behavior == &actDevilTeleport - && target->sprite == 128 ) + if ( !(PARTICLE_LIFE == 1) ) { - break; // found specified center of map + if ( uidToEntity(parentTimer->particleTimerVariable4) ) + { + skip = true; // skip while projectile alive + } } } - - if ( target ) + if ( !skip && parentTimer->particleTimerVariable3 == 0 ) { - createParticleErupt(parent, my->particleTimerEndSprite); - teleported = parent->teleport((target->x / 16) - 11 + local_rng.rand() % 23, (target->y / 16) - 11 + local_rng.rand() % 23); - - if ( teleported ) + if ( Entity* breakable = Entity::createBreakableCollider(EditorEntityData_t::getColliderIndexFromName("mushroom_spell_fragile"), + my->x, my->y, caster) ) { - // teleport success. - if ( multiplayer == SERVER ) + parentTimer->particleTimerVariable3 = 1; + breakable->colliderSpellEvent = 1 + local_rng.rand() % 5; + if ( !caster && achievementObserver.checkUidIsFromPlayer(parentTimer->parent) >= 0 || (caster && caster->behavior == &actPlayer) ) { - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + breakable->colliderCreatedParent = parentTimer->parent; + breakable->colliderSpellEvent = 6; + } + if ( breakable->colliderSpellEvent > 0 && breakable->colliderSpellEvent < 1000 ) + { + breakable->colliderSpellEvent += 1000; } + breakable->colliderSetServerSkillOnSpawned(); // to update the variables modified from create() } } } } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_LICHICE_TELEPORT_STATIONARY ) + + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, radius); + for ( auto it : entLists ) { - // teleport to fixed location spell. - node_t* node; - Entity* target = nullptr; - for ( node = map.entities->first; node != nullptr; node = node->next ) + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE_VOID ) { - target = (Entity*)node->element; - if ( target->behavior == &actDevilTeleport - && target->sprite == 128 ) - { - break; - } + break; } - Entity* parent = uidToEntity(my->parent); - if ( parent && target ) + + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) { - createParticleErupt(parent, my->particleTimerEndSprite); - if ( parent->teleport(target->x / 16, target->y / 16) ) + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic()) + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH /*hits furniture*/ + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT /*hits furniture*/ ) { - // teleport success. - if ( multiplayer == SERVER ) + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT ) { - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + if ( entity->behavior != &actMonster + && entity->behavior != &actPlayer + && entity->behavior != &actDoor + && entity->behavior != &::actIronDoor + && !(entity->isDamageableCollider() && entity->isColliderDamageableByMagic()) + && !entity->isInertMimic() + && entity->behavior != &::actChest + && entity->behavior != &::actFurniture ) + { + continue; + } } - parent->lichIceCreateCannon(); - } - } - } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_SHRINE_TELEPORT ) - { - // teleport to target spell. - Entity* toTeleport = uidToEntity(my->particleTimerVariable2); - Entity* target = uidToEntity(static_cast(my->particleTimerTarget)); - if ( toTeleport && target ) - { - bool teleported = false; - createParticleErupt(toTeleport, my->particleTimerEndSprite); - serverSpawnMiscParticles(toTeleport, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - teleported = toTeleport->teleportAroundEntity(target, my->particleTimerVariable1); - if ( teleported ) - { - createParticleErupt(toTeleport, my->particleTimerEndSprite); - // teleport success. - if ( multiplayer == SERVER ) + + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_SPORES + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM ) { - serverSpawnMiscParticles(toTeleport, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + auto particleEmitterHitPropsTimer = getParticleEmitterHitProps(my->parent, entity); + if ( !particleEmitterHitPropsTimer ) + { + continue; + } + if ( particleEmitterHitPropsTimer->hits > 0 ) + { + if ( parentTimer && parentTimer->particleTimerVariable4 > 0 + && particleEmitterHitPropsTimer->hits > 0 && (ticks - particleEmitterHitPropsTimer->tick) >= TICKS_PER_SECOND ) + { + // allow re-apply + } + else + { + continue; + } + } + + if ( caster && caster->behavior == &actMonster ) + { + if ( caster == entity || caster->checkFriend(entity) ) + { + continue; + } + } + if ( caster && caster->behavior == &actPlayer ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + Stat* stats = entity->getStats(); + if ( stats && entityInsideEntity(my, entity) ) + { + bool targetNonPlayer = false; + if ( caster && caster->behavior == &actPlayer ) + { + targetNonPlayer = true; + } + else if ( !caster && achievementObserver.checkUidIsFromPlayer(parentTimer->parent) >= 0 ) + { + targetNonPlayer = true; + } + + if ( !entity->monsterIsTargetable() ) { continue; } + + bool isEnemy = false; + if ( targetNonPlayer ) + { + isEnemy = entity->behavior == &actMonster && !entity->monsterAllyGetPlayerLeader() && achievementObserver.checkUidIsFromPlayer(stats->leader_uid) < 0; + } + else + { + isEnemy = (caster && !caster->checkFriend(entity) || !caster); + } + + if ( isEnemy ) + { + bool effected = false; + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_SPORES ) + { + int duration = getSpellEffectDurationSecondaryFromID(SPELL_SPORES, caster, nullptr, my); + bool prevEff = stats->getEffectActive(EFF_POISONED); + bool rollLevel = false; + if ( entity->setEffect(EFF_POISONED, true, duration + 10, false, true, false, false) ) + { + effected = true; + stats->poisonKiller = caster ? caster->getUID() : 0; + if ( parentTimer && parentTimer->particleTimerVariable4 > 0 ) + { + entity->char_poison = std::max(TICKS_PER_SECOND, entity->char_poison); + } + if ( !prevEff ) + { + rollLevel = true; + } + } + + prevEff = stats->getEffectActive(EFF_SLOW); + if ( entity->setEffect(EFF_SLOW, true, duration + 10, false, true, false, false) ) + { + effected = true; + if ( !prevEff ) + { + rollLevel = true; + } + } + + if ( particleEmitterHitPropsTimer->hits > 0 ) + { + if ( rollLevel ) + { + rollLevel = false; + } + } + + if ( rollLevel ) + { + if ( caster && caster->behavior == &actPlayer && parentTimer && parentTimer->particleTimerVariable2 == SPELL_SPORES ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_SPORES, 50.0, 1.0, entity); + } + } + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM ) + { + if ( parentTimer->particleTimerVariable3 == 0 ) + { + if ( Entity* breakable = Entity::createBreakableCollider(EditorEntityData_t::getColliderIndexFromName("mushroom_spell_fragile"), + entity->x, entity->y, caster) ) + { + parentTimer->particleTimerVariable3 = 1; + breakable->colliderSpellEvent = 1 + local_rng.rand() % 5; + if ( targetNonPlayer ) + { + breakable->colliderCreatedParent = parentTimer->parent; + breakable->colliderSpellEvent = 6; + } + if ( breakable->colliderSpellEvent > 0 && breakable->colliderSpellEvent < 1000 ) + { + breakable->colliderSpellEvent += 1000; + } + breakable->colliderSetServerSkillOnSpawned(); // to update the variables modified from create() + } + } + + if ( entity->setEffect(EFF_SLOW, true, 6 * TICKS_PER_SECOND + 10, false, true, false, false) ) + { + effected = true; + } + } + + if ( caster ) + { + bool alertTarget = entity->monsterAlertBeforeHit(caster); + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM ) + { + alertTarget = false; + } + + if ( particleEmitterHitPropsTimer->hits == 0 + || particleEmitterHitPropsTimer->hits % 4 == 0 ) + { + // alert the monster! + if ( entity->monsterState != MONSTER_STATE_ATTACK && (stats->type < LICH || stats->type >= SHOPKEEPER) ) + { + if ( alertTarget ) + { + entity->monsterAcquireAttackTarget(*caster, MONSTER_STATE_PATH, true); + } + } + + // alert other monsters too + if ( alertTarget ) + { + entity->alertAlliesOnBeingHit(caster); + } + } + entity->updateEntityOnHit(caster, alertTarget); + } + + if ( parentTimer && parentTimer->particleTimerVariable1 > 0 && my->ticks < 5 ) + { + int damage = parentTimer->particleTimerVariable1; + applyGenericMagicDamage(caster, entity, *my, parentTimer->particleTimerVariable2, damage, true, true); + } + + particleEmitterHitPropsTimer->hits++; + particleEmitterHitPropsTimer->tick = ticks; + } + } } - } - } - } - else if ( my->particleTimerEndAction == PARTICLE_EFFECT_GHOST_TELEPORT ) - { - // teleport to target spell. - if ( Entity* parent = uidToEntity(my->parent) ) - { - if ( my->particleTimerTarget != 0 ) - { - if ( Entity* target = uidToEntity(static_cast(my->particleTimerTarget)) ) + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_LIGHTNING_BOLT ) { - bool teleported = false; - createParticleErupt(parent, my->particleTimerEndSprite); - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - teleported = parent->teleportAroundEntity(target, my->particleTimerVariable1); - if ( teleported ) + auto particleEmitterHitPropsTimer = getParticleEmitterHitProps(my->parent, entity); + if ( !particleEmitterHitPropsTimer ) { - createParticleErupt(parent, my->particleTimerEndSprite); - // teleport success. - if ( multiplayer == SERVER ) + continue; + } + if ( particleEmitterHitPropsTimer->hits > 0 ) + { + continue; + } + + if ( caster && caster->behavior == &actMonster ) + { + if ( caster == entity || caster->checkFriend(entity) ) { - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); + continue; + } + } + if ( caster && caster->behavior == &actPlayer ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } + + Stat* stats = entity->getStats(); + if ( stats && entityDist(my, entity) <= 16.0 ) + { + if ( !entity->monsterIsTargetable(true) && !entity->isUntargetableBat() ) { continue; } + int damage = getSpellDamageFromID(SPELL_LIGHTNING_BOLT, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0); + if ( stats->getEffectActive(EFF_STATIC) ) + { + int extraDamage = getSpellDamageSecondaryFromID(SPELL_LIGHTNING_BOLT, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0); + if ( extraDamage > 0 ) + { + extraDamage *= getSpellDamageFromStatic(SPELL_LIGHTNING_BOLT, stats); + damage += std::max(1, extraDamage); + } + } + if ( applyGenericMagicDamage(caster, entity, *my, SPELL_LIGHTNING_BOLT, damage, true, true) ) + { + Uint8 effectStrength = stats->getEffectActive(EFF_STATIC); + if ( effectStrength < getSpellEffectDurationSecondaryFromID(SPELL_LIGHTNING_BOLT, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0) ) + { + effectStrength += 1; + } + if ( entity->setEffect(EFF_STATIC, effectStrength, + getSpellEffectDurationFromID(SPELL_LIGHTNING_BOLT, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0), true, true, false, false) ) + { + Entity* fx = createParticleAestheticOrbit(entity, 1758, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5; + fx->actmagicOrbitDist = 20; + fx->actmagicNoLight = 1; + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_STATIC_ORBIT, 1758); + } } + + particleEmitterHitPropsTimer->hits++; + particleEmitterHitPropsTimer->tick = ticks; } } - } - else - { - int tx = (my->particleTimerVariable2 >> 16) & 0xFFFF; - int ty = (my->particleTimerVariable2 >> 0) & 0xFFFF; - int dist = my->particleTimerVariable1; - bool forceSpot = false; - std::vector> goodspots; - for ( int iy = std::max(1, ty - dist); !forceSpot && iy <= std::min(ty + dist, static_cast(map.height) - 1); ++iy ) + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH ) { - for ( int ix = std::max(1, tx - dist); !forceSpot && ix <= std::min(tx + dist, static_cast(map.width) - 1); ++ix ) + if ( entityInsideEntity(my, entity) ) { - if ( !checkObstacle((ix << 4) + 8, (iy << 4) + 8, parent, NULL) ) + auto particleEmitterHitPropsTimer = getParticleEmitterHitProps(my->getUID(), entity); + if ( !particleEmitterHitPropsTimer ) + { + continue; + } + Stat* stats = entity->getStats(); + if ( stats ) + { + if ( !entity->monsterIsTargetable(true) ) { continue; } + if ( isLevitating(stats) ) { continue; } + if ( my->ticks >= 10 ) + { + if ( caster && caster->behavior == &actPlayer ) + { + if ( caster->checkFriend(entity) || caster == entity ) + { + // smaller hitbox for lingering terrain + auto sizex = my->sizex; + auto sizey = my->sizey; + my->sizex = 4; + my->sizey = 4; + bool result = entityInsideEntity(my, entity); + my->sizex = sizex; + my->sizey = sizey; + if ( result ) { continue; } + } + } + } + } + + int spellID = parentTimer ? parentTimer->particleTimerVariable3 : SPELL_DISRUPT_EARTH; + + if ( stats ) + { + int prevDuration = stats->getEffectActive(EFF_DISRUPTED) ? stats->EFFECTS_TIMERS[EFF_DISRUPTED] : 0; + int duration = getSpellEffectDurationFromID(spellID, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0); + if ( !stats->getEffectActive(EFF_DISRUPTED) ) + { + entity->setEffect(EFF_DISRUPTED, true, duration, false, true, false, false); // don't override strength + } + else + { + stats->EFFECTS_TIMERS[EFF_DISRUPTED] = std::max(stats->EFFECTS_TIMERS[EFF_DISRUPTED], duration); + } + } + + if ( particleEmitterHitPropsTimer->hits > 0 && spellID != SPELL_EARTH_SPINES ) + { + continue; + } + if ( caster && caster->behavior == &actMonster ) + { + if ( stats ) + { + if ( caster == entity || caster->checkFriend(entity) ) + { + ++particleEmitterHitPropsTimer->hits; + continue; + } + } + } + else if ( caster && caster->behavior == &actPlayer ) + { + if ( stats ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) && caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + ++particleEmitterHitPropsTimer->hits; + continue; + } + } + } + + int damage = 0; + + if ( my->ticks < 10 || spellID == SPELL_EARTH_SPINES ) { - real_t tmpx = parent->x; - real_t tmpy = parent->y; - parent->x = (ix << 4) + 8; - parent->y = (iy << 4) + 8; - if ( !entityInsideSomething(parent) ) + if ( auto particleEmitterHitPropsTimer2 = getParticleEmitterHitProps(my->parent, entity) ) { - if ( ix == tx && iy == ty ) + if ( my->ticks < 10 && (particleEmitterHitPropsTimer2->hits < 3) ) { - forceSpot = true; // directly ontop - goodspots.clear(); + if ( particleEmitterHitPropsTimer2->hits == 0 || ((ticks - particleEmitterHitPropsTimer2->tick) > 15) ) + { + damage = getSpellDamageFromID(spellID, caster, nullptr, parentTimer, my->actmagicSpellbookBonus / 100.0); + ++particleEmitterHitPropsTimer2->hits; + particleEmitterHitPropsTimer2->tick = ticks; + } + } + else if ( spellID == SPELL_EARTH_SPINES && sqrt(pow(entity->vel_x, 2) + pow(entity->vel_y, 2)) > 0.25 ) + { + if ( ((ticks - particleEmitterHitPropsTimer2->tick) > 50) ) + { + damage = getSpellDamageFromID(spellID, caster, nullptr, parentTimer, my->actmagicSpellbookBonus / 100.0); + ++particleEmitterHitPropsTimer2->hits; + particleEmitterHitPropsTimer2->tick = ticks; + } } - goodspots.push_back(std::make_pair(ix, iy)); } - // restore coordinates. - parent->x = tmpx; - parent->y = tmpy; + + if ( damage > 0 ) + { + if ( applyGenericMagicDamage(caster, entity, *my, spellID, damage, true) ) + { + } + } + if ( my->ticks < 10 && stats && entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + real_t pushbackMultiplier = 0.5; + real_t tangent = my->monsterKnockbackTangentDir + PI; // hack to store knockback + if ( entity->behavior == &actPlayer ) + { + if ( !players[entity->skill[2]]->isLocalPlayer() ) + { + entity->monsterKnockbackVelocity = pushbackMultiplier; + entity->monsterKnockbackTangentDir = tangent; + serverUpdateEntityFSkill(entity, 11); + serverUpdateEntityFSkill(entity, 9); + } + else + { + entity->monsterKnockbackVelocity = pushbackMultiplier; + entity->monsterKnockbackTangentDir = tangent; + } + } + else + { + entity->vel_x = cos(tangent) * pushbackMultiplier; + entity->vel_y = sin(tangent) * pushbackMultiplier; + entity->monsterKnockbackVelocity = 0.01; + entity->monsterKnockbackTangentDir = tangent; + entity->monsterKnockbackUID = parentTimer ? parentTimer->parent : 0; + } + } + + if ( my->ticks < 10 && stats ) + { + int prevDuration = stats->getEffectActive(EFF_DISRUPTED) ? stats->EFFECTS_TIMERS[EFF_DISRUPTED] : 0; + int durationAdd = getSpellEffectDurationSecondaryFromID(spellID, caster, nullptr, my); + if ( stats->getEffectActive(EFF_DISRUPTED) != 1 ) + { + if ( spellID == SPELL_EARTH_SPINES ) + { + entity->setEffect(EFF_DISRUPTED, Uint8(3), prevDuration + durationAdd, false, true, true, false); + } + else + { + entity->setEffect(EFF_DISRUPTED, Uint8(2), prevDuration + durationAdd, false, true, true, false); + } + } + else + { + stats->EFFECTS_TIMERS[EFF_DISRUPTED] += durationAdd; + } + } + + particleEmitterHitPropsTimer->hits++; + particleEmitterHitPropsTimer->tick = ticks; } } } - - if ( !goodspots.empty() ) + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_SELF_SUSTAIN + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_TILE + || my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ROOTS_PATH ) { - auto picked = goodspots.at(goodspots.size() - 1); - bool teleported = false; - createParticleErupt(parent, my->particleTimerEndSprite); - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - teleported = parent->teleport(picked.first, picked.second); - if ( teleported ) + auto particleEmitterHitPropsTimer = getParticleEmitterHitProps(my->parent, entity); + if ( !particleEmitterHitPropsTimer ) { - createParticleErupt(parent, my->particleTimerEndSprite); - // teleport success. - if ( multiplayer == SERVER ) - { - serverSpawnMiscParticles(parent, PARTICLE_EFFECT_ERUPT, my->particleTimerEndSprite); - } + continue; + } + if ( particleEmitterHitPropsTimer->hits > 0 && (ticks - particleEmitterHitPropsTimer->tick) < 1.5 * TICKS_PER_SECOND ) + { + continue; } - } - } - - } - } - } - my->removeLightField(); - list_RemoveNode(my->mynode); - return; - } - else - { - --PARTICLE_LIFE; - if ( my->particleTimerPreDelay <= 0 ) - { - // shoot particles for the duration of the timer, centered at caster. - if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SHOOT_PARTICLES ) - { - Entity* parent = uidToEntity(my->parent); - // shoot drops to the sky - if ( parent && my->particleTimerCountdownSprite != 0 ) - { - Entity* entity = newEntity(my->particleTimerCountdownSprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x - 4 + local_rng.rand() % 9; - entity->y = parent->y - 4 + local_rng.rand() % 9; - entity->z = 7.5; - entity->vel_z = -1; - entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->particleDuration = 10 + local_rng.rand() % 30; - entity->behavior = &actParticleDot; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) - { - entity_uids--; - } - entity->setUID(-3); - } - } - // fire once off. - else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPAWN_PORTAL ) - { - Entity* parent = uidToEntity(my->parent); - if ( parent && my->particleTimerCountdownAction < 100 ) - { - playSoundEntityLocal(parent, 167, 128); - createParticleDot(parent); - createParticleCircling(parent, 100, my->particleTimerCountdownSprite); - my->particleTimerCountdownAction = 0; - } - } - // fire once off. - else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SUMMON_MONSTER ) - { - if ( my->particleTimerCountdownAction < 100 ) - { - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, true)); - playSoundEntityLocal(my, 167, 128); - createParticleDropRising(my, 680, 1.0); - createParticleCircling(my, 70, my->particleTimerCountdownSprite); - my->particleTimerCountdownAction = 0; - } - } - // fire once off. - else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_DEVIL_SUMMON_MONSTER ) - { - if ( my->particleTimerCountdownAction < 100 ) - { - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, true)); - playSoundEntityLocal(my, 167, 128); - createParticleDropRising(my, 593, 1.0); - createParticleCircling(my, 70, my->particleTimerCountdownSprite); - my->particleTimerCountdownAction = 0; - } - } - // continually fire - else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_SPELL_SUMMON ) - { - if ( multiplayer != CLIENT && my->particleTimerPreDelay != -100 ) - { - // once-off hack :) - spawnExplosion(my->x, my->y, -1); - playSoundEntity(my, 171, 128); - my->particleTimerPreDelay = -100; - createParticleErupt(my, my->particleTimerCountdownSprite); - serverSpawnMiscParticles(my, PARTICLE_EFFECT_ERUPT, my->particleTimerCountdownSprite); - } - } - // fire once off. - else if ( my->particleTimerCountdownAction == PARTICLE_EFFECT_TELEPORT_PULL_TARGET_LOCATION ) - { - createParticleDropRising(my, my->particleTimerCountdownSprite, 1.0); - my->particleTimerCountdownAction = 0; - } - else if ( my->particleTimerCountdownAction == PARTICLE_TIMER_ACTION_MAGIC_SPRAY ) - { - Entity* parent = uidToEntity(my->parent); - if ( !parent ) - { - PARTICLE_LIFE = 0; - } - else - { - int sound = 0; - int spellID = SPELL_NONE; - switch ( my->particleTimerCountdownSprite ) - { - case 180: - sound = 169; - spellID = SPELL_SLIME_ACID; - break; - case 181: - sound = 169; - spellID = SPELL_SLIME_WATER; - break; - case 182: - sound = 164; - spellID = SPELL_SLIME_FIRE; - break; - case 183: - sound = 169; - spellID = SPELL_SLIME_TAR; - break; - case 184: - sound = 169; - spellID = SPELL_SLIME_METAL; - break; - default: - break; - } - if ( my->particleTimerVariable1 == 0 ) - { - // first fired after delay - my->particleTimerVariable1 = 1; - if ( sound > 0 ) - { - playSoundEntityLocal(parent, sound, 128); - } - } + Stat* stats = entity->getStats(); + if ( stats && entityDist(my, entity) < radius * 16.0 + 16.0 ) + { + if ( !entity->monsterIsTargetable() || entity == caster ) { continue; } + if ( caster ) + { + if ( caster && caster->behavior == &actMonster ) + { + if ( caster->checkFriend(entity) ) + { + continue; + } + } + if ( caster && caster->behavior == &actPlayer ) + { + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } - my->x = parent->x; - my->y = parent->y; - my->yaw = parent->yaw; + // check overlapping roots - Entity* entity = nullptr; - if ( multiplayer != CLIENT && my->ticks % 2 == 0 ) - { - // damage frames - entity = newEntity(my->particleTimerCountdownSprite, 1, map.entities, nullptr); - entity->behavior = &actMagicMissile; - } - else - { - entity = multiplayer == CLIENT ? spawnGibClient(0, 0, 0, -1) : spawnGib(my); - } - if ( entity ) - { - entity->sprite = my->particleTimerCountdownSprite; - entity->x = parent->x; - entity->y = parent->y; - if ( parent->behavior == &actMonster && parent->getMonsterTypeFromSprite() == SLIME ) - { - entity->z = -2 + parent->z + parent->focalz; - } - else - { - entity->z = parent->z; - } - entity->parent = parent->getUID(); + bool found = false; + if ( entityInsideEntity(my, entity) ) + { + found = true; + } + else + { + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, radius); + for ( auto it : entLists ) + { + if ( found ) + { + break; + } + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + Entity* entity2 = (Entity*)node->element; + if ( entity2->behavior == &actParticleRoot + && !entity2->flags[INVISIBLE] + && entity2->parent == my->getUID() + && entityInsideEntity(entity2, entity) ) + { + found = true; + break; + } + } + } + } - entity->ditheringDisabled = true; - entity->flags[SPRITE] = true; - entity->flags[INVISIBLE] = false; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->flags[BRIGHT] = true; + if ( found ) + { + int damage = 0; + if ( parentTimer && parentTimer->particleTimerVariable1 != 0 ) + { + damage = parentTimer->particleTimerVariable1; + } + else + { + if ( parentTimer && parentTimer->particleTimerVariable2 == 0 ) + { + damage = getSpellDamageFromID(SPELL_ROOTS, caster, nullptr, my); + } + else + { + damage = getSpellDamageFromID(parentTimer ? parentTimer->particleTimerVariable2 : SPELL_ROOTS, caster, nullptr, my); + } + } - entity->sizex = 2; - entity->sizey = 2; - entity->yaw = my->yaw - 0.2 + (local_rng.rand() % 20) * 0.02; - entity->pitch = (local_rng.rand() % 360) * PI / 180.0; - entity->roll = (local_rng.rand() % 360) * PI / 180.0; - double vel = (20 + (local_rng.rand() % 5)) / 10.f; - entity->vel_x = vel * cos(entity->yaw) + parent->vel_x; - entity->vel_y = vel * sin(entity->yaw) + parent->vel_y; - entity->vel_z = -.5; - if ( entity->behavior == &actGib ) - { - if ( my->ticks % 5 == 0 ) - { - // add lighting - entity->skill[6] = 1; + if ( applyGenericMagicDamage(caster, entity, *my, parentTimer->particleTimerVariable1, damage, true, true) ) + { + if ( entity->setEffect(EFF_ROOTED, true, TICKS_PER_SECOND, false) ) + { + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 1758); + } + if ( parentTimer && parentTimer->particleTimerVariable3 == SPELL_BLADEVINES ) + { + if ( !stats->getEffectActive(EFF_BLEEDING) ) + { + if ( entity->setEffect(EFF_BLEEDING, true, 3 * TICKS_PER_SECOND, false) ) + { + stats->bleedInflictedBy = caster ? caster->getUID() : 0; + for ( int gibs = 0; gibs < 3; ++gibs ) + { + Entity* gib = spawnGib(entity); + serverSpawnGibForClient(gib); + } + } + } + } + } + particleEmitterHitPropsTimer->hits++; + particleEmitterHitPropsTimer->tick = ticks; + } + } } } - else if ( entity->behavior == &actMagicMissile ) + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ICE_WAVE ) { - spell_t* spell = getSpellFromID(spellID); - entity->skill[4] = 0; // life start - entity->skill[5] = TICKS_PER_SECOND; //lifetime - entity->actmagicSpray = 1; - entity->actmagicSprayGravity = 0.04; - entity->actmagicEmitter = my->getUID(); - node_t* node = list_AddNodeFirst(&entity->children); - node->element = copySpell(spell); - ((spell_t*)node->element)->caster = parent->getUID(); - node_t* elementNode = ((spell_t*)node->element)->elements.first; - spellElement_t* element = (spellElement_t*)elementNode->element; + auto particleEmitterHitPropsTimer = getParticleEmitterHitProps(my->parent, entity); + if ( !particleEmitterHitPropsTimer ) { - elementNode = element->elements.first; - element = (spellElement_t*)elementNode->element; - element->damage = 2; - if ( Stat* stats = parent->getStats() ) + continue; + } + auto particleEmitterHitPropsFloorMagic = getParticleEmitterHitProps(my->getUID(), entity); + if ( !particleEmitterHitPropsFloorMagic ) + { + continue; + } + if ( particleEmitterHitPropsTimer->hits > 0 && ((ticks - particleEmitterHitPropsTimer->tick) < 30) ) + { + if ( particleEmitterHitPropsFloorMagic->hits > 5 ) + { + continue; + } + if ( particleEmitterHitPropsFloorMagic->hits == 0 && my->skill[1] < 10 ) + { + // allowed big hit + } + else { - element->damage += stats->getProficiency(PRO_MAGIC) / 10; + continue; } - element->mana = 5; } - node->deconstructor = &spellDeconstructor; - node->size = sizeof(spell_t); + if ( caster && caster->behavior == &actMonster ) + { + if ( caster == entity || caster->checkFriend(entity) ) + { + continue; + } + } + if ( caster && caster->behavior == &actPlayer ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } + } + Stat* stats = entity->getStats(); + if ( stats && entityInsideEntity(my, entity) ) + { + if ( !entity->monsterIsTargetable() ) { continue; } + int damage = 1; + + int duration = 0; + Uint8 strength = 0; + if ( particleEmitterHitPropsFloorMagic->hits == 0 && my->skill[1] < 10 ) + { + strength = 1; + duration = getSpellEffectDurationFromID(SPELL_ICE_WAVE, caster ? caster : my, nullptr, my, my->actmagicSpellbookBonus / 100.0); + damage = getSpellDamageFromID(SPELL_ICE_WAVE, caster ? caster : my, nullptr, my, my->actmagicSpellbookBonus / 100.0); // big damage region + } + else + { + strength = std::min(8, stats->getEffectActive(EFF_SLOW) + 1); + duration = getSpellEffectDurationSecondaryFromID(SPELL_ICE_WAVE, caster ? caster : my, nullptr, my, my->actmagicSpellbookBonus / 100.0); + damage += strength * getSpellDamageSecondaryFromID(SPELL_ICE_WAVE, caster ? caster : my, nullptr, my, my->actmagicSpellbookBonus / 100.0); + } + if ( damage > 0 ) + { + if ( applyGenericMagicDamage(caster, entity, *my, SPELL_ICE_WAVE, damage, true, true) ) + { + if ( stats->getEffectActive(EFF_SLOW) ) + { + duration = std::max(stats->EFFECTS_TIMERS[EFF_SLOW], duration); + } - --entity_uids; - entity->setUID(-3); + if ( entity->setEffect(EFF_SLOW, strength, duration, false) ) + { + + } + } + } + particleEmitterHitPropsTimer->hits++; + particleEmitterHitPropsTimer->tick = ticks; + + particleEmitterHitPropsFloorMagic->hits++; + particleEmitterHitPropsFloorMagic->tick = ticks; + } } } } } } - else - { - --my->particleTimerPreDelay; - } } -} - -void actParticleSap(Entity* my) -{ - real_t decel = 0.9; - real_t accel = 0.9; - real_t z_accel = accel; - real_t z_decel = decel; - real_t minSpeed = 0.05; - if ( PARTICLE_LIFE < 0 ) - { - list_RemoveNode(my->mynode); - return; - } - else + if ( doParticle ) { - if ( my->sprite == 977 ) // boomerang + if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_ICE_WAVE ) { - if ( my->skill[3] == 1 ) - { - // specific for the animation I want... - // magic numbers that take approximately 75 frames (50% of travel time) to go outward or inward. - // acceleration is a little faster to overshoot into the right hand side. - decel = 0.9718; - accel = 0.9710; - z_decel = decel; - z_accel = z_decel; - } - else - { - decel = 0.95; - accel = 0.949; - z_decel = 0.9935; - z_accel = z_decel; - } - Entity* particle = spawnMagicParticleCustom(my, (local_rng.rand() % 2) ? 943 : 979, 1, 10); + Entity* particle = spawnMagicParticleCustom(my, 225, 0.5, 4); if ( particle ) { particle->lightBonus = vec4(0.5f, 0.5f, 0.5f, 0.f); - particle->focalx = 2; - particle->focaly = -2; - particle->focalz = 2.5; + particle->flags[SPRITE] = true; particle->ditheringDisabled = true; + particle->vel_x = 0.25 * cos(my->yaw); + particle->vel_y = 0.25 * sin(my->yaw); + particle->vel_z = -0.05; + + real_t forward = 2.0 + local_rng.rand() % 5; + particle->z += -4.0 + local_rng.rand() % 7; + real_t side = -2.0 + (local_rng.rand() % 5) * 1.0; + particle->x += forward * cos(my->yaw) + side * cos(my->yaw + PI / 2); + particle->y += forward * sin(my->yaw) + side * sin(my->yaw + PI / 2); } - if ( PARTICLE_LIFE < 100 && my->ticks % 6 == 0 ) + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_SPORES ) + { + if ( my->ticks % 10 == 0 || my->ticks == 1 ) { - if ( PARTICLE_LIFE < 70 ) + Entity* entity = newEntity(227, 1, map.entities, nullptr); //Sprite entity. + + int cycle = (my->ticks / 10) % 5; + if ( cycle > 0 ) { - playSoundEntityLocal(my, 434 + local_rng.rand() % 10, 64); + entity->x = my->x + 8.0 * cos(my->yaw + cycle * PI / 2 + PI / 4); + entity->y = my->y + 8.0 * sin(my->yaw + cycle * PI / 2 + PI / 4); } else { - playSoundEntityLocal(my, 434 + local_rng.rand() % 10, 32); + entity->x = my->x; + entity->y = my->y; + } + entity->z = 6.0; + entity->ditheringDisabled = true; + entity->flags[SPRITE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + entity->behavior = &actSprite; + entity->skill[0] = 1; + entity->skill[1] = 6; + entity->skill[2] = 4; + entity->vel_z = -0.25; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } + } + else if ( my->actfloorMagicType == ParticleTimerEffect_t::EffectType::EFFECT_MYCELIUM ) + { + if ( my->ticks % 10 == 0 || my->ticks == 1 ) + { + Entity* entity = newEntity(248, 1, map.entities, nullptr); //Sprite entity. + + int cycle = (my->ticks / 10) % 5; + if ( cycle > 0 ) + { + entity->x = my->x + 8.0 * cos(my->yaw + cycle * PI / 2 + PI / 4); + entity->y = my->y + 8.0 * sin(my->yaw + cycle * PI / 2 + PI / 4); + } + else + { + entity->x = my->x; + entity->y = my->y; + } + entity->z = 6.0; + entity->ditheringDisabled = true; + entity->flags[SPRITE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = true; + entity->behavior = &actSprite; + entity->skill[0] = 1; + entity->skill[1] = 6; + entity->skill[2] = 4; + entity->vel_z = -0.125; + if ( multiplayer != CLIENT ) + { + entity_uids--; } + entity->setUID(-3); } - //particle->flags[SPRITE] = true; } - else + else if ( Entity* particle = spawnMagicParticleCustom(my, my->sprite, my->scalex * 0.7, 1) ) + { + particle->vel_x = cos(my->yaw); + particle->vel_y = sin(my->yaw); + } + } +} + +void waveParticleSetUID(Entity& fx, bool noupdate) +{ + Sint32 val = (1 << 31); + val |= (Uint8)(22); + val |= (((Uint16)(fx.actParticleWaveStartFrame) & 0xFFF) << 8); + val |= (Uint8)(fx.actParticleWaveMagicType & 0xFF) << 20; + val |= (fx.actParticleWaveLight != 0 ? 1 : 0) << 28; + fx.skill[2] = val; +} + +void particleWaveClientReceive(Entity* my) +{ + if ( !my ) { return; } + if ( multiplayer != CLIENT ) { return; } + + if ( my->actParticleWaveClientReceived != 0 ) { return; } + + my->actParticleWaveStartFrame = (my->skill[2] >> 8) & 0xFFF; + my->sprite = my->actParticleWaveStartFrame; + my->actParticleWaveMagicType = (my->skill[2] >> 20) & 0xFF; + my->actParticleWaveLight = (my->skill[2] >> 28) & 1; + if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_FIRE_WAVE ) + { + my->skill[1] = 6; // frames + my->skill[5] = 4; // frame time + my->ditheringOverride = 6; + real_t startScale = 0.1; + my->scalex = startScale; + my->scaley = startScale; + my->scalez = startScale; + real_t grouping = 13.75; + real_t scale = 1.0; + my->focaly = startScale * grouping; + my->fskill[0] = scale; // final scale + my->fskill[1] = grouping; // final grouping + my->skill[6] = 1; // grow to scale + } + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_KINETIC_FIELD ) + { + my->skill[1] = 12; // frames + my->skill[5] = 4; // frame time + my->ditheringOverride = 6; + + real_t startScale = 0.1; + real_t scale = 1.0; + my->scalex = startScale; + my->scaley = startScale; + my->scalez = startScale; + my->fskill[0] = scale; // final scale + my->skill[6] = 1; // grow to scale + } + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_CHRONOMIC_FIELD ) + { + my->skill[1] = 8; // frames + my->skill[5] = 4; // frame time + my->ditheringOverride = 6; + + real_t startScale = 0.1; + real_t scale = 1.0; + my->scalex = startScale; + my->scaley = startScale; + my->scalez = startScale; + my->fskill[0] = scale; // final scale + my->skill[6] = 1; // grow to scale + } + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_PULSE ) + { + my->skill[1] = 6; // frames + my->skill[5] = 4; // frame time + my->ditheringOverride = 6; + my->scalex = 1.0; + my->scalez = 1.0; + } + + my->actParticleWaveClientReceived = 1; +} + +Entity* createParticleWave(ParticleTimerEffect_t::EffectType particleType, int sprite, real_t x, real_t y, real_t z, real_t dir, Uint32 lifetime, bool light) +{ + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Sprite entity. + entity->x = x; + entity->y = y; + entity->z = z; + //entity->ditheringDisabled = true; + entity->flags[SPRITE] = false; + entity->flags[PASSABLE] = true; + //entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[BRIGHT] = false; + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->behavior = &actParticleWave; + entity->yaw = dir; + entity->pitch = 0; + entity->roll = 0.0; + entity->skill[0] = lifetime; + entity->skill[1] = 12; // frames + entity->skill[3] = 0; // current frame + entity->skill[4] = entity->sprite; // start frame + entity->skill[5] = 5; // frame time + entity->ditheringOverride = 4; + entity->actParticleWaveMagicType = particleType; + entity->actParticleWaveLight = light ? 1 : 0; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + /*if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3);*/ + waveParticleSetUID(*entity, true); + return entity; +} + +void createParticleDemesneDoor(real_t x, real_t y, real_t dir) +{ + for ( int c = 0; c <= 8; c++ ) + { + Entity* entity = newEntity(576, 1, map.entities, nullptr); //Particle entity. + entity->sizex = 1; + entity->sizey = 1; + entity->x = x + (-4.0 + c) * cos(dir + PI / 2); + entity->y = y + (-4.0 + c) * sin(dir + PI / 2); + entity->z = -4.0; + entity->yaw = dir; + entity->vel_x = 0.0;//0.2; + entity->vel_y = 0.0;//0.2; + entity->vel_z = -0.02; + entity->skill[0] = 100; + entity->skill[1] = 0; // direction. + entity->fskill[0] = 0.1; + entity->behavior = &actParticleErupt; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UNCLICKABLE] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} + +void actParticleDemesneDoor(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + createParticleDemesneDoor(my->x, my->y, my->yaw); + serverSpawnMiscParticlesAtLocation(my->x, my->y, my->yaw * 256.0, PARTICLE_EFFECT_DEMESNE_DOOR, 0); + my->removeLightField(); + list_RemoveNode(my->mynode); + if ( Entity* caster = uidToEntity(my->parent) ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_WORLD, Language::get(6691)); + } + return; + } + + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, "demesne_door"); + } + + if ( my->skill[1] == 0 ) + { + my->skill[1] = 1; + createParticleDemesneDoor(my->x, my->y, my->yaw); + } + + my->ditheringOverride = 4; + + if ( multiplayer != CLIENT ) + { + --PARTICLE_LIFE; + + int mapx = my->x / 16; + int mapy = my->y / 16; + bool interrupted = false; + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1); + for ( auto it : entLists ) { - if ( *cvar_magic_fx_use_vismap && !intro ) + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) { - int x = my->x / 16.0; - int y = my->y / 16.0; - if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + if ( Entity* entity = (Entity*)node->element ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( static_cast(entity->x / 16) == mapx && static_cast(entity->y / 16) == mapy ) { - if ( !client_disconnected[i] && players[i]->isLocalPlayer() && cameras[i].vismap[y + x * map.height] ) + if ( entity == my ) { continue; } + if ( entity->behavior == &actDoor ) { - spawnMagicParticle(my); - break; + entity->doorHealth = 0; + } + if ( entity->behavior == &actGate && entity->gateStatus == 0 ) + { + interrupted = true; + } + if ( entity->behavior == &actIronDoor && entity->doorStatus == 0 ) + { + interrupted = true; + } + if ( entity->behavior == &actParticleDemesneDoor ) + { + interrupted = true; + } + if ( entity->behavior == &actPlayer || entity->behavior == &actMonster ) + { + if ( Stat* stats = entity->getStats() ) + { + if ( stats->type != VAMPIRE ) + { + if ( entityInsideEntity(my, entity) ) + { + if ( auto hitProps = getParticleEmitterHitProps(my->getUID(), entity) ) + { + if ( hitProps->hits == 0 ) + { + hitProps->hits++; + hitProps->tick = ticks; + } + } + } + } + } } } } } - else - { - spawnMagicParticle(my); - } - } - Entity* parent = uidToEntity(my->parent); - if ( parent ) - { - my->x = parent->x + my->fskill[0]; - my->y = parent->y + my->fskill[1]; - } - else - { - list_RemoveNode(my->mynode); - return; - } - - if ( my->skill[1] == 0 ) - { - // move outwards diagonally. - if ( abs(my->vel_z) > minSpeed ) - { - my->fskill[0] += my->vel_x; - my->fskill[1] += my->vel_y; - my->vel_x *= decel; - my->vel_y *= decel; - - my->z += my->vel_z; - my->vel_z *= z_decel; - my->yaw += my->fskill[2]; - my->pitch += my->fskill[3]; - } - else + for ( auto& hitProp : particleTimerEmitterHitEntities[my->getUID()] ) { - my->skill[1] = 1; - my->vel_x *= -1; - my->vel_y *= -1; - my->vel_z *= -1; + if ( hitProp.second.hits == 1 ) + { + if ( Entity* entity = uidToEntity(hitProp.first) ) + { + if ( Stat* stats = entity->getStats() ) + { + if ( stats->type != VAMPIRE ) + { + if ( !entityInsideEntity(my, entity) ) + { + hitProp.second.hits++; + hitProp.second.tick = ticks; + Entity* caster = uidToEntity(my->parent); + if ( caster && (caster == entity || caster->checkFriend(entity)) ) + { + int effectStrength = std::min(255, + std::min(getSpellDamageSecondaryFromID(SPELL_DEMESNE_DOOR, caster, nullptr, my), + std::max(1, getSpellDamageFromID(SPELL_DEMESNE_DOOR, caster, nullptr, my)))); + if ( entity->setEffect(EFF_DEMESNE_DOOR, (Uint8)effectStrength, + getSpellEffectDurationSecondaryFromID(SPELL_DEMESNE_DOOR, caster, nullptr, my), false) ) + { + magicOnSpellCastEvent(caster, caster, nullptr, SPELL_DEMESNE_DOOR, spell_t::SPELL_LEVEL_EVENT_EFFECT, 1); + } + } + } + } + } + } + } } } - else if ( my->skill[1] == 1 ) - { - // move inwards diagonally. - if ( (abs(my->vel_z) < 0.08 && my->skill[3] == 0) || (abs(my->vel_z) < 0.4 && my->skill[3] == 1) ) - { - my->fskill[0] += my->vel_x; - my->fskill[1] += my->vel_y; - my->vel_x /= accel; - my->vel_y /= accel; - - my->z += my->vel_z; - my->vel_z /= z_accel; - my->yaw += my->fskill[2]; - my->pitch += my->fskill[3]; - } - else + if ( interrupted ) + { + if ( Entity* caster = uidToEntity(my->parent) ) { - // movement completed. - my->skill[1] = 2; + messagePlayer(caster->isEntityPlayer(), MESSAGE_WORLD, Language::get(6690)); } - } - my->scalex *= 0.99; - my->scaley *= 0.99; - my->scalez *= 0.99; - if ( my->sprite == 977 ) - { - my->scalex = 1.f; - my->scaley = 1.f; - my->scalez = 1.f; - my->roll -= 0.5; - my->pitch = std::max(my->pitch - 0.015, 0.0); + createParticleDemesneDoor(my->x, my->y, my->yaw); + serverSpawnMiscParticlesAtLocation(my->x, my->y, my->yaw * 256.0, PARTICLE_EFFECT_DEMESNE_DOOR, 0); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; } - --PARTICLE_LIFE; } } -void actParticleSapCenter(Entity* my) +void actParticleWave(Entity* my) { - // init - if ( my->skill[3] == 0 ) - { - // for clients and server spawn the visible arcing particles. - my->skill[3] = 1; - createParticleSap(my); - } - - if ( multiplayer == CLIENT ) + Entity* parentTimer = uidToEntity(my->parent); + if ( PARTICLE_LIFE < 0 || (multiplayer != CLIENT && !parentTimer) ) { + my->removeLightField(); + list_RemoveNode(my->mynode); return; } - Entity* parent = uidToEntity(my->parent); - if ( parent ) + if ( multiplayer != CLIENT && my->scalex >= my->fskill[0] * .9 ) { - // if reached the caster, delete self and spawn some particles. - if ( my->sprite == 977 && PARTICLE_LIFE > 1 ) - { - // store these in case parent dies. - // boomerang doesn't check for collision until end of life. - my->fskill[4] = parent->x; - my->fskill[5] = parent->y; - } - else if ( entityInsideEntity(my, parent) || (my->sprite == 977 && PARTICLE_LIFE == 0) ) + Entity* caster = uidToEntity(parentTimer->parent); + if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_CHRONOMIC_FIELD ) { - if ( my->skill[6] == SPELL_STEAL_WEAPON ) + int chronomicLimit = getSpellDamageFromID(SPELL_CHRONOMIC_FIELD, caster, nullptr, my); + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + for ( auto it : entLists ) { - if ( my->skill[7] == 1 ) + if ( my->actParticleWaveVariable1 >= chronomicLimit ) { - // found stolen item. - Item* item = newItemFromEntity(my); - if ( parent->behavior == &actPlayer ) + break; + } + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + if ( my->actParticleWaveVariable1 >= chronomicLimit ) { - itemPickup(parent->skill[2], item); + break; } - else if ( parent->behavior == &actMonster ) + Entity* entity = (Entity*)node->element; + if ( entity->behavior == &actArrow || entity->behavior == &actMagicMissile + || entity->behavior == &actThrown ) { - parent->addItemToMonsterInventory(item); - Stat *myStats = parent->getStats(); - if ( myStats ) + auto particleEmitterHitProps = getParticleEmitterHitProps(my->parent, entity); + if ( !particleEmitterHitProps ) + { + continue; + } + if ( particleEmitterHitProps->hits > 0 ) + { + continue; + } + if ( entityInsideEntity(my, entity) ) { - node_t* weaponNode = itemNodeInInventory(myStats, -1, WEAPON); - if ( weaponNode ) + particleEmitterHitProps->hits++; + my->actParticleWaveVariable1++; // total hits + + playSoundEntity(my, 166, 128); + real_t spd = sqrt(entity->vel_x * entity->vel_x + entity->vel_y * entity->vel_y); + if ( Entity* parent = uidToEntity(entity->parent) ) + { + real_t dir = atan2(entity->y - parent->y, entity->x - parent->x) + PI; + entity->vel_x = spd * cos(dir); + entity->vel_y = spd * sin(dir); + if ( entity->behavior == &actArrow || entity->behavior == &actMagicMissile ) + { + entity->yaw = dir; + } + + if ( caster && caster != parent ) + { + magicOnSpellCastEvent(caster, caster, parent, + SPELL_CHRONOMIC_FIELD, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } + } + else { - swapMonsterWeaponWithInventoryItem(parent, myStats, weaponNode, false, true); - if ( myStats->type == INCUBUS ) + real_t spd = sqrt(entity->vel_x * entity->vel_x + entity->vel_y * entity->vel_y); + real_t dir = atan2(entity->vel_y, entity->vel_x) + PI; + entity->vel_x = spd * cos(dir); + entity->vel_y = spd * sin(dir); + if ( entity->behavior == &actArrow || entity->behavior == &actMagicMissile ) { - parent->monsterSpecialState = INCUBUS_TELEPORT_STEAL; - parent->monsterSpecialTimer = 100 + local_rng.rand() % MONSTER_SPECIAL_COOLDOWN_INCUBUS_TELEPORT_RANDOM; + entity->yaw = dir; } } + if ( parentTimer->parent == entity->parent ) // own spell + { + entity->parent = my->getUID(); + } + else + { + entity->parent = parentTimer->parent; + } } } - item = nullptr; - } - playSoundEntity(parent, 168, 128); - spawnMagicEffectParticles(parent->x, parent->y, parent->z, my->skill[5]); - } - else if ( my->skill[6] == SPELL_DRAIN_SOUL ) - { - parent->modHP(my->skill[7]); - parent->modMP(my->skill[8]); - if ( parent->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(2445)); } - playSoundEntity(parent, 168, 128); - spawnMagicEffectParticles(parent->x, parent->y, parent->z, 169); - } - else if ( my->skill[6] == SHADOW_SPELLCAST ) - { - parent->shadowSpecialAbility(parent->monsterShadowInitialMimic); - playSoundEntity(parent, 166, 128); - spawnMagicEffectParticles(parent->x, parent->y, parent->z, my->skill[5]); - } - else if ( my->skill[6] == SPELL_SUMMON ) - { - parent->modMP(my->skill[7]); - /*if ( parent->behavior == &actPlayer ) - { - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerColor(parent->skill[2], MESSAGE_STATUS, color, Language::get(774)); - }*/ - playSoundEntity(parent, 168, 128); - spawnMagicEffectParticles(parent->x, parent->y, parent->z, 169); } - else if ( my->skill[6] == SPELL_FEAR ) + + if ( my->actParticleWaveVariable1 >= chronomicLimit ) { - playSoundEntity(parent, 168, 128); - spawnMagicEffectParticles(parent->x, parent->y, parent->z, 174); - Entity* caster = uidToEntity(my->skill[7]); - if ( caster ) - { - spellEffectFear(nullptr, spellElement_fear, caster, parent, 0); - } + parentTimer->skill[0] = 1; // decay parent + my->removeLightField(); + list_RemoveNode(my->mynode); + return; } - else if ( my->sprite == 977 ) // boomerang + } + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_KINETIC_FIELD ) + { + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + for ( auto it : entLists ) { - Item* item = newItemFromEntity(my); - if ( parent->behavior == &actPlayer ) + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) { - item->ownerUid = parent->getUID(); - Item* pickedUp = itemPickup(parent->skill[2], item); - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerColor(parent->skill[2], MESSAGE_EQUIPMENT, color, Language::get(3746), items[item->type].getUnidentifiedName()); - achievementObserver.awardAchievementIfActive(parent->skill[2], parent, AchievementObserver::BARONY_ACH_IF_YOU_LOVE_SOMETHING); - if ( pickedUp ) + Entity* entity = (Entity*)node->element; + if ( entity->getStats() ) { - if ( parent->skill[2] == 0 || (parent->skill[2] > 0 && splitscreen) ) + if ( !entity->monsterIsTargetable() ) { continue; } + if ( entityInsideEntity(my, entity) ) { - // pickedUp is the new inventory stack for server, free the original items - free(item); - item = nullptr; - if ( multiplayer != CLIENT && !stats[parent->skill[2]]->weapon ) + auto particleEmitterHitProps = getParticleEmitterHitProps(my->parent, entity); + if ( !particleEmitterHitProps ) { - useItem(pickedUp, parent->skill[2]); + continue; + } + if ( particleEmitterHitProps->hits > 0 && ((ticks - particleEmitterHitProps->tick) < 60) ) + { + continue; + } + + real_t dir = (my->yaw - PI / 2); + while ( dir < 0.0 ) + { + dir += 2 * PI; + } + while ( dir >= 2 * PI ) + { + dir -= 2 * PI; + } + + if ( entity->behavior == &actPlayer ) + { + particleEmitterHitProps->hits++; + particleEmitterHitProps->tick = ticks; + playSoundEntity(entity, 180, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 982); + Uint8 effectStrength = 2 + (1 * (MAXPLAYERS + 1)); + if ( caster && caster->behavior == &actPlayer ) + { + effectStrength += caster->skill[2]; + } + else + { + effectStrength += MAXPLAYERS; + } + entity->setEffect(EFF_DASH, effectStrength, 60, false); + int player = entity->skill[2]; + if ( player > 0 && multiplayer == SERVER && !players[player]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "KINE"); + SDLNet_Write32((dir) * 256.0, &net_packet->data[4]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + else + { + real_t vel = sqrt(pow(entity->vel_y, 2) + pow(entity->vel_x, 2)); + entity->monsterKnockbackVelocity = std::min(2.25, std::max(1.0, vel)); + entity->monsterKnockbackTangentDir = dir; + } + if ( particleEmitterHitProps->hits == 1 ) + { + magicOnSpellCastEvent(caster, caster, nullptr, SPELL_KINETIC_FIELD, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } } - auto& hotbar_t = players[parent->skill[2]]->hotbar; - if ( hotbar_t.magicBoomerangHotbarSlot >= 0 ) + else if ( entity->behavior == &actMonster ) { - auto& hotbar = hotbar_t.slots(); - hotbar[hotbar_t.magicBoomerangHotbarSlot].item = pickedUp->uid; - for ( int i = 0; i < NUM_HOTBAR_SLOTS; ++i ) + particleEmitterHitProps->hits++; + particleEmitterHitProps->tick = ticks; + if ( entity->setEffect(EFF_KNOCKBACK, true, 30, false) ) { - if ( i != hotbar_t.magicBoomerangHotbarSlot - && hotbar[i].item == pickedUp->uid ) + playSoundEntity(entity, 180, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 982); + entity->setEffect(EFF_DASH, true, 15, false); + + real_t push = 1.5; + entity->vel_x = cos(dir) * push; + entity->vel_y = sin(dir) * push; + entity->monsterKnockbackVelocity = 0.01; + entity->monsterKnockbackTangentDir = dir; + entity->monsterKnockbackUID = parentTimer->parent; + + if ( particleEmitterHitProps->hits == 1 ) { - hotbar[i].item = 0; - hotbar[i].resetLastItem(); + magicOnSpellCastEvent(caster, caster, entity, SPELL_KINETIC_FIELD, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); } } } } - else - { - free(pickedUp); // item is the picked up items (item == pickedUp) - } } } - else if ( parent->behavior == &actMonster ) - { - parent->addItemToMonsterInventory(item); - Stat *myStats = parent->getStats(); - if ( myStats ) - { - node_t* weaponNode = itemNodeInInventory(myStats, -1, WEAPON); - if ( weaponNode ) - { - swapMonsterWeaponWithInventoryItem(parent, myStats, weaponNode, false, true); - } - } - } - playSoundEntity(parent, 431 + local_rng.rand() % 3, 92); - item = nullptr; } - list_RemoveNode(my->mynode); - return; } - - // calculate direction to caster and move. - real_t tangent = atan2(parent->y - my->y, parent->x - my->x); - real_t dist = sqrt(pow(my->x - parent->x, 2) + pow(my->y - parent->y, 2)); - real_t speed = dist / std::max(PARTICLE_LIFE, 1); - my->vel_x = speed * cos(tangent); - my->vel_y = speed * sin(tangent); - my->x += my->vel_x; - my->y += my->vel_y; - } - else - { - if ( my->skill[6] == SPELL_SUMMON ) + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_FIRE_WAVE ) { - real_t dist = sqrt(pow(my->x - my->skill[8], 2) + pow(my->y - my->skill[9], 2)); - if ( dist < 4 ) + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 2); + real_t size = 2; + for ( auto it : entLists ) { - spawnMagicEffectParticles(my->skill[8], my->skill[9], 0, my->skill[5]); - Entity* caster = uidToEntity(my->skill[7]); - if ( caster && caster->behavior == &actPlayer && stats[caster->skill[2]] ) + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) { - // kill old summons. - for ( node_t* node = stats[caster->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + Entity* entity = (Entity*)node->element; + if ( true/*entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic())*/ ) { - Entity* follower = nullptr; - if ( (Uint32*)(node)->element ) + real_t yaw = my->yaw + PI / 2; + real_t x = my->x + my->focaly * cos(yaw) - 8.0 * cos(yaw + PI / 2); + real_t y = my->y + my->focaly * sin(yaw) - 8.0 * sin(yaw + PI / 2); + + real_t tangent = yaw + PI / 2; + while ( tangent > PI ) { - follower = uidToEntity(*((Uint32*)(node)->element)); + tangent -= 2 * PI; } - if ( follower && follower->monsterAllySummonRank != 0 ) + while ( tangent <= -PI ) { - Stat* followerStats = follower->getStats(); - if ( followerStats && followerStats->HP > 0 ) - { - follower->setMP(followerStats->MAXMP * (followerStats->HP / static_cast(followerStats->MAXHP))); - follower->setHP(0); - } + tangent += 2 * PI; } - } - Monster creature = SKELETON; - Entity* monster = summonMonster(creature, my->skill[8], my->skill[9]); - if ( monster ) - { - Stat* monsterStats = monster->getStats(); - monster->yaw = my->yaw - PI; - if ( monsterStats ) + auto particleEmitterHitProps = getParticleEmitterHitProps(my->parent, entity); + if ( !particleEmitterHitProps ) { - int magicLevel = 1; - magicLevel = std::min(7, 1 + (stats[caster->skill[2]]->playerSummonLVLHP >> 16) / 5); - - monster->monsterAllySummonRank = magicLevel; - monsterStats->setAttribute("special_npc", "skeleton knight"); - strcpy(monsterStats->name, MonsterData_t::getSpecialNPCName(*monsterStats).c_str()); - forceFollower(*caster, *monster); + continue; + } + if ( particleEmitterHitProps->hits >= 5 || (particleEmitterHitProps->hits > 0 && ((ticks - particleEmitterHitProps->tick) < 20)) ) + { + continue; + } - monster->setEffect(EFF_STUNNED, true, 20, false); - bool spawnSecondAlly = false; - - if ( (caster->getINT() + stats[caster->skill[2]]->getModifiedProficiency(PRO_MAGIC)) >= SKILL_LEVEL_EXPERT ) - { - spawnSecondAlly = true; - } - //parent->increaseSkill(PRO_LEADERSHIP); - monster->monsterAllyIndex = caster->skill[2]; - if ( multiplayer == SERVER ) + if ( caster && caster->behavior == &actMonster ) + { + if ( caster == entity || caster->checkFriend(entity) ) { - serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. + continue; } + } + + int damage = getSpellDamageFromID(SPELL_FIRE_WALL, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0); + for ( int i = 0; i < 8; ++i ) + { + real_t ix = x + i * 2.0 * cos(tangent); + real_t iy = y + i * 2.0 * sin(tangent); - // change the color of the hit entity. - monster->flags[USERFLAG2] = true; - serverUpdateEntityFlag(monster, USERFLAG2); - if ( monsterChangesColorWhenAlly(monsterStats) ) + if ( ix + size > entity->x - entity->sizex ) { - int bodypart = 0; - for ( node_t* node = (monster)->children.first; node != nullptr; node = node->next ) + if ( ix - size < entity->x + entity->sizex ) { - if ( bodypart >= LIMB_HUMANOID_TORSO ) + if ( iy + size > entity->y - entity->sizey ) { - Entity* tmp = (Entity*)node->element; - if ( tmp ) + if ( iy - size < entity->y + entity->sizey ) { - tmp->flags[USERFLAG2] = true; - serverUpdateEntityFlag(tmp, USERFLAG2); - } - } - ++bodypart; - } - } - - if ( spawnSecondAlly ) - { - Entity* monster = summonMonster(creature, my->skill[8], my->skill[9]); - if ( monster ) - { - if ( multiplayer != CLIENT ) - { - spawnExplosion(monster->x, monster->y, -1); - playSoundEntity(monster, 171, 128); + //Entity* particle = spawnMagicParticle(my); + //particle->sprite = 942; + //particle->x = ix; + //particle->y = iy; + //particle->z = 0; + Stat* stats = (entity->behavior == &actPlayer || entity->behavior == &actMonster) ? entity->getStats() : nullptr; + if ( !stats || entity->isInertMimic() ) + { + if ( applyGenericMagicDamage(caster, entity, *my, SPELL_FIRE_WALL, damage, true) ) + { + if ( entity->flags[BURNABLE] && !entity->flags[BURNING] ) + { + if ( local_rng.rand() % 3 < (particleEmitterHitProps->hits + 1) ) + { + entity->SetEntityOnFire(caster); + } + } + } + else if ( entity->behavior == &actGreasePuddleSpawner ) + { + entity->SetEntityOnFire(caster); + } + particleEmitterHitProps->hits++; + particleEmitterHitProps->tick = ticks; + } + else if ( stats ) + { + if ( !entity->monsterIsTargetable() ) { continue; } - createParticleErupt(monster, 791); - serverSpawnMiscParticles(monster, PARTICLE_EFFECT_ERUPT, 791); - } + bool doKnockback = true; + bool doDamage = true; - Stat* monsterStats = monster->getStats(); - monster->yaw = my->yaw - PI; - if ( monsterStats ) - { - monsterStats->setAttribute("special_npc", "skeleton sentinel"); - strcpy(monsterStats->name, MonsterData_t::getSpecialNPCName(*monsterStats).c_str()); - magicLevel = 1; - if ( stats[caster->skill[2]] ) - { - magicLevel = std::min(7, 1 + (stats[caster->skill[2]]->playerSummon2LVLHP >> 16) / 5); - } - monster->monsterAllySummonRank = magicLevel; + if ( particleEmitterHitProps->hits >= 5 ) + { + continue; + } + if ( particleEmitterHitProps->hits >= 3 ) + { + doDamage = false; + } - forceFollower(*caster, *monster); - monster->setEffect(EFF_STUNNED, true, 20, false); + if ( caster && caster->behavior == &actPlayer ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + doDamage = false; + particleEmitterHitProps->hits++; + particleEmitterHitProps->tick = ticks; + } + } + } - monster->monsterAllyIndex = caster->skill[2]; - if ( multiplayer == SERVER ) - { - serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. - } + if ( doDamage && applyGenericMagicDamage(caster, entity, *my, SPELL_FIRE_WALL, damage, true, true) ) + { + particleEmitterHitProps->hits++; + particleEmitterHitProps->tick = ticks; - if ( caster && caster->behavior == &actPlayer ) - { - steamAchievementClient(caster->skill[2], "BARONY_ACH_SKELETON_CREW"); - } + if ( entity->flags[BURNABLE] && !entity->flags[BURNING] ) + { + if ( local_rng.rand() % 10 < (particleEmitterHitProps->hits + 1) ) + { + if ( entity->SetEntityOnFire(caster) ) + { + if ( caster ) + { + stats->burningInflictedBy = caster->getUID(); + } + entity->char_fire = std::min(entity->char_fire, getSpellEffectDurationFromID(SPELL_FIRE_WALL, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0)); + } + } + } + } - // change the color of the hit entity. - monster->flags[USERFLAG2] = true; - serverUpdateEntityFlag(monster, USERFLAG2); - if ( monsterChangesColorWhenAlly(monsterStats) ) - { - int bodypart = 0; - for ( node_t* node = (monster)->children.first; node != nullptr; node = node->next ) - { - if ( bodypart >= LIMB_HUMANOID_TORSO ) + if ( doKnockback && !stats->getEffectActive(EFF_KNOCKBACK) + /*&& !entity->flags[BURNING]*/ + && entity->setEffect(EFF_KNOCKBACK, true, 20, false) ) { - Entity* tmp = (Entity*)node->element; - if ( tmp ) + real_t tangent2 = atan2(iy - entity->y, ix - entity->x); + real_t yawDiff = tangent2 - tangent; + while ( yawDiff > PI ) { - tmp->flags[USERFLAG2] = true; - serverUpdateEntityFlag(tmp, USERFLAG2); + yawDiff -= 2 * PI; + } + while ( yawDiff <= -PI ) + { + yawDiff += 2 * PI; + } + real_t pushback = 0.3; + if ( entity->behavior == &actPlayer ) + { + entity->monsterKnockbackVelocity = pushback; + entity->monsterKnockbackTangentDir = yawDiff >= 0 ? yaw : yaw + PI; + if ( !players[entity->skill[2]]->isLocalPlayer() ) + { + serverUpdateEntityFSkill(entity, 11); + serverUpdateEntityFSkill(entity, 9); + } + } + else if ( entity->behavior == &actMonster ) + { + entity->vel_x = cos(yawDiff >= 0 ? yaw : yaw + PI) * pushback; + entity->vel_y = sin(yawDiff >= 0 ? yaw : yaw + PI) * pushback; + entity->monsterKnockbackVelocity = 0.01; + entity->monsterKnockbackTangentDir = tangent; + entity->monsterKnockbackUID = parentTimer->parent; } } - ++bodypart; } + break; } } } @@ -6882,1221 +18847,2821 @@ void actParticleSapCenter(Entity* my) } } } - list_RemoveNode(my->mynode); - return; } - - // calculate direction to caster and move. - real_t tangent = atan2(my->skill[9] - my->y, my->skill[8] - my->x); - real_t speed = dist / PARTICLE_LIFE; - my->vel_x = speed * cos(tangent); - my->vel_y = speed * sin(tangent); - my->x += my->vel_x; - my->y += my->vel_y; } - else if ( my->skill[6] == SPELL_STEAL_WEAPON ) - { - Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. - entity->flags[INVISIBLE] = true; - entity->flags[UPDATENEEDED] = true; - entity->x = my->x; - entity->y = my->y; - entity->sizex = 4; - entity->sizey = 4; - entity->yaw = my->yaw; - entity->vel_x = (local_rng.rand() % 20 - 10) / 10.0; - entity->vel_y = (local_rng.rand() % 20 - 10) / 10.0; - entity->vel_z = -.5; - entity->flags[PASSABLE] = true; - entity->flags[USERFLAG1] = true; // speeds up game when many items are dropped - entity->behavior = &actItem; - entity->skill[10] = my->skill[10]; - entity->skill[11] = my->skill[11]; - entity->skill[12] = my->skill[12]; - entity->skill[13] = my->skill[13]; - entity->skill[14] = my->skill[14]; - entity->skill[15] = my->skill[15]; - entity->itemOriginalOwner = my->itemOriginalOwner; - entity->parent = 0; - // no parent, no target to travel to. - list_RemoveNode(my->mynode); - return; + + //real_t dist = lineTraceTarget(my, x, y, tangent, 64, 0, false, entity); + /*if ( hit.entity == entity ) + { + messagePlayer(0, MESSAGE_DEBUG, "Hit"); + }*/ + } + + //my->yaw += 0.025; + if ( my->skill[6] == 1 ) // grow to scale + { + real_t increment = std::max(.05, (1.0 - my->scalex)) / 3.0; + my->scalex = std::min(my->fskill[0], my->scalex + increment); + my->scaley = std::min(my->fskill[0], my->scaley + increment); + my->scalez = std::min(my->fskill[0], my->scalez + increment); + my->focaly = my->fskill[1] * my->scalex; + //if ( Entity* particle = spawnMagicParticleCustom(my, my->sprite, my->scalex * 0.7, 1) ) + //{ + // particle->focaly = my->focaly / 2; + // particle->x = my->x; + // particle->y = my->y; + // particle->z = my->z; + // particle->ditheringOverride = my->ditheringOverride; + // //particle->vel_x = cos(my->yaw + PI / 2); + // //particle->vel_y = sin(my->yaw + PI / 2); + //} + + if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_FIRE_WAVE ) + { + if ( my->ticks == 1 ) + { + for ( int i = 0; i < 10; ++i ) + { + Entity* entity = newEntity(16, 1, map.entities, nullptr); //Sprite entity. + entity->behavior = &actFlame; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + entity->ditheringDisabled = true; + entity->flags[SPRITE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[PASSABLE] = true; + //entity->scalex = 0.25f; //MAKE 'EM SMALL PLEASE! + //entity->scaley = 0.25f; + //entity->scalez = 0.25f; + entity->vel_x = (-40 + local_rng.rand() % 81) / 8.f; + entity->vel_y = (-40 + local_rng.rand() % 81) / 8.f; + entity->vel_z = (-40 + local_rng.rand() % 81) / 8.f; + entity->skill[0] = 15 + local_rng.rand() % 10; + } + } + + if ( my->skill[7] == 1 ) + { + if ( !my->light ) + { + playSoundEntityLocal(my, 164, 128); + my->light = addLight(my->x / 16, my->y / 16, "explosion"); + } + } + } + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_KINETIC_FIELD ) + { + if ( my->skill[7] == 1 ) + { + if ( !my->light ) + { + //playSoundEntityLocal(my, 164, 128); + my->light = addLight(my->x / 16, my->y / 16, "magic_purple"); + } + } + } + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_CHRONOMIC_FIELD ) + { + if ( my->skill[7] == 1 ) + { + if ( !my->light ) + { + //playSoundEntityLocal(my, 164, 128); + my->light = addLight(my->x / 16, my->y / 16, "magic_green"); + } + } + } + + if ( my->scalex >= my->fskill[0] ) + { + my->skill[6] = 0; + my->removeLightField(); + } + } + else + { + if ( my->skill[7] == 1 ) + { + const char* color = nullptr; + const char* color_flicker = nullptr; + if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_CHRONOMIC_FIELD ) + { + color = "magic_green_flicker"; + color_flicker = "magic_green_flicker"; + } + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_KINETIC_FIELD ) + { + color = "magic_purple_flicker"; + color_flicker = "magic_purple_flicker"; + } + else if ( my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_FIRE_WAVE ) + { + color = "campfire"; + color_flicker = "campfire_flicker"; + } + if ( flickerLights ) + { + if ( my->ticks % 10 < 5 ) + { + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, color); + } + else + { + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, color_flicker); + } + } + else + { + if ( !my->light ) + { + my->light = addLight(my->x / 16, my->y / 16, color); + } + } + } + } + + if ( my->skill[7] == 1 && my->actParticleWaveMagicType == ParticleTimerEffect_t::EFFECT_FIRE_WAVE ) + { +#ifdef USE_FMOD + if ( my->skill[8] == 0 ) + { + my->skill[8]--; + my->stopEntitySound(); + my->entity_sound = playSoundEntityLocal(my, 710, 64); } - else if ( my->sprite == 977 ) + if ( my->entity_sound ) { - // calculate direction to caster and move. - real_t tangent = atan2(my->fskill[5] - my->y, my->fskill[4] - my->x); - real_t dist = sqrt(pow(my->x - my->fskill[4], 2) + pow(my->y - my->fskill[5], 2)); - real_t speed = dist / std::max(PARTICLE_LIFE, 1); - - if ( dist < 4 || (abs(my->fskill[5]) < 0.001 && abs(my->fskill[4]) < 0.001) ) + bool playing = false; + my->entity_sound->isPlaying(&playing); + if ( !playing ) { - // reached goal, or goal not set then spawn the item. - Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. - entity->flags[INVISIBLE] = true; - entity->flags[UPDATENEEDED] = true; - entity->x = my->x; - entity->y = my->y; - entity->sizex = 4; - entity->sizey = 4; - entity->yaw = my->yaw; - entity->vel_x = (local_rng.rand() % 20 - 10) / 10.0; - entity->vel_y = (local_rng.rand() % 20 - 10) / 10.0; - entity->vel_z = -.5; - entity->flags[PASSABLE] = true; - entity->flags[USERFLAG1] = true; // speeds up game when many items are dropped - entity->behavior = &actItem; - entity->skill[10] = my->skill[10]; - entity->skill[11] = my->skill[11]; - entity->skill[12] = my->skill[12]; - entity->skill[13] = my->skill[13]; - entity->skill[14] = my->skill[14]; - entity->skill[15] = my->skill[15]; - entity->itemOriginalOwner = 0; - entity->parent = 0; - - list_RemoveNode(my->mynode); - return; + my->entity_sound = nullptr; + } + else + { + FMOD_VECTOR position; + position.x = (float)(my->x / (real_t)16.0); + position.y = (float)(0.0); + position.z = (float)(my->y / (real_t)16.0); + fmod_result = my->entity_sound->set3DAttributes(&position, nullptr); } - my->vel_x = speed * cos(tangent); - my->vel_y = speed * sin(tangent); - my->x += my->vel_x; - my->y += my->vel_y; } - else +#else + my->skill[8]--; + if ( my->skill[8] <= 0 ) { - // no parent, no target to travel to. - list_RemoveNode(my->mynode); - return; + my->skill[8] = 480; + playSoundEntityLocal(my, 133, 128); } +#endif } - if ( PARTICLE_LIFE < 0 ) + if ( multiplayer != CLIENT ) { - list_RemoveNode(my->mynode); - return; + --PARTICLE_LIFE; } - else + if ( ::ticks % my->skill[5] == 0 ) { - --PARTICLE_LIFE; + my->sprite = my->skill[4] + my->skill[3]; + ++my->skill[3]; + if ( my->skill[3] >= my->skill[1] ) + { + my->skill[3] = 0; + } } } -void createParticleExplosionCharge(Entity* parent, int sprite, int particleCount, double scale) +Entity* createParticleBoobyTrapExplode(Entity* caster, real_t x, real_t y) { - if ( !parent ) + Entity* spellTimer = createParticleTimer(caster, 25, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_BOOBY_TRAP; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->x = x; + spellTimer->y = y; + + Uint32 color = makeColor(255, 0, 255, 255); + if ( Entity* fx = createParticleAOEIndicator(spellTimer, spellTimer->x, spellTimer->y, 0.0, TICKS_PER_SECOND, 32) ) { - return; + fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + Uint8 r, g, b, a; + getColor(color, &r, &g, &b, &a); + a *= 0.5; + indicator->indicatorColor = makeColor(r, g, b, a); + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->cacheType = AOEIndicators_t::CACHE_BOOBY_TRAP; + } + } + for ( int i = 0; i < 2; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(spellTimer, spellTimer->x, spellTimer->y, -7.5, TICKS_PER_SECOND, 32) ) + { + fx->actSpriteCheckParentExists = 0; + if ( i == 1 ) + { + fx->pitch = PI; + } + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 15; + indicator->cacheType = AOEIndicators_t::CACHE_BOOBY_TRAP2; + } + } } + return spellTimer; +} - for ( int c = 0; c < particleCount; c++ ) +Entity* createParticleShatterObjects(Entity* caster) +{ + if ( !caster ) { return nullptr; } + Entity* spellTimer = createParticleTimer(caster, 25, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SHATTER; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->x = caster->x; + spellTimer->y = caster->y; + + Uint32 color = makeColor(128, 128, 255, 255); + if ( Entity* fx = createParticleAOEIndicator(spellTimer, spellTimer->x, spellTimer->y, 0.0, 1.25 * TICKS_PER_SECOND, 32) ) { - // shoot drops to the sky - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = parent->x - 3 + local_rng.rand() % 7; - entity->y = parent->y - 3 + local_rng.rand() % 7; - entity->z = 0 + local_rng.rand() % 190; - if ( parent && parent->behavior == &actPlayer ) + fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) { - entity->z /= 2; + Uint8 r, g, b, a; + getColor(color, &r, &g, &b, &a); + a *= 0.5; + indicator->indicatorColor = makeColor(r, g, b, a); + indicator->loop = false; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->cacheType = AOEIndicators_t::CACHE_SHATTER_OBJECTS; } - entity->vel_z = -1; - entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->particleDuration = entity->z + 10; - /*if ( local_rng.rand() % 5 > 0 ) + } + for ( int i = 0; i < 2; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(spellTimer, spellTimer->x, spellTimer->y, -7.5, 1.25 * TICKS_PER_SECOND, 32) ) { - entity->vel_x = 0.5*cos(entity->yaw); - entity->vel_y = 0.5*sin(entity->yaw); - entity->particleDuration = 6; - entity->z = 0; - entity->vel_z = 0.5 *(-1 + local_rng.rand() % 3); - }*/ - entity->scalex *= scale; - entity->scaley *= scale; - entity->scalez *= scale; - entity->behavior = &actParticleExplosionCharge; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->parent = parent->getUID(); - if ( multiplayer != CLIENT ) + fx->actSpriteCheckParentExists = 0; + if ( i == 1 ) + { + fx->pitch = PI; + } + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 15; + indicator->cacheType = AOEIndicators_t::CACHE_SHATTER_OBJECTS2; + } + } + } + return spellTimer; +} + +Entity* createParticleIgnite(Entity* caster) +{ + if ( !caster ) { return nullptr; } + Entity* spellTimer = createParticleTimer(caster, 25, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_IGNITE; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->x = caster->x; + spellTimer->y = caster->y; + + Uint32 color = makeColor(255, 128, 0, 255); + if ( Entity* fx = createParticleAOEIndicator(spellTimer, spellTimer->x, spellTimer->y, 0.0, 1.25 * TICKS_PER_SECOND, 32) ) + { + fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) { - entity_uids--; + Uint8 r, g, b, a; + getColor(color, &r, &g, &b, &a); + a *= 0.5; + indicator->indicatorColor = makeColor(r, g, b, a); + indicator->loop = false; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->cacheType = AOEIndicators_t::CACHE_IGNITE; + } + } + for ( int i = 0; i < 2; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(spellTimer, spellTimer->x, spellTimer->y, -7.5, 1.25 * TICKS_PER_SECOND, 32) ) + { + fx->actSpriteCheckParentExists = 0; + if ( i == 1 ) + { + fx->pitch = PI; + } + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 15; + indicator->cacheType = AOEIndicators_t::CACHE_IGNITE2; + } } - entity->setUID(-3); } + return spellTimer; +} - int radius = STRIKERANGE * 2 / 3; - real_t arc = PI / 16; - int randScale = 1; - for ( int c = 0; c < 128; c++ ) +void tunnelPortalSetAttributes(Entity* portal, int duration, int dir) +{ + if ( !portal ) { return; } + + portal->z = 0.0; + portal->yaw = (dir + 2) * PI / 2; + + portal->sprite = 1810; + portal->scalex = 0.1; + portal->scaley = 0.1; + portal->scalez = 0.1; + portal->sizex = 4; + portal->sizey = 4; + portal->behavior = &actTeleporter; + portal->teleporterType = 3; + portal->teleporterStartFrame = 1810; + portal->teleporterCurrentFrame = 0; + portal->teleporterNumFrames = 4; + portal->teleporterDuration = duration; + portal->flags[PASSABLE] = true; + + if ( multiplayer != CLIENT ) { - // shoot drops to the sky - Entity* entity = newEntity(670, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->yaw = 0 + c * arc; + portal->flags[UPDATENEEDED] = true; + Uint32 val = (1 << 31); + val |= (Uint8)(26); + val |= ((Uint8)(duration & 0xFFFF) << 8); + val |= ((Uint8)(dir & 0xF) << 24); + portal->skill[2] = val; + } +} - entity->x = parent->x + (radius * cos(entity->yaw));// - 2 + local_rng.rand() % 5; - entity->y = parent->y + (radius * sin(entity->yaw));// - 2 + local_rng.rand() % 5; - entity->z = radius + 150; - entity->particleDuration = entity->z + local_rng.rand() % 3; - entity->vel_z = -1; - if ( parent && parent->behavior == &actPlayer ) +Entity* createTunnelPortal(real_t x, real_t y, int duration, int dir, Entity* caster) +{ + if ( dir == 0 ) { return nullptr; } + int checkx = x; + int checky = y; + if ( dir == 1 ) + { + checkx += 1; + } + else if ( dir == 3 ) + { + checkx -= 1; + } + else if ( dir == 2 ) + { + checky += 1; + } + else if ( dir == 4 ) + { + checky -= 1; + } + + if ( checkObstacle((checkx << 4) + 8, (checky << 4) + 8, caster, nullptr, true, true, false, false) ) + { + return nullptr; + } + + // check destination + int lastx = x; + int lasty = y; + int startx = x; + int starty = y; + checkx = x; + checky = y; + if ( !mapTileDiggable(checkx, checky) ) + { + return nullptr; + } + int walls = 0; + while ( true ) + { + bool goodspot = false; + if ( checkx > 0 && checkx < map.width - 1 && checky > 0 && checky < map.height - 1 ) { - entity->z /= 2; + int mapIndex = (checky)*MAPLAYERS + (checkx) * MAPLAYERS * map.height; + if ( !map.tiles[OBSTACLELAYER + mapIndex] ) + { + if ( !checkObstacle((checkx << 4) + 8, (checky << 4) + 8, caster, nullptr, true, true, false, false) ) + { + if ( walls > 0 ) + { + lastx = checkx; + lasty = checky; + goodspot = true; + break; + } + } + } + else + { + ++walls; + } + } + else + { + break; } - randScale = 1 + local_rng.rand() % 3; - entity->scalex *= (scale / randScale); - entity->scaley *= (scale / randScale); - entity->scalez *= (scale / randScale); - entity->behavior = &actParticleExplosionCharge; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->parent = parent->getUID(); - if ( multiplayer != CLIENT ) + if ( dir == 1 ) { - entity_uids--; + checkx -= 1; } - entity->setUID(-3); - if ( c > 0 && c % 16 == 0 ) + else if ( dir == 3 ) { - radius -= 2; + checkx += 1; + } + else if ( dir == 2 ) + { + checky -= 1; + } + else if ( dir == 4 ) + { + checky += 1; } } -} -void actParticleExplosionCharge(Entity* my) -{ - if ( PARTICLE_LIFE < 0 || (my->z < -4 && local_rng.rand() % 4 == 0) || (ticks % 14 == 0 && uidToEntity(my->parent) == nullptr) ) + if ( lastx == startx && lasty == starty ) { - list_RemoveNode(my->mynode); + return nullptr; } - else + + Entity* portal = newEntity(1810, 1, map.entities, nullptr); + const real_t wallDist = 0.5; + if ( dir == 1 ) { - --PARTICLE_LIFE; - my->yaw += 0.1; - my->x += my->vel_x; - my->y += my->vel_y; - my->z += my->vel_z; - my->scalex /= 0.99; - my->scaley /= 0.99; - my->scalez /= 0.99; - //my->z -= 0.01; + portal->x = x * 16.0 + 16.0 + wallDist; + portal->y = y * 16.0 + 8.0; + } + else if ( dir == 3 ) + { + portal->x = x * 16.0 - wallDist; + portal->y = y * 16.0 + 8.0; + } + else if ( dir == 2 ) + { + portal->x = x * 16.0 + 8.0; + portal->y = y * 16.0 + 16.0 + wallDist; + } + else if ( dir == 4 ) + { + portal->x = x * 16.0 + 8.0; + portal->y = y * 16.0 - wallDist; } - return; + tunnelPortalSetAttributes(portal, duration, dir); + if ( caster ) + { + portal->parent = caster->getUID(); + } + portal->teleporterX = lastx; + portal->teleporterY = lasty; + return portal; } -bool Entity::magicFallingCollision() +Entity* createWindMagic(Uint32 casterUID, int x, int y, int duration, int dir, int length) { - hit.entity = nullptr; - if ( z <= -5 || fabs(vel_z) < 0.01 ) + Entity* caster = uidToEntity(casterUID); + Entity* fx = createParticleAOEIndicator(caster, x, y, 0.0, duration, 32); + fx->roll = PI; + fx->z = 0.0; + fx->yaw = dir * PI / 2; + fx->actSpriteFollowUID = 0; + //fx->actSpritePitchRotate = 0.1; + fx->scalex = 0.5; + fx->scaley = 0.5; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) { - // check if particle stopped or too high. - return false; + //indicator->arc = PI / 4; + indicator->framesPerTick = 4; + indicator->ticksPerUpdate = 4; + } + if ( dir == 1 ) + { + fx->x = fx->x * 16.0 + 16.001; + fx->y = fx->y * 16.0 + 8.0; + } + else if ( dir == 3 ) + { + fx->x = fx->x * 16.0 - 0.001; + fx->y = fx->y * 16.0 + 8.0; + } + else if ( dir == 2 ) + { + fx->x = fx->x * 16.0 + 8.0; + fx->y = fx->y * 16.0 + 16.001; + } + else if ( dir == 4 ) + { + fx->x = fx->x * 16.0 + 8.0; + fx->y = fx->y * 16.0 - 0.001; } - if ( z >= 7.5 ) + Entity* wind = newEntity(-1, 1, map.entities, nullptr); + wind->behavior = &actWind; + wind->yaw = (dir - 1) * PI / 2; + if ( dir == 1 ) { - return true; + wind->x = x * 16.0 + 16.0 + 8.0; + wind->y = y * 16.0 + 8.0; + } + else if ( dir == 3 ) + { + wind->x = x * 16.0 - 16.0 + 8.0; + wind->y = y * 16.0 + 8.0; + } + else if ( dir == 2 ) + { + wind->x = x * 16.0 + 8.0; + wind->y = y * 16.0 + 16.0 + 8.0; + } + else if ( dir == 4 ) + { + wind->x = x * 16.0 + 8.0; + wind->y = y * 16.0 - 16.0 + 8.0; } + wind->flags[PASSABLE] = true; + wind->flags[INVISIBLE] = true; + wind->flags[UPDATENEEDED] = false; + wind->flags[NOUPDATE] = true; + wind->sizex = 6; + wind->sizey = 6; + wind->actWindParticleEffect = 1; + wind->actWindEffectsProjectiles = 1; + wind->actWindTileBonusLength = length; + wind->actWindLifetime = duration; + wind->parent = casterUID; + /*if ( multiplayer != CLIENT ) + { + --entity_uids; + } + wind->setUID(-3);*/ + return wind; +} - if ( actmagicIsVertical == MAGIC_ISVERTICAL_Z ) +void actRadiusMagicBadge(Entity* my) +{ + Entity* parent = uidToEntity(my->parent); + if ( !parent ) { - std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 1); - for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + for ( int i = 0; i < 4; ++i ) { - list_t* currentList = *it; - node_t* node; - for ( node = currentList->first; node != nullptr; node = node->next ) - { - Entity* entity = (Entity*)node->element; - if ( entity ) - { - if ( entity == this ) - { - continue; - } - if ( entityInsideEntity(this, entity) && !entity->flags[PASSABLE] && (entity->getUID() != this->parent) ) - { - hit.entity = entity; - //hit.side = HORIZONTAL; - return true; - } - } - } + Entity* fx = spawnMagicParticle(my); + fx->vel_x = 0.5 * cos(my->yaw + i * PI / 2); + fx->vel_y = 0.5 * sin(my->yaw + i * PI / 2); } - } - return false; -} + Entity* fx = createParticleAestheticOrbit(my, my->sprite, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE_NOSOUND); + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->yaw = my->yaw; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 1; -bool Entity::magicOrbitingCollision() -{ - hit.entity = nullptr; + list_RemoveNode(my->mynode); + return; + } - if ( this->actmagicIsOrbiting == 2 ) + if ( my->actRadiusMagicFollowUID != 0 ) { - if ( this->ticks == 5 && this->actmagicOrbitHitTargetUID4 != 0 ) + // if we want this to bubble in and out as an entity walks between the radius distance + bool invis = true; + if ( Entity* follow = uidToEntity(my->actRadiusMagicFollowUID) ) { - // hit this target automatically - Entity* tmp = uidToEntity(actmagicOrbitHitTargetUID4); - if ( tmp ) + my->x = follow->x; + my->y = follow->y; + if ( entityDist(parent, follow) <= parent->actRadiusMagicDist + 4.0 ) { - hit.entity = tmp; - return true; + invis = false; } } - if ( this->z < -8 || this->z > 3 ) - { - return false; - } - else if ( this->ticks >= 12 && this->ticks % 4 != 0 ) // check once every 4 ticks, after the missile is alive for a bit + + if ( invis && !my->flags[INVISIBLE] ) { - return false; + for ( int i = 0; i < 4; ++i ) + { + Entity* fx = spawnMagicParticle(my); + fx->vel_x = 0.5 * cos(my->yaw + i * PI / 2); + fx->vel_y = 0.5 * sin(my->yaw + i * PI / 2); + } + Entity* fx = createParticleAestheticOrbit(my, my->sprite, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE_NOSOUND); + fx->x = my->x; + fx->y = my->y; + fx->z = my->z; + fx->yaw = my->yaw; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 1; + + my->skill[1] = 0; + my->skill[3] = 0; + my->z = 4.0; + my->vel_z = -0.8; + my->scalex = 0.1; + my->scaley = 0.1; + my->scalez = 0.1; } + my->flags[INVISIBLE] = invis; } - else if ( this->z < -10 ) + else { - return false; + my->x = parent->x; + my->y = parent->y; } - if ( this->actmagicIsOrbiting == 2 ) + if ( my->flags[INVISIBLE] ) { - if ( this->actmagicOrbitStationaryHitTarget >= 3 ) - { - return false; - } + return; } - Entity* caster = uidToEntity(parent); - - std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 1); - - for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + if ( my->skill[1] >= 50 ) // stop changing size { - list_t* currentList = *it; - node_t* node; - for ( node = currentList->first; node != NULL; node = node->next ) + real_t maxspeed = .03; + real_t acceleration = 0.95; + if ( my->skill[3] == 0 ) { - Entity* entity = (Entity*)node->element; - if ( entity == this ) - { - continue; - } - if ( entity->behavior != &actMonster - && entity->behavior != &actPlayer - && entity->behavior != &actDoor - && !(entity->isDamageableCollider() && entity->isColliderDamageableByMagic()) - && !entity->isInertMimic() - && entity->behavior != &::actChest - && entity->behavior != &::actFurniture ) - { - continue; - } - if ( caster && !(svFlags & SV_FLAG_FRIENDLYFIRE) && caster->checkFriend(entity) ) - { - continue; - } - if ( actmagicIsOrbiting == 2 ) + // once off, store the normal height of the particle. + my->skill[3] = 1; + my->vel_z = -maxspeed; + } + + // bob up and down movement. + if ( my->skill[3] == 1 ) + { + my->vel_z *= acceleration; + if ( my->vel_z > -0.005 ) { - if ( static_cast(actmagicOrbitHitTargetUID1) == entity->getUID() - || static_cast(actmagicOrbitHitTargetUID2) == entity->getUID() - || static_cast(actmagicOrbitHitTargetUID3) == entity->getUID() - || static_cast(actmagicOrbitHitTargetUID4) == entity->getUID() ) - { - // we already hit these guys. - continue; - } + my->skill[3] = 2; + my->vel_z = -0.005; } - if ( entityInsideEntity(this, entity) && !entity->flags[PASSABLE] && (entity->getUID() != this->parent) ) + my->z += my->vel_z; + } + else if ( my->skill[3] == 2 ) + { + my->vel_z /= acceleration; + if ( my->vel_z < -maxspeed ) { - hit.entity = entity; - if ( hit.entity->behavior == &actMonster || hit.entity->behavior == &actPlayer ) - { - if ( actmagicIsOrbiting == 2 ) - { - if ( actmagicOrbitHitTargetUID4 != 0 && caster && caster->behavior == &actPlayer ) - { - if ( actmagicOrbitHitTargetUID1 == 0 - && actmagicOrbitHitTargetUID2 == 0 - && actmagicOrbitHitTargetUID3 == 0 - && hit.entity->behavior == &actMonster ) - { - steamStatisticUpdateClient(caster->skill[2], STEAM_STAT_VOLATILE, STEAM_STAT_INT, 1); - } - } - ++actmagicOrbitStationaryHitTarget; - if ( actmagicOrbitHitTargetUID1 == 0 ) - { - actmagicOrbitHitTargetUID1 = entity->getUID(); - } - else if ( actmagicOrbitHitTargetUID2 == 0 ) - { - actmagicOrbitHitTargetUID2 = entity->getUID(); - } - else if ( actmagicOrbitHitTargetUID3 == 0 ) - { - actmagicOrbitHitTargetUID3 = entity->getUID(); - } - } - } - return true; + my->skill[3] = 3; + my->vel_z = -maxspeed; } + my->z -= my->vel_z; } - } - - return false; -} - -void Entity::castFallingMagicMissile(int spellID, real_t distFromCaster, real_t angleFromCasterDirection, int heightDelay) -{ - spell_t* spell = getSpellFromID(spellID); - Entity* entity = castSpell(getUID(), spell, false, true); - if ( entity ) - { - entity->x = x + distFromCaster * cos(yaw + angleFromCasterDirection); - entity->y = y + distFromCaster * sin(yaw + angleFromCasterDirection); - entity->z = -25 - heightDelay; - double missile_speed = 4 * ((double)(((spellElement_t*)(spell->elements.first->element))->mana) - / ((spellElement_t*)(spell->elements.first->element))->overload_multiplier); - entity->vel_x = 0.0; - entity->vel_y = 0.0; - entity->vel_z = 0.5 * (missile_speed); - entity->pitch = PI / 2; - entity->actmagicIsVertical = MAGIC_ISVERTICAL_Z; - spawnMagicEffectParticles(entity->x, entity->y, 0, 174); - playSoundEntity(entity, spellGetCastSound(spell), 128); - } -} - -Entity* Entity::castOrbitingMagicMissile(int spellID, real_t distFromCaster, real_t angleFromCasterDirection, int duration) -{ - spell_t* spell = getSpellFromID(spellID); - Entity* entity = castSpell(getUID(), spell, false, true); - if ( entity ) - { - if ( spellID == SPELL_FIREBALL ) + else if ( my->skill[3] == 3 ) { - entity->sprite = 671; + my->vel_z *= acceleration; + if ( my->vel_z > -0.005 ) + { + my->skill[3] = 4; + my->vel_z = -0.005; + } + my->z -= my->vel_z; } - else if ( spellID == SPELL_MAGICMISSILE ) + else if ( my->skill[3] == 4 ) { - entity->sprite = 679; - } - entity->yaw = angleFromCasterDirection; - entity->x = x + distFromCaster * cos(yaw + entity->yaw); - entity->y = y + distFromCaster * sin(yaw + entity->yaw); - entity->z = -2.5; - double missile_speed = 4 * ((double)(((spellElement_t*)(spell->elements.first->element))->mana) - / ((spellElement_t*)(spell->elements.first->element))->overload_multiplier); - entity->vel_x = 0.0; - entity->vel_y = 0.0; - entity->actmagicIsOrbiting = 1; - entity->actmagicOrbitDist = distFromCaster; - entity->actmagicOrbitStartZ = entity->z; - entity->z += 4 * sin(angleFromCasterDirection); - entity->roll += (PI / 8) * (1 - abs(sin(angleFromCasterDirection))); - entity->actmagicOrbitVerticalSpeed = 0.1; - entity->actmagicOrbitVerticalDirection = 1; - entity->actmagicOrbitLifetime = duration; - entity->vel_z = entity->actmagicOrbitVerticalSpeed; - playSoundEntity(entity, spellGetCastSound(spell), 128); - //spawnMagicEffectParticles(entity->x, entity->y, 0, 174); - } - return entity; -} - -Entity* castStationaryOrbitingMagicMissile(Entity* parent, int spellID, real_t centerx, real_t centery, - real_t distFromCenter, real_t angleFromCenterDirection, int duration) -{ - spell_t* spell = getSpellFromID(spellID); - if ( !parent ) - { - Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = centerx; - entity->y = centery; - entity->z = 15; - entity->vel_z = 0; - //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->skill[0] = 100; - entity->skill[1] = 10; - entity->behavior = &actParticleDot; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->flags[INVISIBLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - parent = entity; - } - Stat* stats = parent->getStats(); - bool amplify = false; - if ( stats ) - { - amplify = stats->EFFECTS[EFF_MAGICAMPLIFY]; - stats->EFFECTS[EFF_MAGICAMPLIFY] = false; // temporary skip amplify effects otherwise recursion. - } - Entity* entity = castSpell(parent->getUID(), spell, false, true); - if ( stats ) - { - stats->EFFECTS[EFF_MAGICAMPLIFY] = amplify; + my->vel_z /= acceleration; + if ( my->vel_z < -maxspeed ) + { + my->skill[3] = 1; + my->vel_z = -maxspeed; + } + my->z += my->vel_z; + } + my->yaw += 0.01; } - if ( entity ) + else { - if ( spellID == SPELL_FIREBALL ) + my->z += my->vel_z; + my->yaw += my->vel_z * 2; + + real_t maxScale = 1.0; + real_t badgeScale = 1.0; + if ( my->sprite == 2385 || my->sprite == 2386 + || my->sprite == 2387 + || my->sprite == 2388 + || my->sprite == 2389 + || my->sprite == 2390 + || my->sprite == 2391 + || my->sprite == 2392 + || my->sprite == 2393 + || my->sprite == 2394 + || my->sprite == 2395 + || my->sprite == 2396 + || my->sprite == 2397 + || my->sprite == 2398 + || my->sprite == 2399 + || my->sprite == 2400 + || my->sprite == 2402 + || my->sprite == 2403 + || my->sprite == 2408 + || my->sprite == 2409 ) { - entity->sprite = 671; + badgeScale = 0.5; } - else if ( spellID == SPELL_COLD ) + + if ( my->scalex < badgeScale ) { - entity->sprite = 797; + my->scalex += 0.04 * badgeScale / maxScale; } - else if ( spellID == SPELL_LIGHTNING ) + else { - entity->sprite = 798; + my->scalex = badgeScale; } - else if ( spellID == SPELL_MAGICMISSILE ) + my->scaley = my->scalex; + my->scalez = my->scalex; + if ( my->z < -3 + my->fskill[0] ) { - entity->sprite = 679; + my->vel_z *= 0.9; } - entity->yaw = angleFromCenterDirection; - entity->x = centerx; - entity->y = centery; - entity->z = 4; - double missile_speed = 4 * ((double)(((spellElement_t*)(spell->elements.first->element))->mana) - / ((spellElement_t*)(spell->elements.first->element))->overload_multiplier); - entity->vel_x = 0.0; - entity->vel_y = 0.0; - entity->actmagicIsOrbiting = 2; - entity->actmagicOrbitDist = distFromCenter; - entity->actmagicOrbitStationaryCurrentDist = 0.0; - entity->actmagicOrbitStartZ = entity->z; - //entity->roll -= (PI / 8); - entity->actmagicOrbitVerticalSpeed = -0.3; - entity->actmagicOrbitVerticalDirection = 1; - entity->actmagicOrbitLifetime = duration; - entity->actmagicOrbitStationaryX = centerx; - entity->actmagicOrbitStationaryY = centery; - entity->vel_z = -0.1; - playSoundEntity(entity, spellGetCastSound(spell), 128); + } + ++my->skill[1]; +} - //spawnMagicEffectParticles(entity->x, entity->y, 0, 174); +void radiusMagicClientReceive(Entity* entity) +{ + if ( entity ) + { + entity->flags[NOUPDATE] = (entity->skill[2] >> 30) & 1; + entity->actRadiusMagicDist = (entity->skill[2] >> 20) & 0xFF; + entity->actRadiusMagicID = (entity->skill[2] >> 8) & 0xFFF; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->flags[INVISIBLE] = true; } - return entity; } -void createParticleFollowerCommand(real_t x, real_t y, real_t z, int sprite, Uint32 uid) +void radiusMagicSetUID(Entity& fx, bool noupdate) { - Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Particle entity. - //entity->sizex = 1; - //entity->sizey = 1; + Sint32 val = (1 << 31); + if ( noupdate ) + { + val |= (1 << 30); + } + val |= (Uint8)(24); + val |= (((Uint16)(fx.actRadiusMagicID) & 0xFFF) << 8); + val |= (((Uint8)(fx.actRadiusMagicDist) & 0xFF) << 20); + fx.skill[2] = val; +} + +Entity* createRadiusMagic(int spellID, Entity* caster, real_t x, real_t y, real_t radius, Uint32 lifetime, Entity* follow) +{ + if ( !caster ) + { + return nullptr; + } + + int sprite = -1; + switch ( spellID ) + { + case SPELL_NULL_AREA: + sprite = 1817; + break; + case SPELL_SPHERE_SILENCE: + sprite = 1818; + break; + case SPELL_HEAL_PULSE: + sprite = 1686; + break; + case SPELL_FOCI_DARK_LIFE: + sprite = 2179; + break; + case SPELL_FOCI_DARK_SILENCE: + sprite = 2180; + break; + case SPELL_FOCI_DARK_SUPPRESS: + sprite = 2181; + break; + case SPELL_FOCI_DARK_VENGEANCE: + sprite = 2182; + break; + case SPELL_FOCI_DARK_RIFT: + sprite = 2183; + break; + case SPELL_FOCI_LIGHT_JUSTICE: + sprite = 2184; + break; + case SPELL_FOCI_LIGHT_PEACE: + sprite = 2185; + break; + case SPELL_FOCI_LIGHT_PROVIDENCE: + sprite = 2186; + break; + case SPELL_FOCI_LIGHT_PURITY: + sprite = 2187; + break; + case SPELL_FOCI_LIGHT_SANCTUARY: + sprite = 2188; + break; + case SPELL_HEAL_MINOR: + case SPELL_HEAL_OTHER: + sprite = 2384; + break; + case SPELL_DIVINE_ZEAL: + sprite = 2385; + break; + case SPELL_PROF_STURDINESS: + sprite = 2386; + break; + case SPELL_PROF_GREATER_MIGHT: + sprite = 2387; + break; + case SPELL_PROF_NIMBLENESS: + sprite = 2388; + break; + case SPELL_PROF_COUNSEL: + sprite = 2389; + break; + case SPELL_COMMAND: + sprite = 2390; + break; + case SPELL_SEEK_ALLY: + sprite = 2392; + break; + case SPELL_SEEK_FOE: + sprite = 2393; + break; + case SPELL_TABOO: + sprite = 2391; + break; + case SPELL_DETECT_ENEMY: + sprite = 2395; + break; + case SPELL_SCRY_ALLIES: + sprite = 2394; + break; + case SPELL_DONATION: + sprite = 2396; + break; + case SPELL_BLESS_FOOD: + sprite = 2400; + break; + case SPELL_SCRY_TRAPS: + sprite = 2399; + break; + case SPELL_SCRY_TREASURES: + sprite = 2398; + break; + case SPELL_SCRY_SHRINES: + sprite = 2397; + break; + case SPELL_PINPOINT: + sprite = 2402; + break; + case SPELL_TURN_UNDEAD: + sprite = 2403; + break; + case SPELL_SIGIL: + sprite = 2404; + break; + case SPELL_SANCTUARY: + sprite = 2405; + break; + case SPELL_MAGICMAPPING: + sprite = 2408; + break; + case SPELL_FORGE_JEWEL: + sprite = 2409; + break; + default: + break; + } + + if ( sprite == -1 ) + { + return nullptr; + } + + Entity* entity = newEntity(sprite, 1, map.entities, nullptr); //Sprite entity. entity->x = x; entity->y = y; - entity->z = 0; + entity->z = 7.5; + entity->yaw = 0.0; + entity->skill[0] = lifetime; + entity->parent = caster->getUID(); + entity->actRadiusMagicID = spellID; + entity->actRadiusMagicDist = radius; + if ( follow ) + { + entity->actRadiusMagicFollowUID = follow->getUID(); + } + entity->behavior = &actRadiusMagic; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->flags[INVISIBLE] = true; + bool noupdate = !follow; + radiusMagicSetUID(*entity, noupdate); + return entity; +} + +Entity* createMagicRadiusBadge(Entity& parent) +{ + Entity* entity = newEntity(parent.sprite, 1, map.entities, nullptr); //Particle entity. + entity->parent = parent.getUID(); + entity->x = parent.x; + entity->y = parent.y; + static ConsoleVariable cvar_magic_radius_badge1("/magic_radius_badge1", 4.0); + static ConsoleVariable cvar_magic_radius_badge2("/magic_radius_badge2", 0.0); + entity->z = 4.0; + int mapx = entity->x / 16; + int mapy = entity->y / 16; + if ( mapx >= 0 && mapx < map.width && mapy >= 0 && mapy < map.height ) + { + if ( !map.tiles[(MAPLAYERS - 1) + mapy * MAPLAYERS + mapx * MAPLAYERS * map.height] ) + { + // no ceiling + entity->fskill[0] = *cvar_magic_radius_badge2; + } + else + { + entity->fskill[0] = *cvar_magic_radius_badge1; + } + } + else + { + entity->fskill[0] = *cvar_magic_radius_badge2; + } entity->vel_z = -0.8; - //entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->skill[0] = 50; - entity->skill[1] = 0; - entity->parent = uid; - entity->fskill[1] = entity->z; // start z - entity->behavior = &actParticleFollowerCommand; - entity->scalex = 0.8; - entity->scaley = 0.8; - entity->scalez = 0.8; + entity->scalex = 0.1; + entity->scaley = 0.1; + entity->scalez = 0.1; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->behavior = &actRadiusMagicBadge; entity->flags[PASSABLE] = true; entity->flags[NOUPDATE] = true; entity->flags[UNCLICKABLE] = true; - entity->flags[BRIGHT] = true; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); if ( multiplayer != CLIENT ) { entity_uids--; } entity->setUID(-3); + return entity; +} - // boosty boost - for ( int c = 0; c < 10; c++ ) +void actRadiusMagicOnFade(Entity* my) +{ + Entity* caster = uidToEntity(my->parent); + Entity* follow = uidToEntity(my->actRadiusMagicFollowUID); + if ( caster ) { - entity = newEntity(174, 1, map.entities, nullptr); //Particle entity. - entity->x = x - 4 + local_rng.rand() % 9; - entity->y = y - 4 + local_rng.rand() % 9; - entity->z = z - 0 + local_rng.rand() % 11; - entity->scalex = 0.5; - entity->scaley = 0.5; - entity->scalez = 0.5; - entity->sizex = 1; - entity->sizey = 1; - entity->yaw = (local_rng.rand() % 360) * PI / 180.f; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->flags[BRIGHT] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - entity->behavior = &actMagicParticle; - entity->vel_z = -1; - if ( multiplayer != CLIENT ) + if ( my->actRadiusMagicID == SPELL_HEAL_OTHER ) { - entity_uids--; + if ( follow && (follow != caster) && follow->getStats() ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 255, 255), *follow->getStats(), + Language::get(6927), Language::get(6926), MSG_COMBAT); + } + } + else if ( my->actRadiusMagicID == SPELL_HEAL_MINOR ) + { + if ( follow == caster ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_STATUS, Language::get(6931)); + } } - entity->setUID(-3); } - } -static ConsoleVariable cvar_follower_particle_speed("/follower_particle_speed", 2.0); -void actParticleFollowerCommand(Entity* my) +void actRadiusMagic(Entity* my) { - if ( PARTICLE_LIFE < 0 ) + Entity* caster = nullptr; + if ( multiplayer != CLIENT ) { + caster = uidToEntity(my->parent); + } + if ( PARTICLE_LIFE < 0 || (!caster && multiplayer != CLIENT) ) + { + my->removeLightField(); list_RemoveNode(my->mynode); return; } - else + + if ( multiplayer != CLIENT ) { - --PARTICLE_LIFE; - if ( my->parent != 0 ) + if ( my->actRadiusMagicID == SPELL_HEAL_MINOR || my->actRadiusMagicID == SPELL_HEAL_OTHER ) { - if ( Entity* parent = uidToEntity(my->parent) ) + if ( caster && caster->getStats() ) { - my->x = parent->x; - my->y = parent->y; + if ( !caster->getStats()->getEffectActive(EFF_HEALING_WORD) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, makeColorRGB(255, 0, 0), Language::get(6933)); + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } } } - my->z += my->vel_z; - my->yaw += (std::min(my->vel_z * 2, -0.1)) / *cvar_follower_particle_speed; - if ( my->z < (my->fskill[1] - 3) ) + + if ( my->actRadiusMagicFollowUID != 0 ) { - my->skill[1] = 1; - my->vel_z *= 0.9; + Entity* follow = uidToEntity(my->actRadiusMagicFollowUID); + if ( !follow ) + { + my->removeLightField(); + list_RemoveNode(my->mynode); + return; + } + else + { + my->x = follow->x; + my->y = follow->y; + } } - if ( my->skill[1] != 0 && abs(my->vel_z) < 0.1 ) + + //if ( my->actRadiusMagicID == SPELL_FOCI_DARK_LIFE + // || my->actRadiusMagicID == SPELL_FOCI_DARK_RIFT + // || my->actRadiusMagicID == SPELL_FOCI_DARK_SILENCE + // || my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS + // || my->actRadiusMagicID == SPELL_FOCI_DARK_VENGEANCE ) + //{ + // bool endEffect = false; + // if ( Stat* casterStats = caster->getStats() ) + // { + // if ( !casterStats->defending ) + // { + // endEffect = true; + + // Entity* spellEntity = createParticleSapCenter(caster, my, 0, 2178, 2178); + // if ( spellEntity ) + // { + // spellEntity->skill[0] = 25; // duration + // spellEntity->skill[7] = my->getUID(); + // } + + // my->removeLightField(); + // list_RemoveNode(my->mynode); + // return; + // } + // } + //} + } + else + { + if ( my->actRadiusMagicFollowUID != 0 ) { - my->z += .1 * sin(my->fskill[0]); - my->fskill[0] += 1.0 * PI / 32; - /*if ( my->fskill[0] > PI / 2 ) + if ( Entity* follow = uidToEntity(my->actRadiusMagicFollowUID) ) { - my->scalex *= .9; - my->scaley *= .9; - my->scalez *= .9; - }*/ + my->x = follow->x; + my->y = follow->y; + my->new_x = my->x; // skip dead reckoning interpolation since we know the follow target + my->new_y = my->y; + } } } -} -void actParticleShadowTag(Entity* my) -{ - if ( PARTICLE_LIFE < 0 ) + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, magicLightColorForSprite(my, my->sprite, false)); + + my->flags[INVISIBLE] = true; + + bool refireLoop = true; + if ( my->actRadiusMagicID == SPELL_FOCI_DARK_LIFE + || my->actRadiusMagicID == SPELL_FOCI_DARK_RIFT + || my->actRadiusMagicID == SPELL_FOCI_DARK_SILENCE + || my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS + || my->actRadiusMagicID == SPELL_FOCI_DARK_VENGEANCE ) { - // once off, fire some erupt dot particles at end of life. - real_t yaw = 0; - int numParticles = 8; - for ( int c = 0; c < 8; c++ ) + refireLoop = false; + } + + if ( my->actRadiusMagicInit == 0 + || (refireLoop && ((ticks - my->actRadiusMagicDoPulseTick) == 1) + || (my->actRadiusMagicAutoPulseTick > 0 && my->actRadiusMagicDoPulseTick > 0 && + (ticks - my->actRadiusMagicDoPulseTick) % my->actRadiusMagicAutoPulseTick == 1)) ) + { + if ( Entity* fx = createParticleAOEIndicator(my, my->x, my->y, 0.0, TICKS_PER_SECOND * 99, my->actRadiusMagicDist) ) { - Entity* entity = newEntity(871, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = my->x; - entity->y = my->y; - entity->z = -10 + my->fskill[0]; - entity->yaw = yaw; - entity->vel_x = 0.2; - entity->vel_y = 0.2; - entity->vel_z = -0.02; - entity->skill[0] = 100; - entity->skill[1] = 0; // direction. - entity->fskill[0] = 0.1; - entity->behavior = &actParticleErupt; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) + //fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + Uint32 color = 0xFFFFFFFF; + switch ( my->sprite ) + { + case 1817: + color = makeColorRGB(195, 48, 165); + break; + case 1818: + color = makeColorRGB(124, 107, 209); + break; + case 1686: + case 2384: + color = makeColorRGB(252, 107, 35); + break; + case 2179: + case 2180: + case 2181: + case 2182: + case 2183: + color = makeColorRGB(132, 47, 241); + break; + case 2390: + case 2391: + case 2392: + case 2393: + color = makeColorRGB(102, 117, 204); + break; + case 2404: + case 2409: + color = makeColorRGB(252, 208, 88); + break; + case 2405: + color = makeColorRGB(104, 188, 252); + break; + default: + break; + } + Uint8 r, g, b, a; + getColor(color, &r, &g, &b, &a); + a *= 0.8; + indicator->indicatorColor = makeColor(r, g, b, a); + indicator->loop = true; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->gradient = 4; + indicator->delayTicks = 0; + indicator->radiusMin = 4; + indicator->radius = 4; + indicator->loopType = 1; + indicator->loopTimer = 50; + indicator->cacheType = AOEIndicators_t::CACHE_RADIUS_MAGIC_GENERIC; + + if ( my->actRadiusMagicID == SPELL_HEAL_PULSE ) + { + indicator->loopTimer = 3.5 * TICKS_PER_SECOND; + } + else if ( my->actRadiusMagicID == SPELL_HEAL_MINOR || my->actRadiusMagicID == SPELL_HEAL_OTHER ) + { + //indicator->loopTimer = 3.5 * TICKS_PER_SECOND; + fx->scalex = 0.8; + fx->scaley = 0.8; + } + else if ( my->actRadiusMagicID == SPELL_FOCI_DARK_LIFE + || my->actRadiusMagicID == SPELL_FOCI_DARK_RIFT + || my->actRadiusMagicID == SPELL_FOCI_DARK_SILENCE + || my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS + || my->actRadiusMagicID == SPELL_FOCI_DARK_VENGEANCE ) + { + indicator->loop = false; + //indicator->lifetime = 100; + //indicator->loopType = 0; + //indicator->loopTimer = 0; + } + else if ( my->actRadiusMagicID == SPELL_FOCI_LIGHT_PEACE + || my->actRadiusMagicID == SPELL_FOCI_LIGHT_JUSTICE + || my->actRadiusMagicID == SPELL_FOCI_LIGHT_PROVIDENCE + || my->actRadiusMagicID == SPELL_FOCI_LIGHT_PURITY + || my->actRadiusMagicID == SPELL_FOCI_LIGHT_SANCTUARY ) + { + indicator->loop = false; + indicator->loopType = 0; + indicator->loopTimer = 0; + } + else if ( + my->actRadiusMagicID == SPELL_DIVINE_ZEAL + || my->actRadiusMagicID == SPELL_PROF_COUNSEL + || my->actRadiusMagicID == SPELL_PROF_GREATER_MIGHT + || my->actRadiusMagicID == SPELL_PROF_NIMBLENESS + || my->actRadiusMagicID == SPELL_PROF_STURDINESS ) + { + fx->scalex = 0.8; + fx->scaley = 0.8; + indicator->loop = false; + indicator->loopType = 0; + indicator->loopTimer = 0; + } + else if ( + my->actRadiusMagicID == SPELL_COMMAND + || my->actRadiusMagicID == SPELL_SEEK_ALLY + || my->actRadiusMagicID == SPELL_SEEK_FOE + || my->actRadiusMagicID == SPELL_TABOO + || my->actRadiusMagicID == SPELL_DETECT_ENEMY + || my->actRadiusMagicID == SPELL_SCRY_ALLIES + || my->actRadiusMagicID == SPELL_DONATION + || my->actRadiusMagicID == SPELL_BLESS_FOOD + || my->actRadiusMagicID == SPELL_SCRY_TRAPS + || my->actRadiusMagicID == SPELL_SCRY_TREASURES + || my->actRadiusMagicID == SPELL_SCRY_SHRINES + || my->actRadiusMagicID == SPELL_PINPOINT + || my->actRadiusMagicID == SPELL_MAGICMAPPING + || my->actRadiusMagicID == SPELL_FORGE_JEWEL + ) + { + fx->scalex = 0.8; + fx->scaley = 0.8; + indicator->loop = false; + indicator->loopType = 0; + indicator->loopTimer = 0; + } + } + } + + if ( my->actRadiusMagicID == SPELL_FOCI_DARK_LIFE + || my->actRadiusMagicID == SPELL_FOCI_DARK_RIFT + || my->actRadiusMagicID == SPELL_FOCI_DARK_SILENCE + || my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS + || my->actRadiusMagicID == SPELL_FOCI_DARK_VENGEANCE ) + { + createParticleFociDark(my, my->actRadiusMagicID, false); + } + spawnMagicEffectParticles(my->x, my->y, my->z, my->sprite); + if ( my->actRadiusMagicInit == 0 ) + { + createMagicRadiusBadge(*my); + + if ( my->actRadiusMagicID == SPELL_TURN_UNDEAD + || my->actRadiusMagicID == SPELL_SIGIL + || my->actRadiusMagicID == SPELL_SANCTUARY ) { - entity_uids--; + createParticleFociLight(my, my->actRadiusMagicID, false); } - entity->setUID(-3); - yaw += 2 * PI / numParticles; } + my->actRadiusMagicInit = 1; + } - if ( multiplayer != CLIENT ) + my->vel_x = 0.0; + my->vel_y = 0.0; + + bool checkArea = true; + if ( my->actRadiusMagicID == SPELL_HEAL_PULSE ) + { + checkArea = false; + if ( my->ticks % (4 * TICKS_PER_SECOND) == 1 ) { - Uint32 casterUid = static_cast(my->skill[2]); - Entity* caster = uidToEntity(casterUid); - Entity* parent = uidToEntity(my->parent); - if ( caster && caster->behavior == &actPlayer - && parent ) + checkArea = true; + } + } + else if ( my->actRadiusMagicID == SPELL_HEAL_MINOR || my->actRadiusMagicID == SPELL_HEAL_OTHER ) + { + checkArea = false; + int interval = getSpellEffectDurationSecondaryFromID(my->actRadiusMagicID, caster, nullptr, caster, my->actmagicSpellbookBonus / 100.0); + if ( my->ticks % (interval) == 1 && my->ticks >= interval ) + { + checkArea = true; + } + } + else if ( my->actRadiusMagicID == SPELL_FOCI_DARK_LIFE + || my->actRadiusMagicID == SPELL_FOCI_DARK_RIFT + || my->actRadiusMagicID == SPELL_FOCI_DARK_SILENCE + || my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS + || my->actRadiusMagicID == SPELL_FOCI_DARK_VENGEANCE ) + { + checkArea = false; + if ( my->ticks == 1 || (ticks - my->actRadiusMagicDoPulseTick) == 1 ) + { + checkArea = true; + } + else if ( my->actRadiusMagicAutoPulseTick > 0 && my->actRadiusMagicDoPulseTick > 0 && + (ticks - my->actRadiusMagicDoPulseTick) % my->actRadiusMagicAutoPulseTick == 1 ) + { + checkArea = true; + } + + //if ( my->actRadiusMagicFollowUID == 0 ) + //{ + // real_t prevDist = 10000.0; + // Entity* target = nullptr; + // /*if ( my->actmagicOrbitHitTargetUID4 != 0 ) + // { + // target = uidToEntity(my->actmagicOrbitHitTargetUID4); + // }*/ + // if ( !target ) + // { + // for ( node_t* node = map.creatures->first; node; node = node->next ) + // { + // if ( Entity* entity = (Entity*)node->element ) + // { + // if ( Stat* entityStats = entity->getStats() ) + // { + // if ( caster->checkEnemy(entity) && entity->monsterIsTargetable() ) + // { + // real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + // real_t targetdist = sqrt(pow(my->x - entity->x, 2) + pow(my->y - entity->y, 2)); + // real_t dist = lineTraceTarget(my, my->x, my->y, tangent, 64.0, 0, true, entity); + // if ( hit.entity == entity && dist < prevDist ) + // { + // prevDist = dist; + // target = entity; + // } + // } + // } + // } + // } + // } + + // if ( target ) + // { + // my->actRadiusMagicFollowUID = target->getUID(); + // serverUpdateEntitySkill(my, 5); // update follow UID + // //real_t dist = entityDist(my, target); + // //if ( dist < 8.0 ) + // //{ + // // my->actRadiusMagicFollowUID = target->getUID(); + // // serverUpdateEntitySkill(my, 5); // update follow UID + // //} + // //else + // //{ + // // real_t tangent = atan2(target->y - my->y, target->x - my->x); + // // real_t speed = std::min(dist, std::max(16.0, 64.0 - dist) / 100.0); + // // my->vel_x = speed * cos(tangent); + // // my->vel_y = speed * sin(tangent); + // //} + + // //my->x += my->vel_x; + // //my->y += my->vel_y; + // } + //} + } + else if ( my->actRadiusMagicID == SPELL_FOCI_LIGHT_PEACE + || my->actRadiusMagicID == SPELL_FOCI_LIGHT_JUSTICE + || my->actRadiusMagicID == SPELL_FOCI_LIGHT_PROVIDENCE + || my->actRadiusMagicID == SPELL_FOCI_LIGHT_PURITY + || my->actRadiusMagicID == SPELL_FOCI_LIGHT_SANCTUARY + || my->actRadiusMagicID == SPELL_DIVINE_ZEAL + || my->actRadiusMagicID == SPELL_PROF_COUNSEL + || my->actRadiusMagicID == SPELL_PROF_GREATER_MIGHT + || my->actRadiusMagicID == SPELL_PROF_NIMBLENESS + || my->actRadiusMagicID == SPELL_PROF_STURDINESS + || my->actRadiusMagicID == SPELL_COMMAND + || my->actRadiusMagicID == SPELL_SEEK_ALLY + || my->actRadiusMagicID == SPELL_SEEK_FOE + || my->actRadiusMagicID == SPELL_TABOO + || my->actRadiusMagicID == SPELL_DETECT_ENEMY + || my->actRadiusMagicID == SPELL_SCRY_ALLIES + || my->actRadiusMagicID == SPELL_DONATION + || my->actRadiusMagicID == SPELL_BLESS_FOOD + || my->actRadiusMagicID == SPELL_SCRY_TRAPS + || my->actRadiusMagicID == SPELL_SCRY_TREASURES + || my->actRadiusMagicID == SPELL_SCRY_SHRINES + || my->actRadiusMagicID == SPELL_PINPOINT + || my->actRadiusMagicID == SPELL_MAGICMAPPING + || my->actRadiusMagicID == SPELL_FORGE_JEWEL + ) + { + checkArea = false; // visual effect only + } + + if ( multiplayer == SERVER ) + { + if ( my->ticks >= *cvar_entity_bodypart_sync_tick ) + { + if ( (my->ticks - *cvar_entity_bodypart_sync_tick) % (2 * TICKS_PER_SECOND) == 0 ) { - // caster is alive, notify they lost their mark - Uint32 color = makeColorRGB(255, 255, 255); - if ( parent->getStats() ) + if ( my->actRadiusMagicFollowUID != 0 ) { - messagePlayerMonsterEvent(caster->skill[2], color, *(parent->getStats()), Language::get(3466), Language::get(3467), MSG_COMBAT); - parent->setEffect(EFF_SHADOW_TAGGED, false, 0, true); + serverUpdateEntitySkill(my, 5); // update follow UID } } } - my->removeLightField(); - list_RemoveNode(my->mynode); - return; } - else + + if ( multiplayer != CLIENT ) { --PARTICLE_LIFE; - my->removeLightField(); - my->light = addLight(my->x / 16, my->y / 16, colorForSprite(my, my->sprite, true)); - - Entity* parent = uidToEntity(my->parent); - if ( parent ) + if ( PARTICLE_LIFE == -1 ) { - my->x = parent->x; - my->y = parent->y; + actRadiusMagicOnFade(my); } - if ( my->skill[1] >= 50 ) // stop changing size + if ( checkArea ) { - real_t maxspeed = .03; - real_t acceleration = 0.95; - if ( my->skill[3] == 0 ) - { - // once off, store the normal height of the particle. - my->skill[3] = 1; - my->vel_z = -maxspeed; - } - if ( my->skill[1] % 5 == 0 ) + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(my, 1 + my->actRadiusMagicDist / 16); + std::vector applyEffects; + for ( auto it : entLists ) { - Uint32 casterUid = static_cast(my->skill[2]); - Entity* caster = uidToEntity(casterUid); - if ( caster && caster->creatureShadowTaggedThisUid == my->parent && parent ) - { - // caster is alive, and they have still marked the parent this particle is following. - } - else + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) { - PARTICLE_LIFE = 0; + if ( Entity* entity = (Entity*)node->element ) + { + if ( my->actRadiusMagicID == SPELL_HEAL_PULSE ) + { + if ( entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic()) ) + { + if ( entity->monsterIsTargetable() ) + { + if ( entityDist(my, entity) <= (real_t)my->actRadiusMagicDist + 4.0 ) + { + if ( caster && caster->checkFriend(entity) ) + { + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + /*if ( props->hits > 5 ) + { + continue; + }*/ + props->hits++; + applyEffects.push_back(entity); + } + } + } + } + } + else if ( my->actRadiusMagicID == SPELL_HEAL_MINOR || my->actRadiusMagicID == SPELL_HEAL_OTHER ) + { + if ( entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic()) ) + { + if ( my->actRadiusMagicFollowUID == entity->getUID() ) + { + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + props->hits++; + applyEffects.push_back(entity); + } + } + } + else if ( my->actRadiusMagicID == SPELL_NULL_AREA ) + { + if ( entity->behavior == &actMagicMissile ) + { + if ( entityDist(my, entity) <= (real_t)my->actRadiusMagicDist + 4.0 ) + { + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 ) + { + continue; + } + props->hits++; + applyEffects.push_back(entity); + } + } + } + else if ( my->actRadiusMagicID == SPELL_SPHERE_SILENCE ) + { + if ( entity->behavior == &actMagicMissile ) + { + if ( entityDist(my, entity) <= (real_t)my->actRadiusMagicDist + 4.0 ) + { + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 ) + { + continue; + } + props->hits++; + Entity* parent = uidToEntity(entity->parent); + if ( parent && entityDist(my, parent) < (real_t)my->actRadiusMagicDist + 4.0 ) + { + applyEffects.push_back(entity); + } + } + } + } + else if ( my->actRadiusMagicID == SPELL_SANCTUARY ) + { + if ( entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic()) ) + { + if ( entity->monsterIsTargetable() ) + { + if ( entityDist(my, entity) <= (real_t)my->actRadiusMagicDist + 4.0 ) + { + if ( (caster && caster->checkFriend(entity)) ) + { + /*if ( entity->getStats() && entity->getStats()->getEffectActive(EFF_SANCTUARY) + && entity->getStats()->EFFECTS_TIMERS[EFF_SANCTUARY] > TICKS_PER_SECOND ) + { + continue; + }*/ + + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 && (ticks - props->tick) < TICKS_PER_SECOND / 2 ) + { + continue; + } + props->hits++; + props->tick = ticks; + applyEffects.push_back(entity); + } + } + } + } + } + else if ( my->actRadiusMagicID == SPELL_SIGIL ) + { + if ( entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic()) ) + { + if ( entity->monsterIsTargetable() ) + { + if ( entityDist(my, entity) <= (real_t)my->actRadiusMagicDist + 4.0 ) + { + /*if ( caster && caster != entity && caster->checkEnemy(entity) ) + { + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + real_t dist = lineTraceTarget(my, my->x, my->y, tangent, my->actRadiusMagicDist, 0, false, entity); + if ( hit.entity == entity ) + { + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 && (ticks - props->tick) < TICKS_PER_SECOND / 2 ) + { + continue; + } + props->hits++; + props->tick = ticks; + applyEffects.push_back(entity); + } + } + else*/ + { + /*if ( entity->getStats() && entity->getStats()->getEffectActive(EFF_SIGIL) + && entity->getStats()->EFFECTS_TIMERS[EFF_SIGIL] > TICKS_PER_SECOND ) + { + continue; + }*/ + + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 && (ticks - props->tick) < TICKS_PER_SECOND / 2 ) + { + continue; + } + + + props->hits++; + props->tick = ticks; + applyEffects.push_back(entity); + } + } + } + } + } + else if ( my->actRadiusMagicID == SPELL_TURN_UNDEAD ) + { + if ( entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic()) ) + { + if ( entity->monsterIsTargetable() && entity->isSmiteWeakMonster() ) + { + if ( entityDist(my, entity) <= (real_t)my->actRadiusMagicDist + 4.0 ) + { + if ( caster && caster != entity && caster->checkEnemy(entity) ) + { + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + real_t dist = lineTraceTarget(my, my->x, my->y, tangent, my->actRadiusMagicDist + 4.0, 0, false, entity); + if ( hit.entity == entity ) + { + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + if ( props->hits > 0 ) + { + continue; + } + props->hits++; + applyEffects.push_back(entity); + } + } + } + } + } + } + else if ( my->actRadiusMagicID == SPELL_FOCI_DARK_RIFT + || my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS ) + { + if ( entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic()) ) + { + if ( entity->monsterIsTargetable() ) + { + if ( entityDist(my, entity) <= (real_t)my->actRadiusMagicDist + 4.0 ) + { + if ( caster && caster != entity && caster->checkEnemy(entity) ) + { + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + real_t dist = lineTraceTarget(my, my->x, my->y, tangent, my->actRadiusMagicDist + 4.0, 0, false, entity); + if ( hit.entity == entity ) + { + auto props = getParticleEmitterHitProps(my->getUID(), entity); + if ( !props ) + { + continue; + } + props->hits++; + applyEffects.push_back(entity); + } + } + } + } + } + } + else + { + /*if ( entity->behavior == &actPlayer || (entity->behavior == &actMonster && !entity->isInertMimic()) ) + { + if ( entity->monsterIsTargetable() ) + { + if ( entityDist(my, entity) <= (real_t)my->actRadiusMagicDist ) + { + if ( caster->checkFriend(entity) || caster == entity ) + { + entity->setEffect(EFF_SLOW, true, 15, false); + } + } + } + }*/ + } + } } } - if ( PARTICLE_LIFE > 0 && PARTICLE_LIFE < TICKS_PER_SECOND ) + /*if ( my->actRadiusMagicID == SPELL_FOCI_DARK_LIFE ) { - if ( parent && parent->getStats() && parent->getStats()->EFFECTS[EFF_SHADOW_TAGGED] ) + while ( applyEffects.size() > 1 ) { - ++PARTICLE_LIFE; + unsigned int pick = local_rng.rand() % applyEffects.size(); + applyEffects.erase(applyEffects.begin() + pick); } - } - // bob up and down movement. - if ( my->skill[3] == 1 ) + }*/ + + bool firstEffect = true; + int totalHeal = 0; + for ( auto ent : applyEffects ) { - my->vel_z *= acceleration; - if ( my->vel_z > -0.005 ) + if ( my->actRadiusMagicID == SPELL_HEAL_PULSE ) { - my->skill[3] = 2; - my->vel_z = -0.005; + int amount = getSpellDamageFromID(SPELL_HEAL_PULSE, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0); + if ( firstEffect ) + { + playSoundEntity(my, 168, 128); + spawnMagicEffectParticles(my->x, my->y, my->z, 169); + } + + int oldHP = ent->getHP(); + spell_changeHealth(ent, amount); + int heal = std::max(ent->getHP() - oldHP, 0); + totalHeal += heal; + if ( heal > 0 ) + { + spawnDamageGib(ent, -heal, DamageGib::DMG_HEAL, DamageGibDisplayType::DMG_GIB_NUMBER, true); + if ( caster ) + { + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(my->actRadiusMagicID, 30.0, 1.0, nullptr); + } + //magicOnSpellCastEvent(caster, caster, uidToEntity(ent->parent), my->actRadiusMagicID, + // spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + } + spawnMagicEffectParticles(ent->x, ent->y, ent->z, 169); + firstEffect = false; } - my->z += my->vel_z; - } - else if ( my->skill[3] == 2 ) - { - my->vel_z /= acceleration; - if ( my->vel_z < -maxspeed ) + else if ( my->actRadiusMagicID == SPELL_HEAL_MINOR || my->actRadiusMagicID == SPELL_HEAL_OTHER ) { - my->skill[3] = 3; - my->vel_z = -maxspeed; + int amount = getSpellDamageFromID(my->actRadiusMagicID, caster, nullptr, my, my->actmagicSpellbookBonus / 100.0); + /*if ( firstEffect ) + { + playSoundEntity(my, 168, 128); + spawnMagicEffectParticles(my->x, my->y, my->z, 169); + }*/ + + int oldHP = ent->getHP(); + spell_changeHealth(ent, amount, false, false); + int heal = std::max(ent->getHP() - oldHP, 0); + totalHeal += heal; + if ( heal > 0 ) + { + spawnDamageGib(ent, -heal, DamageGib::DMG_HEAL, DamageGibDisplayType::DMG_GIB_NUMBER, true); + if ( caster ) + { + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(my->actRadiusMagicID, 25.0, 1.0, nullptr); + } + //magicOnSpellCastEvent(caster, caster, uidToEntity(ent->parent), my->actRadiusMagicID, + // spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + } + //spawnMagicEffectParticles(ent->x, ent->y, ent->z, 169); + firstEffect = false; + } + else if ( my->actRadiusMagicID == SPELL_NULL_AREA || my->actRadiusMagicID == SPELL_SPHERE_SILENCE ) + { + Entity* fx = createParticleAestheticOrbit(my, my->sprite, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE); + fx->x = ent->x; + fx->y = ent->y; + fx->z = ent->z; + real_t tangent = atan2(ent->y - my->y, ent->x - my->x); + fx->yaw = tangent; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 0; + serverSpawnMiscParticlesAtLocation(fx->x, fx->y, fx->z, PARTICLE_EFFECT_NULL_PARTICLE, my->sprite, 0, fx->yaw * 256.0); + + if ( caster ) + { + magicOnSpellCastEvent(caster, caster, uidToEntity(ent->parent), my->actRadiusMagicID, + spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + + if ( caster->behavior == &actPlayer ) + { + if ( ent->behavior == &actMagicMissile ) + { + if ( ent->children.first ) + { + if ( spell_t* spell = (spell_t*)ent->children.first->element ) + { + if ( Entity* spellCaster = uidToEntity(spell->caster) ) + { + if ( spellCaster->behavior == &actMonster ) + { + int type = spellCaster->getMonsterTypeFromSprite(); + if ( type == LICH || type == LICH_FIRE || type == LICH_ICE ) + { + serverUpdatePlayerGameplayStats(caster->skill[2], STATISTICS_THATS_CHEATING, 1); + } + } + } + } + } + } + } + } + + ent->removeLightField(); + list_RemoveNode(ent->mynode); + firstEffect = false; } - my->z -= my->vel_z; - } - else if ( my->skill[3] == 3 ) - { - my->vel_z *= acceleration; - if ( my->vel_z > -0.005 ) + else if ( my->actRadiusMagicID == SPELL_SIGIL ) { - my->skill[3] = 4; - my->vel_z = -0.005; + int effectDuration = getSpellEffectDurationSecondaryFromID(SPELL_SIGIL, caster, nullptr, my); + Uint8 effectStrength = std::min(getSpellDamageSecondaryFromID(SPELL_SIGIL, caster, nullptr, my), + std::max(1, getSpellDamageFromID(SPELL_SIGIL, caster, nullptr, my))); + + if ( caster && caster->behavior == &actPlayer ) + { + effectStrength |= ((caster->skill[2] + 1) << 4); + } + else + { + effectStrength |= ((MAXPLAYERS + 1) << 4); + } + + if ( Stat* entitystats = ent->getStats() ) + { + if ( !entitystats->getEffectActive(EFF_SIGIL) ) + { + if ( ent->setEffect(EFF_SIGIL, effectStrength, effectDuration, false, true, true) ) + { + spawnMagicEffectParticles(ent->x, ent->y, ent->z, 174); + firstEffect = false; + + /*if ( Entity* fx = createMagicRadiusBadge(*my) ) + { + fx->actRadiusMagicFollowUID = ent->getUID(); + }*/ + } + } + else + { + if ( entitystats->EFFECTS_TIMERS[EFF_SIGIL] < TICKS_PER_SECOND ) + { + spawnMagicEffectParticles(ent->x, ent->y, ent->z, 174); + } + entitystats->setEffectActive(EFF_SIGIL, effectStrength); + entitystats->EFFECTS_TIMERS[EFF_SIGIL] = effectDuration; + firstEffect = false; + } + } } - my->z -= my->vel_z; - } - else if ( my->skill[3] == 4 ) - { - my->vel_z /= acceleration; - if ( my->vel_z < -maxspeed ) + else if ( my->actRadiusMagicID == SPELL_SANCTUARY ) { - my->skill[3] = 1; - my->vel_z = -maxspeed; + int effectDuration = getSpellEffectDurationSecondaryFromID(SPELL_SANCTUARY, caster, nullptr, my); + Uint8 effectStrength = std::min(getSpellDamageSecondaryFromID(SPELL_SANCTUARY, caster, nullptr, my), + std::max(1, getSpellDamageFromID(SPELL_SANCTUARY, caster, nullptr, my))); + + if ( caster && caster->behavior == &actPlayer ) + { + effectStrength |= ((caster->skill[2] + 1) << 4); + } + else + { + effectStrength |= ((MAXPLAYERS + 1) << 4); + } + + if ( Stat* entitystats = ent->getStats() ) + { + if ( !entitystats->getEffectActive(EFF_SANCTUARY) ) + { + if ( ent->setEffect(EFF_SANCTUARY, effectStrength, effectDuration, false, true, true) ) + { + spawnMagicEffectParticles(ent->x, ent->y, ent->z, 174); + firstEffect = false; + + /*if ( Entity* fx = createMagicRadiusBadge(*my) ) + { + fx->actRadiusMagicFollowUID = ent->getUID(); + }*/ + } + } + else + { + if ( entitystats->EFFECTS_TIMERS[EFF_SANCTUARY] < TICKS_PER_SECOND ) + { + spawnMagicEffectParticles(ent->x, ent->y, ent->z, 174); + } + entitystats->setEffectActive(EFF_SANCTUARY, effectStrength); + entitystats->EFFECTS_TIMERS[EFF_SANCTUARY] = effectDuration; + firstEffect = false; + } + } } - my->z += my->vel_z; - } - my->yaw += 0.01; - } - else - { - my->z += my->vel_z; - my->yaw += my->vel_z * 2; - if ( my->scalex < 0.5 ) - { - my->scalex += 0.02; - } - else - { - my->scalex = 0.5; - } - my->scaley = my->scalex; - my->scalez = my->scalex; - if ( my->z < -3 + my->fskill[0] ) - { - my->vel_z *= 0.9; - } - } + else if ( my->actRadiusMagicID == SPELL_TURN_UNDEAD ) + { + int effectDuration = getSpellEffectDurationSecondaryFromID(SPELL_TURN_UNDEAD, caster, nullptr, my); + int effectStrength = getSpellDamageSecondaryFromID(SPELL_TURN_UNDEAD, caster, nullptr, my); + if ( effectStrength >= 2 ) + { + effectDuration *= 2; + } - // once off, fire some erupt dot particles at start. - if ( my->skill[1] == 0 ) - { - real_t yaw = 0; - int numParticles = 8; - for ( int c = 0; c < 8; c++ ) - { - Entity* entity = newEntity(871, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = my->x; - entity->y = my->y; - entity->z = -10 + my->fskill[0]; - entity->yaw = yaw; - entity->vel_x = 0.2; - entity->vel_y = 0.2; - entity->vel_z = -0.02; - entity->skill[0] = 100; - entity->skill[1] = 0; // direction. - entity->fskill[0] = 0.1; - entity->behavior = &actParticleErupt; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) + if ( effectStrength >= 2 ) + { + if ( Entity* fx1 = createParticleAestheticOrbit(ent, 2401, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_TURN_UNDEAD) ) + { + fx1->yaw = ent->yaw; + fx1->fskill[4] = ent->x; + fx1->fskill[5] = ent->y; + fx1->x = ent->x; + fx1->y = ent->y; + fx1->fskill[6] = fx1->yaw; + fx1->skill[3] = caster ? caster->getUID() : 0; + if ( effectStrength >= 3 ) + { + fx1->skill[6] = EFF_HOLY_FIRE; + } + fx1->actmagicSpellbookBonus = my->actmagicSpellbookBonus; + fx1->actmagicFromSpellbook = my->actmagicFromSpellbook; + } + + serverSpawnMiscParticles(ent, PARTICLE_EFFECT_TURN_UNDEAD, 2401); + + if ( firstEffect ) + { + if ( caster ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6497)); + } + } + firstEffect = false; + } + if ( ent->setEffect(EFF_FEAR, true, effectDuration, true) + || (effectStrength >= 2 && ent->setEffect(EFF_PARALYZED, true, effectDuration / 2, false)) ) + { + playSoundEntity(ent, 687, 128); // fear.ogg + spawnMagicEffectParticles(ent->x, ent->y, ent->z, 174); + + ent->monsterAcquireAttackTarget(*my, MONSTER_STATE_WAIT); + ent->monsterFearfulOfUid = my->getUID(); + + if ( firstEffect ) + { + if ( caster ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6497)); + } + } + firstEffect = false; + } + + if ( !firstEffect ) + { + if ( caster ) + { + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_TURN_UNDEAD, 100.0, 1.0, ent); + } + } + } + } + else if ( my->actRadiusMagicID == SPELL_FOCI_DARK_RIFT + || my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS ) { - entity_uids--; + Entity* spellEntity = createParticleSapCenter(ent, my, 0, 2178, 2178); + if ( spellEntity ) + { + /*spellEntity->x = ent->x; + spellEntity->y = ent->y;*/ + spellEntity->skill[0] = 25; // duration + spellEntity->skill[7] = my->getUID(); + } + int effectID = EFF_STASIS; + if ( my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS ) + { + effectID = EFF_ROOTED; + } + if ( Stat* entStats = ent->getStats() ) + { + int duration = my->actRadiusMagicEffectPower; + if ( Uint8 effectStrength = entStats->getEffectActive(effectID) ) + { + duration = std::max(entStats->EFFECTS_TIMERS[effectID], duration); + duration = std::min(duration, getSpellEffectDurationSecondaryFromID(my->actRadiusMagicID, caster, nullptr, caster)); + } + if ( duration > 0 ) + { + bool updateClients = effectID == EFF_STASIS; + if ( ent->setEffect(effectID, true, duration, updateClients) ) + { + if ( my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS ) + { + if ( Entity* spellTimer = createParticleTimer(caster, duration, -1) ) + { + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE_VOID; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->x = ent->x; + spellTimer->y = ent->y; + } + } + + if ( caster->behavior == &actPlayer ) + { + if ( my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS ) + { + Compendium_t::Events_t::eventUpdate(caster->skill[2], Compendium_t::CPDM_SPELL_TARGETS, + TOOL_FOCI_DARK_SUPPRESS, 1); + } + else if ( my->actRadiusMagicID == SPELL_FOCI_DARK_RIFT ) + { + Compendium_t::Events_t::eventUpdate(caster->skill[2], Compendium_t::CPDM_SPELL_TARGETS, + TOOL_FOCI_DARK_RIFT, 1); + } + } + + if ( firstEffect ) + { + if ( caster ) + { + if ( my->actRadiusMagicID == SPELL_FOCI_DARK_SUPPRESS ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6841)); + } + else + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6840)); + } + } + } + firstEffect = false; + } + } + } } - entity->setUID(-3); - yaw += 2 * PI / numParticles; } - } - ++my->skill[1]; - } -} -void createParticleShadowTag(Entity* parent, Uint32 casterUid, int duration) -{ - if ( !parent ) - { - return; - } - Entity* entity = newEntity(870, 1, map.entities, nullptr); //Particle entity. - entity->parent = parent->getUID(); - entity->x = parent->x; - entity->y = parent->y; - entity->z = 7.5; - entity->fskill[0] = parent->z; - entity->vel_z = -0.8; - entity->scalex = 0.1; - entity->scaley = 0.1; - entity->scalez = 0.1; - entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->skill[0] = duration; - entity->skill[1] = 0; - entity->skill[2] = static_cast(casterUid); - entity->skill[3] = 0; - entity->behavior = &actParticleShadowTag; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - /*entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f);*/ - if ( multiplayer != CLIENT ) - { - entity_uids--; - } - entity->setUID(-3); -} + if ( totalHeal > 0 ) + { + int player = caster ? caster->isEntityPlayer() : -1; + serverUpdatePlayerGameplayStats(player, STATISTICS_HEAL_BOT, totalHeal); + //Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELL_HEAL, SPELL_ITEM, totalHeal, false, my->actRadiusMagicID); -void createParticleCharmMonster(Entity* parent) -{ - if ( !parent ) - { - return; - } - Entity* entity = newEntity(685, 1, map.entities, nullptr); //Particle entity. - //entity->sizex = 1; - //entity->sizey = 1; - entity->parent = parent->getUID(); - entity->x = parent->x; - entity->y = parent->y; - entity->z = 7.5; - entity->vel_z = -0.8; - entity->scalex = 0.1; - entity->scaley = 0.1; - entity->scalez = 0.1; - entity->yaw = (local_rng.rand() % 360) * PI / 180.0; - entity->skill[0] = 45; - entity->behavior = &actParticleCharmMonster; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) - { - entity_uids--; + if ( player >= 0 ) + { + int healingSpellID = my->actRadiusMagicID; + if ( healingSpellID == SPELL_HEAL_PULSE ) + { + healingSpellID = SPELL_SHRUB; + } + if ( my->actmagicFromSpellbook ) + { + auto find = ItemTooltips.spellItems.find(healingSpellID); + if ( find != ItemTooltips.spellItems.end() ) + { + if ( find->second.spellbookId >= 0 && find->second.spellbookId < NUMITEMS && items[find->second.spellbookId].category == SPELLBOOK ) + { + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELL_HEAL, (ItemType)find->second.spellbookId, totalHeal); + } + } + } + else + { + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELL_HEAL, SPELL_ITEM, totalHeal, false, healingSpellID); + } + } + } + } } - entity->setUID(-3); } -void actParticleCharmMonster(Entity* my) +Entity* createSpellExplosionArea(int spellID, Entity* caster, real_t x, real_t y, real_t z, real_t radius, int damage, Entity* ohitentity) { - if ( PARTICLE_LIFE < 0 ) + Entity* spellTimer = nullptr; + if ( multiplayer != CLIENT ) { - real_t yaw = 0; - int numParticles = 8; - for ( int c = 0; c < 8; c++ ) + spellTimer = createParticleTimer(caster, 5, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_DAMAGE_LOS_AREA; + spellTimer->particleTimerCountdownSprite = -1; + + if ( damage == 0 ) { - Entity* entity = newEntity(576, 1, map.entities, nullptr); //Particle entity. - entity->sizex = 1; - entity->sizey = 1; - entity->x = my->x; - entity->y = my->y; - entity->z = -10; - entity->yaw = yaw; - entity->vel_x = 0.2; - entity->vel_y = 0.2; - entity->vel_z = -0.02; - entity->skill[0] = 100; - entity->skill[1] = 0; // direction. - entity->fskill[0] = 0.1; - entity->behavior = &actParticleErupt; - entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; - entity->flags[UNCLICKABLE] = true; - entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, - *cvar_magic_fx_light_bonus, 0.f); - if ( multiplayer != CLIENT ) - { - entity_uids--; - } - entity->setUID(-3); - yaw += 2 * PI / numParticles; + damage = getSpellDamageFromID(spellID, caster, nullptr, caster); } - list_RemoveNode(my->mynode); - return; + + spellTimer->particleTimerVariable1 = damage; // damage + spellTimer->particleTimerVariable2 = spellID; + spellTimer->particleTimerVariable3 = radius + 4.0; + spellTimer->particleTimerVariable4 = ohitentity ? ohitentity->getUID() : 0; + spellTimer->yaw = 0.0; + spellTimer->x = x; + spellTimer->y = y; + spellTimer->z = z; } - else + + if ( spellID == SPELL_EARTH_ELEMENTAL ) { - --PARTICLE_LIFE; - Entity* parent = uidToEntity(my->parent); - if ( parent ) - { - my->x = parent->x; - my->y = parent->y; - } - my->z += my->vel_z; - my->yaw += my->vel_z * 2; - if ( my->scalex < 0.8 ) - { - my->scalex += 0.02; - } - else + spellTimer->particleTimerDuration = 2; + + if ( multiplayer != CLIENT ) { - my->scalex = 0.8; + Uint32 color = makeColorRGB(112, 104, 96); + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticlesAtLocation(x, y, z, PARTICLE_EFFECT_EARTH_ELEMENTAL_SUMMON_AOE, 0, radius, color); + } + if ( Entity* fx = createParticleAOEIndicator(spellTimer, x, y, 0.0, TICKS_PER_SECOND, radius) ) + { + fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + } + } } - my->scaley = my->scalex; - my->scalez = my->scalex; - if ( my->z < -3 ) + return spellTimer; + } + if ( spellID == SPELL_PROJECT_SPIRIT ) + { + spellTimer->particleTimerDuration = 2; + + if ( multiplayer != CLIENT ) { - my->vel_z *= 0.9; + Uint32 color = makeColorRGB(112, 104, 96); + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticlesAtLocation(x, y, z, PARTICLE_EFFECT_EARTH_ELEMENTAL_SUMMON_AOE, 0, radius, color); + } + if ( Entity* fx = createParticleAOEIndicator(spellTimer, x, y, 0.0, TICKS_PER_SECOND, radius) ) + { + fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.975; + } + } } + return spellTimer; + } + if ( spellID == SPELL_ETERNALS_GAZE ) + { + spawnExplosionFromSprite(135, + spellTimer->x - 2.0 + local_rng.rand() % 5, + spellTimer->y - 2.0 + local_rng.rand() % 5, + -4.0 + local_rng.rand() % 9); + return spellTimer; } -} -void spawnMagicTower(Entity* parent, real_t x, real_t y, int spellID, Entity* autoHitTarget, bool castedSpell) -{ - bool autoHit = false; - if ( autoHitTarget && (autoHitTarget->behavior == &actPlayer || autoHitTarget->behavior == &actMonster) ) + Uint32 color = makeColor(255, 128, 0, 255); + for ( int i = 0; i < 4; ++i ) { - autoHit = true; - if ( parent ) + if ( Entity* fx = createParticleAOEIndicator(spellTimer, x, y, -7.5, TICKS_PER_SECOND, radius) ) { - if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) && parent->checkFriend(autoHitTarget) ) + //fx->yaw = my->yaw + PI / 2; + if ( i == 0 ) { - autoHit = false; // don't hit friendlies + if ( multiplayer != CLIENT ) + { + spawnExplosion(fx->x, fx->y, 0.0); + } + } + if ( i >= 2 ) + { + fx->pitch -= PI / 8; + } + else + { + fx->pitch += PI / 8; + } + if ( i % 2 == 1 ) + { + fx->pitch += PI; + } + fx->z = 0.0; + fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + fx->fskill[0] = 0.125; // rotate + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + //indicator->arc = PI / 2; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.9; + indicator->cacheType = AOEIndicators_t::CACHE_EXPLOSION_AREA; } } } - Entity* orbit = castStationaryOrbitingMagicMissile(parent, spellID, x, y, 16.0, 0.0, 40); - if ( orbit ) + + if ( Entity* fx = createParticleAOEIndicator(spellTimer, x, y, 0.0, TICKS_PER_SECOND, radius) ) { - if ( castedSpell ) - { - orbit->actmagicOrbitCastFromSpell = 1; - } - if ( autoHit ) + fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) { - orbit->actmagicOrbitHitTargetUID4 = autoHitTarget->getUID(); + //indicator->arc = PI / 2; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + indicator->expireAlphaRate = 0.9; + indicator->cacheType = AOEIndicators_t::CACHE_EXPLOSION_AREA; } } - orbit = castStationaryOrbitingMagicMissile(parent, spellID, x, y, 16.0, 2 * PI / 3, 40); - if ( orbit ) + + if ( spellID == SPELL_METEOR || spellID == SPELL_METEOR_SHOWER || spellID == SPELL_FIREBALL ) { - if ( castedSpell ) - { - orbit->actmagicOrbitCastFromSpell = 1; - } - if ( autoHit ) + playSoundPosLocal(x, y, 819, 128); + for ( int i = 0; i < 8; ++i ) { - orbit->actmagicOrbitHitTargetUID4 = autoHitTarget->getUID(); + if ( Entity* fx = createParticleAestheticOrbit(nullptr, 233, 25 + (10 * (local_rng.rand() % 6)), PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + real_t rad = radius * 0.25 * (1 + (i / 2)); + real_t ang = 2 * PI * (i / 4.0) + (i >= 4 ? PI / 4 : 0.0); + fx->x = x + rad * cos(ang); + fx->y = y + rad * sin(ang); + fx->z = 6.0 + -2.0 + local_rng.rand() % 5; + fx->fskill[0] = x; + fx->fskill[1] = y; + fx->fskill[2] = ang; + fx->fskill[3] += (local_rng.rand() % 10) * PI / 10.0; + fx->z = 7.5; + fx->vel_z = -0.1 + (local_rng.rand() % 10) * -.025; + fx->actmagicOrbitDist = rad; + } + //Entity* entity = newEntity(233, 1, map.entities, nullptr); //Sprite entity. + //entity->z = 6.0; + //entity->ditheringDisabled = true; + //entity->flags[SPRITE] = true; + //entity->flags[PASSABLE] = true; + //entity->flags[NOUPDATE] = true; + //entity->flags[UNCLICKABLE] = true; + //entity->flags[BRIGHT] = true; + //entity->behavior = &actSprite; + //entity->skill[0] = 1; + //entity->skill[1] = 12; + //entity->skill[2] = 4; + //entity->vel_z = -0.25; + //if ( multiplayer != CLIENT ) + //{ + // entity_uids--; + //} + //entity->setUID(-3); } } - orbit = castStationaryOrbitingMagicMissile(parent, spellID, x, y, 16.0, 4 * PI / 3, 40); - if ( orbit ) + + serverSpawnMiscParticlesAtLocation(x, y, z, PARTICLE_EFFECT_AREA_EFFECT, spellID, radius); + return spellTimer; +} + +void doSpellExplosionArea(int spellID, Entity* my, Entity* caster, real_t x, real_t y, real_t z, real_t radius) +{ + if ( my->particleTimerVariable4 != 0 ) { - if ( castedSpell ) - { - orbit->actmagicOrbitCastFromSpell = 1; - } - if ( autoHit ) + if ( Entity* ignoreEntity = uidToEntity(my->particleTimerVariable4) ) { - orbit->actmagicOrbitHitTargetUID4 = autoHitTarget->getUID(); + if ( auto hitProps = getParticleEmitterHitProps(my->getUID(), ignoreEntity) ) + { + hitProps->hits++; + } } } - spawnMagicEffectParticles(x, y, 0, 174); - spawnExplosion(x, y, -4 + local_rng.rand() % 8); -} -bool magicDig(Entity* parent, Entity* projectile, int numRocks, int randRocks) -{ - if ( !hit.entity ) + std::vector entLists = TileEntityList.getEntitiesWithinRadius(x / 16, y / 16, 1 + (radius / 16)); + for ( auto it : entLists ) { - if ( map.tiles[(int)(OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height)] != 0 ) + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) { - if ( MFLAG_DISABLEDIGGING ) + Entity* entity = (Entity*)node->element; + if ( entityDist(my, entity) > radius ) { - if ( parent && parent->behavior == &actPlayer ) + continue; + } + if ( entity == my ) + { + continue; + } + bool mimic = entity->isInertMimic(); + + if ( caster && caster->getStats() ) + { + if ( caster->behavior == &actMonster && !caster->monsterAllyGetPlayerLeader() ) { - Uint32 color = makeColorRGB(255, 0, 255); - messagePlayerColor(parent->skill[2], MESSAGE_HINT, color, Language::get(2380)); // disabled digging. + if ( caster->checkFriend(entity) ) + { + continue; + } + } + if ( caster->behavior == &actPlayer ) + { + if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + { + if ( caster->checkFriend(entity) && caster->friendlyFireProtection(entity) ) + { + continue; + } + } } - playSoundPos(hit.x, hit.y, 66, 128); // strike wall } - else if ( swimmingtiles[map.tiles[OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height]] - || lavatiles[map.tiles[OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height]] ) + + auto hitProps = getParticleEmitterHitProps(my->getUID(), entity); + if ( !hitProps ) { - // no effect for lava/water tiles. + continue; } - else if ( !mapTileDiggable(hit.mapx, hit.mapy) ) + if ( hitProps->hits > 0 ) { - if ( parent && parent->behavior == &actPlayer ) + continue; + } + + real_t tangent = atan2(entity->y - my->y, entity->x - my->x); + bool oldPassable = entity->flags[PASSABLE]; + entity->flags[PASSABLE] = false; + real_t d = lineTraceTarget(my, my->x, my->y, tangent, radius, 0, false, entity); + entity->flags[PASSABLE] = oldPassable; + if ( hit.entity != entity ) + { + continue; + } + + if ( entity->behavior == &actGreasePuddleSpawner ) + { + if ( spellID != SPELL_EARTH_ELEMENTAL && spellID != SPELL_PROJECT_SPIRIT ) { - messagePlayer(parent->skill[2], MESSAGE_HINT, Language::get(706)); + ++hitProps->hits; + entity->SetEntityOnFire(caster); } - playSoundPos(hit.x, hit.y, 66, 128); // strike wall } else { - if ( projectile ) + int damage = my->particleTimerVariable1; + if ( entity->getStats() ) { - playSoundEntity(projectile, 66, 128); - playSoundEntity(projectile, 67, 128); + if ( spellID != SPELL_ETERNALS_GAZE && spellID != SPELL_EARTH_ELEMENTAL && spellID != SPELL_PROJECT_SPIRIT ) + { + if ( !entity->monsterIsTargetable(true) ) { continue; } + } + else if ( entity->flags[PASSABLE] ) + { + continue; + } + } + if ( applyGenericMagicDamage(caster, entity, *my, spellID, damage, true) ) + { + ++hitProps->hits; + if ( spellID != SPELL_EARTH_ELEMENTAL && spellID != SPELL_PROJECT_SPIRIT ) + { + if ( entity->SetEntityOnFire(caster) ) + { + if ( entity->getStats() ) + { + if ( caster ) + { + entity->getStats()->burningInflictedBy = caster->getUID(); + } + } + } + } } + } + } + } +} - // spawn several rock items - if ( randRocks <= 0 ) +void createParticleSpin(Entity* entity) +{ + if ( entity ) + { + constexpr auto color = makeColor(255, 255, 255, 255); + for ( int i = 0; i < 24; ++i ) + { + if ( Entity* fx = createParticleAOEIndicator(entity, entity->x, entity->y, -7.5, TICKS_PER_SECOND * 5, 16 + (i / 2) * 2) ) + { + fx->yaw = entity->yaw + PI / 2 - (i / 2) * PI / 3; + fx->pitch += PI / 32; + if ( i % 2 == 1 ) { - randRocks = 1; + fx->pitch += PI; } - int i = numRocks + local_rng.rand() % randRocks; - for ( int c = 0; c < i; c++ ) + fx->z = 8.0; + fx->z -= (i / 2) * 0.5; + fx->vel_z -= 0.25; + fx->fskill[0] = 0.3; // rotate + fx->scalex = 0.5;// + (i / 2) * 0.25 / 12; + fx->scaley = 0.5;// + (i / 2) * 0.25 / 12; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) { - Entity* rock = newEntity(-1, 1, map.entities, nullptr); //Rock entity. - rock->flags[INVISIBLE] = true; - rock->flags[UPDATENEEDED] = true; - rock->x = hit.mapx * 16 + 4 + local_rng.rand() % 8; - rock->y = hit.mapy * 16 + 4 + local_rng.rand() % 8; - rock->z = -6 + local_rng.rand() % 12; - rock->sizex = 4; - rock->sizey = 4; - rock->yaw = local_rng.rand() % 360 * PI / 180; - rock->vel_x = (local_rng.rand() % 20 - 10) / 10.0; - rock->vel_y = (local_rng.rand() % 20 - 10) / 10.0; - rock->vel_z = -.25 - (local_rng.rand() % 5) / 10.0; - rock->flags[PASSABLE] = true; - rock->behavior = &actItem; - rock->flags[USERFLAG1] = true; // no collision: helps performance - rock->skill[10] = GEM_ROCK; // type - rock->skill[11] = WORN; // status - rock->skill[12] = 0; // beatitude - rock->skill[13] = 1; // count - rock->skill[14] = 0; // appearance - rock->skill[15] = 1; // identified + indicator->arc = PI / 4; + indicator->indicatorColor = color; + indicator->loop = false; + indicator->framesPerTick = 1; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; } + } + } + } +} + +void actParticleShatterEarth(Entity* my) +{ + if ( PARTICLE_LIFE < 0 ) + { + list_RemoveNode(my->mynode); + return; + } + + --PARTICLE_LIFE; + + bool noground = false; + bool tallCeiling = false; + int x = my->x / 16; + int y = my->y / 16; + if ( x < 0 || x >= map.width || y < 0 || y >= map.height ) + { + noground = true; + } + else + { + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( !map.tiles[mapIndex] || swimmingtiles[map.tiles[mapIndex]] + || lavatiles[map.tiles[mapIndex]] ) + { + noground = true; + } - if ( map.tiles[(int)(OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height)] >= 41 - && map.tiles[(int)(OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height)] <= 49 ) - { - steamAchievementEntity(parent, "BARONY_ACH_BAD_REVIEW"); - } + if ( !map.tiles[(MAPLAYERS - 1) + mapIndex] ) + { + tallCeiling = true; + } + } - map.tiles[(int)(OBSTACLELAYER + hit.mapy * MAPLAYERS + hit.mapx * MAPLAYERS * map.height)] = 0; + if ( my->sprite == 1869 ) + { + my->x = my->fskill[0]; + my->y = my->fskill[1]; + if ( my->ticks < TICKS_PER_SECOND + my->actmagicDelayMove ) + { + if ( my->ticks >= TICKS_PER_SECOND / 2 ) + { + my->fskill[2] = (local_rng.rand() % 31 - 15) / 50.f; + my->fskill[3] = (local_rng.rand() % 31 - 15) / 50.f; + my->x += my->fskill[2]; + my->y += my->fskill[3]; + } + { + real_t diff = std::max(0.01, (my->fskill[4] - my->z) / 10); + my->z = std::min(my->z + diff, my->fskill[4]); + } - // send wall destroy info to clients - if ( multiplayer == SERVER ) + my->scalex = std::min(1.0, my->scalex + 0.05); + my->scaley = my->scalex; + my->scalez = my->scalex; + } + + int tallCeilingDelay = 0; + if ( tallCeiling ) + { + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + static ConsoleVariable cvar_se9("/se9", -5); + tallCeilingDelay = *cvar_se9; + } + else + { + tallCeilingDelay = -5; + } + } + + static ConsoleVariable cvar_se4("/se4", 20); + int boulderSpawn = 20 + tallCeilingDelay; + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + boulderSpawn = *cvar_se4 + tallCeilingDelay; + } + boulderSpawn += my->actmagicDelayMove; + if ( my->ticks == 1 ) + { + playSoundEntity(my, 799, 128); + } + else if ( my->ticks == boulderSpawn ) + { + if ( multiplayer != CLIENT ) + { + if ( my->skill[1] == SPELL_SHATTER_EARTH ) { - for ( int c = 1; c < MAXPLAYERS; c++ ) + Entity* entity = newEntity(245, 1, map.entities, nullptr); // boulder + entity->parent = my->getUID(); + entity->x = static_cast(my->x / 16) * 16.0 + 8.0; + entity->y = static_cast(my->y / 16) * 16.0 + 8.0; + entity->z = -64; + entity->yaw = 0.0; + entity->sizex = 7; + entity->sizey = 7; + entity->behavior = &actBoulder; + entity->boulderShatterEarthSpell = my->parent; + entity->boulderShatterEarthDamage = getSpellDamageFromID(SPELL_SHATTER_EARTH, uidToEntity(my->parent), nullptr, uidToEntity(my->parent)); + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + + playSoundEntity(entity, 150, 128); + playSoundPlayer(clientnum, 150, 64); + for ( int c = 0; c < MAXPLAYERS; c++ ) { - if ( players[c]->isLocalPlayer() || client_disconnected[c] == true ) + if ( players[c]->isLocalPlayer() ) { - continue; + inputs.addRumbleForHapticType(c, Inputs::HAPTIC_SFX_BOULDER_LAUNCH_VOL, entity->getUID()); + } + else + { + playSoundPlayer(c, 150, 64); + inputs.addRumbleRemotePlayer(c, Inputs::HAPTIC_SFX_BOULDER_LAUNCH_VOL, entity->getUID()); + } + } + } + else if ( my->skill[1] == SPELL_EARTH_ELEMENTAL ) + { + Entity* caster = uidToEntity(my->parent); + if ( auto monster = summonMonsterNoSmoke(EARTH_ELEMENTAL, static_cast(my->x / 16) * 16.0 + 8.0, + static_cast(my->y / 16) * 16.0 + 8.0) ) + { + if ( caster ) + { + if ( forceFollower(*caster, *monster) ) + { + if ( caster->behavior == &actPlayer ) + { + //Compendium_t::Events_t::eventUpdateMonster(caster->skill[2], Compendium_t::CPDM_RECRUITED, monster, 1); + monster->monsterAllyIndex = caster->skill[2]; + monster->monsterAllySummonRank = 1; + monster->flags[USERFLAG2] = true; + serverUpdateEntityFlag(monster, USERFLAG2); + if ( Stat* monsterStats = monster->getStats() ) + { + monsterStats->setAttribute("SUMMONED_CREATURE", "1"); + monsterStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; + int lvl = getSpellDamageFromID(SPELL_EARTH_ELEMENTAL, caster, nullptr, caster); + int maxlvl = getSpellDamageSecondaryFromID(SPELL_EARTH_ELEMENTAL, caster, nullptr, caster); + lvl = std::min(lvl, maxlvl); + monsterStats->LVL = lvl; + } + if ( caster->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateMonster(caster->skill[2], Compendium_t::CPDM_RECRUITED, monster, 1); + } + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. + serverUpdateEntitySkill(monster, 50); // update monsterAllySummonRank + } + } + } } - strcpy((char*)net_packet->data, "WALD"); - SDLNet_Write16((Uint16)hit.mapx, &net_packet->data[4]); - SDLNet_Write16((Uint16)hit.mapy, &net_packet->data[6]); - net_packet->address.host = net_clients[c - 1].host; - net_packet->address.port = net_clients[c - 1].port; - net_packet->len = 8; - sendPacketSafe(net_sock, -1, net_packet, c - 1); } } - - generatePathMaps(); - return true; } } - return false; - } - else if ( hit.entity->behavior == &actColliderDecoration && hit.entity->colliderDiggable != 0 ) - { - int sprite = EditorEntityData_t::colliderData[hit.entity->colliderDamageTypes].gib; - if ( sprite > 0 ) + + static ConsoleVariable cvar_se5("/se5", 45); + if ( my->ticks == *cvar_se5 + tallCeilingDelay + my->actmagicDelayMove ) { - createParticleRock(hit.entity, sprite); - if ( multiplayer == SERVER ) + for ( int i = 0; i < 8; ++i ) { - serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_ABILITY_ROCK, sprite); + Entity* entity = newEntity(78, 1, map.entities, nullptr); //rubble + entity->yaw = i * PI / 4; + entity->x = my->x + 4.0 * cos(entity->yaw); + entity->y = my->y + 4.0 * sin(entity->yaw); + entity->z = my->z; + entity->vel_x = (0.1 + (local_rng.rand() % 20) / 50.0) * cos(entity->yaw); + entity->vel_y = (0.1 + (local_rng.rand() % 20) / 50.0) * sin(entity->yaw); + entity->pitch = (local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->behavior = &actParticleShatterEarthRock; + static ConsoleVariable cvar_se2("/se2", 3.0); + entity->vel_z = *cvar_se2; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[NOUPDATE] = true; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); } + my->flags[INVISIBLE] = true; } - hit.entity->colliderOnDestroy(); - if ( parent ) + static ConsoleVariable cvar_se6("/se6", 50); + static ConsoleVariable cvar_se7("/se7", 87); + static ConsoleVariable cvar_se8("/se8", 20); + + if ( noground ) { - if ( parent->behavior == &actPlayer && hit.entity->isDamageableCollider() ) + if ( my->ticks == *cvar_se6 + my->actmagicDelayMove ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(4337), - Language::get(hit.entity->getColliderLangName())); // you destroy the %s! - if ( hit.entity->isColliderWall() ) + list_RemoveNode(my->mynode); + return; + } + } + else if ( !noground ) + { + if ( my->ticks == *cvar_se6 + my->actmagicDelayMove ) + { + my->flags[INVISIBLE] = false; + my->pitch = 0.0; + my->z = 7.0; + my->scalex = 0.05; + my->scaley = 0.05; + my->scalez = 0.05; + my->bNeedsRenderPositionInit = true; + } + else if ( my->ticks >= *cvar_se7 + my->actmagicDelayMove ) + { + if ( my->ticks >= *cvar_se7 + *cvar_se8 + my->actmagicDelayMove ) { - Compendium_t::Events_t::eventUpdateWorld(parent->skill[2], Compendium_t::CPDM_BARRIER_DESTROYED, "breakable barriers", 1); + my->z += 0.25; + my->scalex = std::max(0.0, my->scalex - 0.05); + my->scaley = my->scalex; + my->scalez = my->scalex; + if ( my->scalex <= 0.0 ) + { + list_RemoveNode(my->mynode); + return; + } } + /*else + { + my->scalex = std::min(1.0, my->scalex + 0.1); + my->scaley = my->scalex; + my->scalez = my->scalex; + my->z = std::max(my->z - 0.1, 6.5); + }*/ + } + else if ( my->ticks >= *cvar_se6 + my->actmagicDelayMove ) + { + my->scalex = std::min(0.5, my->scalex + 0.1); + my->scaley = my->scalex; + my->scalez = my->scalex; } } - - // destroy the object - playSoundEntity(hit.entity, 67, 128); - list_RemoveNode(hit.entity->mynode); - return true; } - else if ( hit.entity->behavior == &::actDaedalusShrine ) + else if ( my->sprite == 1868 ) { - createParticleRock(hit.entity); - if ( multiplayer == SERVER ) + static ConsoleVariable cvar_se10("/se10", 42); + int tallCeilingDelay = 0; + if ( tallCeiling ) { - serverSpawnMiscParticles(hit.entity, PARTICLE_EFFECT_ABILITY_ROCK, 78); + static ConsoleVariable cvar_se11("/se11", -5); + tallCeilingDelay = *cvar_se11; + } + if ( my->ticks >= TICKS_PER_SECOND * 2 + my->actmagicDelayMove ) + { + my->scalex = std::max(0.0, my->scalex - 0.1); + //my->scaley = my->scalex; + my->scalez = my->scalex; + if ( my->scalex <= 0.0 ) + { + list_RemoveNode(my->mynode); + return; + } + } + else if ( my->ticks >= *cvar_se10 + tallCeilingDelay + my->actmagicDelayMove ) + { + my->flags[INVISIBLE] = false; + } + else + { + my->scalex = std::min(1.0, my->scalex + 0.05); + my->scaley = my->scalex; + my->scalez = my->scalex; } + } +} - playSoundEntity(hit.entity, 67, 128); - list_RemoveNode(hit.entity->mynode); +void actParticleShatterEarthRock(Entity* my) +{ + my->x += my->vel_x; + my->y += my->vel_y; + my->z += my->vel_z; + my->vel_z += 0.04; + my->vel_x *= 0.95; + my->vel_y *= 0.95; + + bool noground = false; + int x = my->x / 16; + int y = my->y / 16; + if ( x < 0 || x >= map.width || y < 0 || y >= map.height ) + { + noground = true; } - else if ( hit.entity->behavior == &actBoulder ) + else { - int i = numRocks + local_rng.rand() % 4; - - // spawn several rock items //TODO: This should really be its own function. - for ( int c = 0; c < i; c++ ) + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( !map.tiles[mapIndex] || swimmingtiles[map.tiles[mapIndex]] + || lavatiles[map.tiles[mapIndex]] ) { - Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Rock entity. - entity->flags[INVISIBLE] = true; - entity->flags[UPDATENEEDED] = true; - entity->x = hit.entity->x - 4 + local_rng.rand() % 8; - entity->y = hit.entity->y - 4 + local_rng.rand() % 8; - entity->z = -6 + local_rng.rand() % 12; - entity->sizex = 4; - entity->sizey = 4; - entity->yaw = local_rng.rand() % 360 * PI / 180; - entity->vel_x = (local_rng.rand() % 20 - 10) / 10.0; - entity->vel_y = (local_rng.rand() % 20 - 10) / 10.0; - entity->vel_z = -.25 - (local_rng.rand() % 5) / 10.0; - entity->flags[PASSABLE] = true; - entity->behavior = &actItem; - entity->flags[USERFLAG1] = true; // no collision: helps performance - entity->skill[10] = GEM_ROCK; // type - entity->skill[11] = WORN; // status - entity->skill[12] = 0; // beatitude - entity->skill[13] = 1; // count - entity->skill[14] = 0; // appearance - entity->skill[15] = false; // identified + noground = true; } + } - double ox = hit.entity->x; - double oy = hit.entity->y; - - boulderLavaOrArcaneOnDestroy(hit.entity, hit.entity->sprite, nullptr); - - auto& rng = hit.entity->entity_rng ? *hit.entity->entity_rng : local_rng; - Uint32 monsterSpawnSeed = rng.getU32(); - - // destroy the boulder - playSoundEntity(hit.entity, 67, 128); - list_RemoveNode(hit.entity->mynode); - if ( parent ) + if ( noground ) + { + my->roll += 0.04; + my->pitch += 0.04; + if ( my->z >= 7.5 ) { - if ( parent->behavior == &actPlayer ) + ++my->skill[1]; + if ( my->skill[1] >= TICKS_PER_SECOND / 2 ) { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(405)); + list_RemoveNode(my->mynode); + return; } } - - // on sokoban, destroying boulders spawns scorpions - if ( !strcmp(map.name, "Sokoban") ) + } + else + { + if ( my->skill[0] == 0 ) { - Entity* monster = nullptr; - if ( local_rng.rand() % 2 == 0 ) + if ( my->z >= 7.5 ) { - monster = summonMonster(INSECTOID, ox, oy); - } - else - { - monster = summonMonster(SCORPION, ox, oy); + static ConsoleVariable cvar_se3("/se3", -0.5); + my->z = 7.5; + my->vel_z = *cvar_se3; + my->skill[0] = 1; } - if ( monster ) + } + else if ( my->skill[0] == 1 ) + { + my->z = std::min(my->z, 7.5); + if ( my->z >= 7.5 ) { - monster->seedEntityRNG(monsterSpawnSeed); - for ( int c = 0; c < MAXPLAYERS; c++ ) + ++my->skill[1]; + if ( my->skill[1] >= TICKS_PER_SECOND / 2 ) { - Uint32 color = makeColorRGB(255, 128, 0); - messagePlayerColor(c, MESSAGE_HINT, color, Language::get(406)); + list_RemoveNode(my->mynode); + return; } } - boulderSokobanOnDestroy(false); } - return true; + + if ( my->z < 7.45 ) + { + my->roll += 0.04; + my->pitch += 0.04; + } } - return false; } + +void createParticleShatterEarth(Entity* my, Entity* caster, real_t _x, real_t _y, int spellID) +{ + int x = static_cast(_x / 16); + int y = static_cast(_y / 16); + + if ( !(x > 0 && x < map.width - 2 && y > 0 && y < map.height - 2) ) + { + return; + } + + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + bool tallCeiling = false; + if ( !map.tiles[(MAPLAYERS - 1) + mapIndex] ) + { + tallCeiling = true; + } + + { + Entity* entity = newEntity(1869, 1, map.entities, nullptr); //rubble + entity->x = x * 16.0 + 8.0; + entity->y = y * 16.0 + 8.0; + + static ConsoleVariable cvar_se("/se", -10.5); + entity->z = *cvar_se + (tallCeiling ? -16.0 : 0.0); + entity->yaw = 0.0; + entity->pitch = PI; + entity->fskill[0] = entity->x; // start coord + entity->fskill[1] = entity->y; // start coord + entity->fskill[4] = -7.5 + (tallCeiling ? -16.0 : 0.0); + entity->scalex = 0.25; + entity->scaley = 0.25; + entity->scalez = 0.25; + entity->skill[0] = 5 * TICKS_PER_SECOND; + entity->skill[1] = spellID; + entity->behavior = &actParticleShatterEarth; + entity->parent = caster ? caster->getUID() : 0; + entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f); + entity->actmagicDelayMove = TICKS_PER_SECOND; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[NOUPDATE] = true; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } + + { + Entity* entity = newEntity(1868, 1, map.entities, nullptr); //boulder hole + entity->x = x * 16.0 + 8.0; + entity->y = y * 16.0 + 8.0; + + static ConsoleVariable cvar_se1("/se1", -8.0); + entity->z = *cvar_se1 + (tallCeiling ? -16.0 : 0.0); + entity->yaw = 0.0; + entity->scalex = 0.25; + entity->scaley = 0.25; + entity->scalez = 0.25; + entity->skill[0] = 5 * TICKS_PER_SECOND; + entity->flags[INVISIBLE] = true; + entity->behavior = &actParticleShatterEarth; + /*entity->lightBonus = vec4(*cvar_magic_fx_light_bonus, *cvar_magic_fx_light_bonus, + *cvar_magic_fx_light_bonus, 0.f);*/ + entity->actmagicDelayMove = TICKS_PER_SECOND; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + entity->flags[UPDATENEEDED] = false; + entity->flags[NOUPDATE] = true; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + entity->setUID(-3); + } +} \ No newline at end of file diff --git a/src/magic/castSpell.cpp b/src/magic/castSpell.cpp index 51c0cc885..4196322db 100644 --- a/src/magic/castSpell.cpp +++ b/src/magic/castSpell.cpp @@ -25,24 +25,18 @@ #include "magic.hpp" #include "../prng.hpp" #include "../mod_tools.hpp" +#include "../paths.hpp" -bool spellIsNaturallyLearnedByRaceOrClass(Entity& caster, Stat& stat, int spellID); - -void castSpellInit(Uint32 caster_uid, spell_t* spell, bool usingSpellbook) +void castSpellInit(Uint32 caster_uid, spell_t* spell, bool usingSpellbook, bool usingTome) { Entity* caster = uidToEntity(caster_uid); node_t* node = NULL; - if ( !caster || !spell ) + if ( !caster ) { //Need a spell and caster to cast a spell. return; } - if ( !spell->elements.first ) - { - return; - } - int player = -1; for ( int i = 0; i < MAXPLAYERS; ++i ) { @@ -63,6 +57,31 @@ void castSpellInit(Uint32 caster_uid, spell_t* spell, bool usingSpellbook) if ( cast_animation[player].active || cast_animation[player].active_spellbook ) { //Already casting spell. + if ( player >= 0 && players[player]->isLocalPlayer() ) + { + if ( cast_animation[player].spellWaitingAttackInput() ) + { + //if ( multiplayer == SINGLE ) + //{ + // // refund some spent mana + // int refund = (cast_animation[player].mana_cost - cast_animation[player].mana_left) / 2; + // players[player]->entity->modMP(refund); + //} + spellcastingAnimationManager_deactivate(&cast_animation[player]); + messagePlayer(player, MESSAGE_COMBAT, Language::get(6496)); + playSoundEntityLocal(players[player]->entity, 163, 64); + } + } + return; + } + + if ( !spell ) + { + return; + } + + if ( !spell->elements.first ) + { return; } @@ -118,7 +137,7 @@ void castSpellInit(Uint32 caster_uid, spell_t* spell, bool usingSpellbook) return; } if ( spell->ID == SPELL_VAMPIRIC_AURA && player >= 0 && client_classes[player] == CLASS_ACCURSED && - stats[player]->EFFECTS[EFF_VAMPIRICAURA] && players[player]->entity->playerVampireCurse == 1 ) + stats[player]->getEffectActive(EFF_VAMPIRICAURA) && players[player]->entity->playerVampireCurse == 1 ) { if ( multiplayer == CLIENT ) { @@ -160,23 +179,57 @@ void castSpellInit(Uint32 caster_uid, spell_t* spell, bool usingSpellbook) } // Entity cannot cast Spells while Paralyzed or Asleep - if ( stat->EFFECTS[EFF_PARALYZED] || stat->EFFECTS[EFF_ASLEEP] ) + if ( stat->getEffectActive(EFF_PARALYZED) || stat->getEffectActive(EFF_ASLEEP) ) { return; } // Calculate the cost of the Spell for Singleplayer - if ( spell->ID == SPELL_FORCEBOLT && skillCapstoneUnlocked(player, PRO_SPELLCASTING) ) + //if ( spell->ID == SPELL_FORCEBOLT && skillCapstoneUnlocked(player, PRO_LEGACY_SPELLCASTING) ) + //{ + // // Reaching Spellcasting capstone makes forcebolt free + // magiccost = 0; + //} + //else { - // Reaching Spellcasting capstone makes forcebolt free - magiccost = 0; + magiccost = getCostOfSpell(spell, caster); } - else + + if ( player >= 0 ) { - magiccost = getCostOfSpell(spell, caster); + if ( players[player]->isLocalPlayer() ) + { + if ( usingTome && spell->ID != SPELL_OVERCHARGE ) + { + magiccost = std::max(1, magiccost / 2); + } + else if ( cast_animation[player].overcharge_init && !usingSpellbook && spell->ID != SPELL_OVERCHARGE ) + { + magiccost = std::max(1, magiccost / 2); + } + } + + if ( stats[player]->type == SALAMANDER && stats[player]->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + magiccost = 0; + } + + if ( spell->ID == SPELL_MUSHROOM || spell->ID == SPELL_SHRUB ) + { + if ( stats[player]->getEffectActive(EFF_GROWTH) <= (Uint8)1 ) + { + if ( players[player]->isLocalPlayer() ) + { + playSound(563, 64); + } + messagePlayer(player, MESSAGE_HINT, Language::get(6794)); + return; + } + } + } - if ( caster->behavior == &actPlayer && stat->type == VAMPIRE ) + if ( (caster->behavior == &actPlayer && stat->type == VAMPIRE) || (usingTome && spell->ID != SPELL_OVERCHARGE) ) { // allow overexpending. } @@ -208,8 +261,38 @@ void castSpellInit(Uint32 caster_uid, spell_t* spell, bool usingSpellbook) return; } + if ( player >= 0 ) + { + int goldCost = getGoldCostOfSpell(spell, player); + if ( goldCost > 0 ) + { + if ( goldCost > stat->GOLD ) + { + if ( players[player]->isLocalPlayer() ) + { + playSound(563, 64); + } + messagePlayer(player, MESSAGE_COMBAT, Language::get(6622)); + return; + } + else if ( goldCost > 0 ) + { + stat->GOLD -= goldCost; + stat->GOLD = std::max(0, stat->GOLD); + + if ( players[player]->isLocalPlayer() ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_GOLD_CASTED, "gold", goldCost); + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_GOLD_CASTED_RUN, "gold", goldCost); + + steamStatisticUpdate(STEAM_STAT_PAY_TO_WIN, STEAM_STAT_INT, goldCost); + } + } + } + } + //Hand the torch off to the spell animator. And stuff. Stuff. I mean spell animation handler thingymabobber. - fireOffSpellAnimation(&cast_animation[player], caster->getUID(), spell, usingSpellbook); + fireOffSpellAnimation(&cast_animation[player], caster->getUID(), spell, usingSpellbook, usingTome); //castSpell(caster, spell); //For now, do this while the spell animations are worked on. } @@ -221,7 +304,7 @@ int getSpellcastingAbilityFromUsingSpellbook(spell_t* spell, Entity* caster, Sta return 0; } - int spellcastingAbility = std::min(std::max(0, casterStats->getModifiedProficiency(PRO_SPELLCASTING) + statGetINT(casterStats, caster)), 100); + int spellcastingAbility = std::min(std::max(0, casterStats->getModifiedProficiency(spell->skillID) + statGetINT(casterStats, caster)), 100); // penalty for not knowing spellbook. e.g 40 spellcasting, 80 difficulty = 40% more chance to fumble/use mana. @@ -248,7 +331,7 @@ int getSpellcastingAbilityFromUsingSpellbook(spell_t* spell, Entity* caster, Sta } -bool isSpellcasterBeginner(int player, Entity* caster) +bool isSpellcasterBeginner(int player, Entity* caster, int skillID) { if ( !caster && player < 0 ) { @@ -272,7 +355,7 @@ bool isSpellcasterBeginner(int player, Entity* caster) { return false; } - else if ( std::min(std::max(0, myStats->getModifiedProficiency(PRO_SPELLCASTING) + statGetINT(myStats, caster)), 100) < SPELLCASTING_BEGINNER ) + else if ( std::min(std::max(0, myStats->getModifiedProficiency(skillID) + statGetINT(myStats, caster)), 100) < SPELLCASTING_BEGINNER ) { return true; //The caster has lower spellcasting skill. Cue happy fun times. } @@ -286,7 +369,7 @@ bool isSpellcasterBeginnerFromSpellbook(int player, Entity* caster, Stat* stat, return false; } - int spellcastingLvl = std::min(std::max(0, stat->getModifiedProficiency(PRO_SPELLCASTING) + statGetINT(stat, caster)), 100); + int spellcastingLvl = std::min(std::max(0, stat->getModifiedProficiency(spell->skillID) + statGetINT(stat, caster)), 100); bool newbie = false; if ( spellcastingLvl >= spell->difficulty || playerLearnedSpellbook(player, spellbookItem) ) @@ -307,20 +390,150 @@ bool isSpellcasterBeginnerFromSpellbook(int player, Entity* caster, Stat* stat, int getSpellbookBonusPercent(Entity* caster, Stat* stat, Item* spellbookItem) { - if ( !spellbookItem || itemCategory(spellbookItem) != SPELLBOOK ) + if ( !spellbookItem || !(itemCategory(spellbookItem) == SPELLBOOK || itemTypeIsFoci(spellbookItem->type)) ) + { + return 0; + } + + int spellBookBonusPercent = 0; + int spellID = SPELL_NONE; + if ( itemCategory(spellbookItem) == SPELLBOOK ) + { + spellID = getSpellIDFromSpellbook(spellbookItem->type); + } + else if ( itemTypeIsFoci(spellbookItem->type) ) + { + spellID = getSpellIDFromFoci(spellbookItem->type); + } + if ( spellID == SPELL_NONE ) { return 0; } - int spellBookBonusPercent = (statGetINT(stat, caster) * 0.5); + if ( auto spell = getSpellFromID(spellID) ) + { + if ( itemCategory(spellbookItem) == SPELLBOOK ) + { + auto find = ItemTooltips.spellItems.find(spell->ID); + if ( find != ItemTooltips.spellItems.end() + && find->second.spellTags.find(ItemTooltips_t::SPELL_TAG_SPELLBOOK_SCALING) == find->second.spellTags.end() ) + { + return 0; + } + spellBookBonusPercent = getSpellbookBaseINTBonus(caster, stat, spell->skillID); + } + } + if ( spellbookItem->beatitude > 0 || (shouldInvertEquipmentBeatitude(stat) && spellbookItem->beatitude < 0) ) { - spellBookBonusPercent += abs(spellbookItem->beatitude) * 25; + if ( itemTypeIsFoci(spellbookItem->type) ) + { + spellBookBonusPercent += abs(spellbookItem->beatitude) * 5; + } + else + { + spellBookBonusPercent += abs(spellbookItem->beatitude) * 10; + } } return spellBookBonusPercent; } -Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool trap, bool usingSpellbook) +enum SpellTarget_t +{ + TARGET_NEUTRAL = 1, + TARGET_ENEMY = 2, + TARGET_FRIEND = 4 +}; +Entity* getSpellTarget(node_t* node, int radius, Entity* caster, bool targetCaster, SpellTarget_t target) +{ + if ( !node ) + { + return nullptr; + } + Entity* entity = (Entity*)(node->element); + if ( !entity ) + { + return nullptr; + } + if ( entity == caster && !targetCaster ) + { + return nullptr; + } + if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) + { + return nullptr; + } + + if ( !entity->monsterIsTargetable() ) + { + return nullptr; + } + + if ( entityDist(entity, caster) <= radius + && ((target & TARGET_ENEMY && entity->checkEnemy(caster) + || (target & TARGET_NEUTRAL && !entity->checkEnemy(caster)) + || (target & TARGET_FRIEND && entity->checkFriend(caster)))) ) + { + return entity->getStats() ? entity : nullptr; + } + return nullptr; +} + +bool CastSpellProps_t::setToMonsterCast(Entity* monster, int spellID) +{ + if ( !monster ) { return false; } + if ( monster->behavior != &actMonster ) { return false; } + caster_x = monster->x; + caster_y = monster->y; + + real_t spellDist = 64.0; + spell_t* spell = getSpellFromID(spellID); + if ( spell ) + { + spellDist = std::max(spellDist, spell->distance); + } + spellDist += 16.0; + + if ( Entity* target = uidToEntity(monster->monsterTarget) ) + { + Entity* ohit = hit.entity; + real_t tangent = atan2(target->y - monster->y, target->x - monster->x); + real_t dist = lineTraceTarget(monster, monster->x, monster->y, tangent, spellDist, 0, false, target); + if ( hit.entity == target ) + { + target_x = target->x; + target_y = target->y; + if ( spell && (spell->rangefinder == RANGEFINDER_TOUCH || spell->rangefinder == RANGEFINDER_TOUCH_INTERACT) ) + { + targetUID = target->getUID(); + } + } + else + { + if ( spell && (spell->rangefinder == RANGEFINDER_TOUCH || spell->rangefinder == RANGEFINDER_TOUCH_INTERACT) ) + { + hit.entity = ohit; + return false; + } + target_x = caster_x + dist * cos(tangent); + target_y = caster_y + dist * sin(tangent); + } + hit.entity = ohit; + return true; + } + return false; +} + +int getEffectiveSpellcastingAbility(Entity* caster, Stat* stat, spell_t* spell) // to check for fumbling +{ + if ( !caster || !stat || !spell ) + { + return 0; + } + return std::min(std::max(0, stat->getModifiedProficiency(spell->skillID) + statGetINT(stat, caster)), 100); +} + +Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool trap, bool usingSpellbook, CastSpellProps_t* castSpellProps, bool usingFoci) { Entity* caster = uidToEntity(caster_uid); @@ -331,7 +544,6 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } Entity* result = NULL; //If the spell spawns an entity (like a magic light ball or a magic missile), it gets stored here and returned. -#define spellcasting std::min(std::max(0,stat->getModifiedProficiency(PRO_SPELLCASTING)+statGetINT(stat, caster)),100) //Shortcut! if (clientnum != 0 && multiplayer == CLIENT) { @@ -355,7 +567,22 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool { net_packet->data[9] = 0; } - net_packet->len = 10; + if ( castSpellProps ) + { + SDLNet_Write32(static_cast(castSpellProps->caster_x * 256.0), &net_packet->data[10]); + SDLNet_Write32(static_cast(castSpellProps->caster_y * 256.0), &net_packet->data[14]); + SDLNet_Write32(static_cast(castSpellProps->target_x * 256.0), &net_packet->data[18]); + SDLNet_Write32(static_cast(castSpellProps->target_y * 256.0), &net_packet->data[22]); + SDLNet_Write32(castSpellProps->targetUID, &net_packet->data[26]); + net_packet->data[30] = castSpellProps->wallDir; + net_packet->data[31] = castSpellProps->optionalData; + net_packet->data[32] = castSpellProps->overcharge; + net_packet->len = 33; + } + else + { + net_packet->len = 10; + } } net_packet->address.host = net_server.host; net_packet->address.port = net_server.port; @@ -376,8 +603,6 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool int propulsion = 0; int traveltime = 0; int magiccost = 0; - int extramagic = 0; //Extra magic drawn in from the caster being a newbie. - int extramagic_to_use = 0; //Instead of doing element->mana (which causes bugs), this is an extra factor in the mana equations. Pumps extra mana into elements from extramagic. spell_t* channeled_spell = NULL; //Pointer to the spell if it's a channeled spell. For the purpose of giving it its node in the channeled spell list. node_t* node = spell->elements.first; @@ -404,17 +629,45 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool { sustainedSpell = (findSpellDef->second.spellType == ItemTooltips_t::SpellItemTypes::SPELL_TYPE_SELF_SUSTAIN); } + bool allowedSkillup = false; + if ( player >= 0 ) + { + if ( !trap ) + { + if ( using_magicstaff ) + { + allowedSkillup = true; + } + else if ( (!using_magicstaff && !usingFoci) ) + { + allowedSkillup = true; + } + } + } + Uint32 spellEventFlags = 0; + if ( using_magicstaff && !trap && !usingFoci ) + { + spellEventFlags |= spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF; + } int oldMP = caster->getMP(); - if ( !using_magicstaff && !trap && stat ) + Sint32 prevMP = 0; + if ( stat ) { - newbie = isSpellcasterBeginner(player, caster); + prevMP = stat->MP; + } + + if ( !using_magicstaff && !trap && !usingFoci && stat && player >= 0 ) + { + newbie = isSpellcasterBeginner(player, caster, spell->skillID); if ( usingSpellbook && stat->shield && itemCategory(stat->shield) == SPELLBOOK ) { + spellEventFlags |= spell_t::SPELL_LEVEL_EVENT_SPELLBOOK; + spellbookType = stat->shield->type; spellBookBeatitude = stat->shield->beatitude; spellBookBonusPercent += getSpellbookBonusPercent(caster, stat, stat->shield); - if ( spellcasting >= spell->difficulty || playerLearnedSpellbook(player, stat->shield) ) + if ( getEffectiveSpellcastingAbility(caster, stat, spell) >= spell->difficulty || playerLearnedSpellbook(player, stat->shield) ) { // bypass newbie penalty since we're good enough to cast the spell. playerCastingFromKnownSpellbook = true; @@ -437,27 +690,65 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool int prevHP = caster->getHP(); if ( multiplayer == SINGLE ) { - if ( spell->ID == SPELL_FORCEBOLT && skillCapstoneUnlocked(player, PRO_SPELLCASTING) ) + /*if ( spell->ID == SPELL_FORCEBOLT && skillCapstoneUnlocked(player, PRO_LEGACY_SPELLCASTING) ) { magiccost = 0; } else { - magiccost = cast_animation[player].mana_left; - } + }*/ + magiccost = cast_animation[player].mana_left; caster->drainMP(magiccost, false); } else // Calculate the cost of the Spell for Multiplayer { - if ( spell->ID == SPELL_FORCEBOLT && skillCapstoneUnlocked(player, PRO_SPELLCASTING) ) - { - // Reaching Spellcasting capstone makes Forcebolt free - magiccost = 0; - } - else + //if ( spell->ID == SPELL_FORCEBOLT && skillCapstoneUnlocked(player, PRO_LEGACY_SPELLCASTING) ) + //{ + // // Reaching Spellcasting capstone makes Forcebolt free + // magiccost = 0; + //} + //else { + if ( !players[player]->isLocalPlayer() ) + { + int goldCost = getGoldCostOfSpell(spell, player); + if ( goldCost > 0 && player >= 0 ) + { + if ( goldCost > stat->GOLD ) + { + playSoundEntity(caster, 163, 128); + messagePlayer(player, MESSAGE_COMBAT, Language::get(6622)); + return nullptr; + } + else if ( goldCost > 0 ) + { + stat->GOLD -= goldCost; + stat->GOLD = std::max(0, stat->GOLD); + + if ( player >= 1 ) + { + // send the client info on the gold it picked up + strcpy((char*)net_packet->data, "GOLD"); + SDLNet_Write32(stats[player]->GOLD, &net_packet->data[4]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + } + } + } + magiccost = getCostOfSpell(spell, caster); + if ( castSpellProps && castSpellProps->overcharge > 0 && !usingSpellbook ) + { + magiccost = std::max(1, magiccost / 2); + } + if ( stat->type == SALAMANDER && stat->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + magiccost = 0; + } if ( magiccost > stat->MP ) { // damage sound/effect due to overdraw. @@ -505,40 +796,75 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } } + + if ( caster->behavior == &actPlayer ) + { + if ( sustainedSpell ) + { + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - stat->MP, spell->skillID); + } + else + { + players[caster->skill[2]]->mechanics.baseSpellIncrementMP(oldMP - stat->MP, spell->skillID); + } + } + if ( newbie && stat ) { //So This wizard is a newbie. //First, drain some extra mana maybe. - int chance = local_rng.rand() % 10; - int spellcastingAbility = spellcasting; + int chance = local_rng.rand() % 100; + int spellcastingAbility = getEffectiveSpellcastingAbility(caster, stat, spell); if ( usingSpellbook ) { spellcastingAbility = getSpellcastingAbilityFromUsingSpellbook(spell, caster, stat); } - if (chance >= spellcastingAbility / 10) //At skill 20, there's an 80% chance you'll use extra mana. At 70, there's a 30% chance. + if (chance >= spellcastingAbility) //At skill 20, there's an 80% chance you'll use extra mana. At 70, there's a 30% chance. { - extramagic = local_rng.rand() % (300 / (spellcastingAbility + 1)); //Use up extra mana. More mana used the lower your spellcasting skill. + int extramagic = local_rng.rand() % (300 / (spellcastingAbility + 1)); //Use up extra mana. More mana used the lower your spellcasting skill. extramagic = std::min(extramagic, stat->MP / 10); //To make sure it doesn't draw, say, 5000 mana. Cause dammit, if you roll a 1 here...you're doomed. + + if ( stat->type == SALAMANDER && stat->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + extramagic = 0; + } + + Sint32 oldMP = stat->MP; caster->drainMP(extramagic); - } - if ( caster->behavior == &actPlayer && sustainedSpell ) - { - players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - stat->MP); + if ( caster->behavior == &actPlayer ) + { + if ( sustainedSpell ) + { + players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - stat->MP, spell->skillID); + } + else + { + players[caster->skill[2]]->mechanics.baseSpellIncrementMP(oldMP - stat->MP, spell->skillID); + } + } } + bool fizzleSpell = false; - chance = local_rng.rand() % 10; - if ( chance >= spellcastingAbility / 10 ) + chance = local_rng.rand() % 100; + if ( chance >= spellcastingAbility ) { fizzleSpell = true; + if ( !usingSpellbook ) + { + if ( spellcastingAbility >= SKILL_LEVEL_BASIC ) + { + fizzleSpell = false; + } + } } // Check for natural monster spells - we won't fizzle those. if ( caster->behavior == &actPlayer ) { - if ( spellIsNaturallyLearnedByRaceOrClass(*caster, *stat, spell->ID) ) + if ( spellIsNaturallyLearnedByRaceOrClass(caster, *stat, spell->ID) ) { fizzleSpell = false; } @@ -547,7 +873,13 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool //Now, there's a chance they'll fumble the spell. if ( fizzleSpell ) { - if ( local_rng.rand() % 3 == 1 ) + int fizzleChance = 35; + if ( usingSpellbook ) + { + fizzleChance += std::max(spell->difficulty - getEffectiveSpellcastingAbility(caster, stat, spell), 0) / 2; + } + int chance = local_rng.rand() % 100; + if ( chance <= fizzleChance ) { //Fizzle the spell. //TODO: Cool effects. @@ -598,11 +930,6 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } } - if ( caster->behavior == &actPlayer && sustainedSpell ) - { - players[caster->skill[2]]->mechanics.sustainedSpellIncrementMP(oldMP - stat->MP); - } - //Check if the bugger is levitating. bool levitating = false; if (!trap) @@ -614,10 +941,10 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool bool waterwalkingboots = false; if (!trap) { - if ( player >= 0 && skillCapstoneUnlocked(player, PRO_SWIMMING) ) + /*if ( player >= 0 && skillCapstoneUnlocked(player, PRO_LEGACY_SWIMMING) ) { waterwalkingboots = true; - } + }*/ if ( stat && stat->shoes != NULL ) { if (stat->shoes->type == IRON_BOOTS_WATERWALKING ) @@ -627,7 +954,6 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } } - node_t* node2; //For traversing the map looking for...liquids? //Check if swimming. if (!waterwalkingboots && !levitating && !trap && player >= 0) { @@ -654,29 +980,10 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool //Right. First, grab the root element, which is what determines the delivery system. //spellElement_t *element = (spellElement_t *)spell->elements->first->element; - spellElement_t* element = (spellElement_t*)node->element; + spellElement_t* const element = (spellElement_t*)node->element; + spellElement_t* const innerElement = element->elements.first ? (spellElement_t*)(element->elements.first->element) : nullptr; if (element) { - extramagic_to_use = 0; - /*if (magiccost > stat->MP) { - if (player >= 0) - messagePlayer(player, "Insufficient mana!"); //TODO: Allow overexpending at the cost of extreme danger? (maybe an immensely powerful tree of magic actually likes this -- using your life-force to power spells instead of mana) - return NULL; - }*/ - - if (extramagic > 0) - { - //Extra magic. Pump it in here? - chance = local_rng.rand() % 5; - if (chance == 1) - { - //Use some of that extra magic in this element. - int amount = local_rng.rand() % extramagic; - extramagic -= amount; - extramagic_to_use += amount; - } - } - if (!strcmp(element->element_internal_name, spellElement_missile.element_internal_name)) { //Set the propulsion to missile. @@ -686,16 +993,15 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool { //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. chance = local_rng.rand() % 10; - if (chance >= spellcasting / 10) + if (chance >= getEffectiveSpellcastingAbility(caster, stat, spell) / 10) { - traveltime -= local_rng.rand() % (1000 / (spellcasting + 1)); + traveltime -= local_rng.rand() % (1000 / (getEffectiveSpellcastingAbility(caster, stat, spell) + 1)); } if (traveltime < 30) { traveltime = 30; //Range checking. } } - traveltime += (((element->mana + extramagic_to_use) - element->base_mana) / static_cast(element->overload_multiplier)) * element->duration; if ( caster->behavior == &actBoulder ) { traveltime /= 4; // lava boulder casting. @@ -710,32 +1016,74 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool { //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. chance = local_rng.rand() % 10; - if ( chance >= spellcasting / 10 ) + if ( chance >= getEffectiveSpellcastingAbility(caster, stat, spell) / 10 ) { - traveltime -= local_rng.rand() % (1000 / (spellcasting + 1)); + traveltime -= local_rng.rand() % (1000 / (getEffectiveSpellcastingAbility(caster, stat, spell) + 1)); } if ( traveltime < 30 ) { traveltime = 30; //Range checking. } } - traveltime += (((element->mana + extramagic_to_use) - element->base_mana) / static_cast(element->overload_multiplier)) * element->duration; } + /*else if ( !strcmp(element->element_internal_name, "spell_element_propulsion_floor_tile") ) + { + if ( castSpellProps ) + { + int sprite = -1; + if ( !strcmp(innerElement->element_internal_name, "spell_ice_wave") ) + { + sprite = 1720; + } + if ( sprite >= 0 ) + { + real_t tangent = atan2(castSpellProps->target_y - castSpellProps->caster_y, castSpellProps->target_x - castSpellProps->caster_x); + real_t tx = castSpellProps->target_x + castSpellProps->distanceOffset * cos(tangent); + real_t ty = castSpellProps->target_y + castSpellProps->distanceOffset * sin(tangent); + int duration = element->duration + innerElement->duration; + Entity* floorMagic = createFloorMagic(ParticleTimerEffect_t::EffectType::EFFECT_ICE_WAVE, sprite, tx, ty, 7.5, tangent, element->duration + innerElement->duration); + floorMagic->actmagicDelayMove = castSpellProps->elementIndex * 5; + if ( floorMagic->actmagicDelayMove > 0 ) + { + floorMagic->flags[INVISIBLE] = true; + floorMagic->flags[UPDATENEEDED] = false; + } + floorMagic->yaw = ((local_rng.rand() % 32) / 32.0) * 2 * PI; + node_t* node = list_AddNodeFirst(&floorMagic->children); + node->element = copySpell(spell, castSpellProps->elementIndex); + ((spell_t*)node->element)->caster = caster->getUID(); + node->deconstructor = &spellDeconstructor; + node->size = sizeof(spell_t); + castSpellProps->distanceOffset += 4.0; + } + } + }*/ else if (!strcmp(element->element_internal_name, spellElement_light.element_internal_name)) { if (using_magicstaff) { + bool removed = false; for (auto node = map.entities->first; node != nullptr; node = node->next) { auto entity = (Entity*)node->element; - if (entity->behavior == &actMagiclightBall) { + if (entity->behavior == &actMagiclightBall && entity->sprite == 174) { if (entity->parent == caster->getUID()) { auto spell = (spell_t*)entity->children.first->element; if ( spell && spell->magicstaff ) { spell->sustain = false; // remove other lightballs to prevent lightball insanity + removed = true; } } } } + + if ( removed ) + { + if ( player >= 0 ) + { + messagePlayer(player, MESSAGE_HINT, Language::get(6845)); + } + return nullptr; + } } Entity* entity = newEntity(174, 1, map.entities, nullptr); // black magic ball entity->parent = caster->getUID(); @@ -751,7 +1099,7 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool entity->behavior = &actMagiclightBall; entity->skill[4] = entity->x; //Store what x it started shooting out from the player at. entity->skill[5] = entity->y; //Store what y it started shooting out from the player at. - entity->skill[12] = (element->duration);// *(((element->mana + extramagic_to_use) / static_cast(element->base_mana)) * element->overload_multiplier)); //How long this thing lives. + entity->skill[12] = (element->duration); //How long this thing lives. node_t* spellnode = list_AddNodeLast(&entity->children); spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. channeled_spell = (spell_t*)(spellnode->element); @@ -762,28 +1110,10 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool ((spell_t*)spellnode->element)->magicstaff = true; } spellnode->deconstructor = &spellDeconstructor; - //if (newbie) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if (chance >= spellcasting / 10) - // { - // // lifespan of the lightball - // entity->skill[12] -= local_rng.rand() % (2000 / (spellcasting + 1)); - // if (entity->skill[12] < 180) - // { - // entity->skill[12] = 180; //Range checking. - // } - // } - //} if (using_magicstaff || trap) { entity->skill[12] = MAGICSTAFF_LIGHT_DURATION; //TODO: Grab the duration from the magicstaff or trap? } - else - { - entity->skill[12] /= getCostOfSpell((spell_t*)spellnode->element); - } ((spell_t*)spellnode->element)->channel_duration = entity->skill[12]; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. result = entity; @@ -809,43 +1139,81 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } } } - else if (!strcmp(element->element_internal_name, spellElement_invisible.element_internal_name)) + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_DEEP_SHADE].element_internal_name) ) { - int duration = element->duration; - //duration += (((element->mana + extramagic_to_use) - element->base_mana) / static_cast(element->overload_multiplier)) * element->duration; - node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); - spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. - channeled_spell = (spell_t*)(spellnode->element); - channeled_spell->magic_effects_node = spellnode; - spellnode->size = sizeof(spell_t); - ((spell_t*)spellnode->element)->caster = caster->getUID(); - spellnode->deconstructor = &spellDeconstructor; - //if (newbie) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if (chance >= spellcasting / 10) - // { - // duration -= local_rng.rand() % (1000 / (spellcasting + 1)); - // } - // if (duration < 180) - // { - // duration = 180; //Range checking. - // } - //} - duration /= getCostOfSpell((spell_t*)spellnode->element); - channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. - caster->setEffect(EFF_INVISIBLE, true, duration, false); - bool isPlayer = false; - for ( int i = 0; i < MAXPLAYERS; ++i ) - { - if ( players[i] && caster && (caster == players[i]->entity) ) - { - serverUpdateEffects(i); - isPlayer = true; - } - } - + if ( using_magicstaff ) { + for ( auto node = map.entities->first; node != nullptr; node = node->next ) { + auto entity = (Entity*)node->element; + if ( entity->behavior == &actMagiclightBall && entity->sprite == 1800 ) { + if ( entity->parent == caster->getUID() ) { + auto spell = (spell_t*)entity->children.first->element; + if ( spell && spell->magicstaff ) + { + spell->sustain = false; // remove other lightballs to prevent lightball insanity + } + } + } + } + } + Entity* entity = newEntity(1800, 1, map.entities, nullptr); // black magic ball + entity->parent = caster->getUID(); + entity->x = caster->x; + entity->y = caster->y; + entity->z = -5.5 + ((-6.5f + -4.5f) / 2) * sin(0); + entity->skill[7] = -5; //Base z. + entity->fskill[1] = entity->skill[7] - entity->z; + entity->fskill[2] = 0.0; + entity->sizex = 1; + entity->sizey = 1; + entity->yaw = caster->yaw; + entity->flags[UPDATENEEDED] = true; + entity->flags[PASSABLE] = true; + entity->behavior = &actMagiclightBall; + entity->skill[4] = entity->x; //Store what x it started shooting out from the player at. + entity->skill[5] = entity->y; //Store what y it started shooting out from the player at. + entity->skill[12] = (element->duration); //How long this thing lives. + node_t* spellnode = list_AddNodeLast(&entity->children); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + if ( using_magicstaff ) + { + ((spell_t*)spellnode->element)->magicstaff = true; + } + spellnode->deconstructor = &spellDeconstructor; + if ( using_magicstaff || trap ) + { + entity->skill[12] = MAGICSTAFF_LIGHT_DURATION; //TODO: Grab the duration from the magicstaff or trap? + } + ((spell_t*)spellnode->element)->channel_duration = entity->skill[12]; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + result = entity; + + playSoundEntity(entity, 165, 128); + } + else if (!strcmp(element->element_internal_name, spellElement_invisible.element_internal_name)) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + + int duration = element->duration; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + caster->setEffect(EFF_INVISIBLE, true, duration, false); + bool isPlayer = false; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + serverUpdateEffects(i); + isPlayer = true; + } + } + if ( isPlayer ) { for ( node_t* node = map.creatures->first; node != nullptr; node = node->next ) @@ -872,8 +1240,6 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } else if (!strcmp(element->element_internal_name, spellElement_levitation.element_internal_name)) { - int duration = element->duration; - //duration += (((element->mana + extramagic_to_use) - element->base_mana) / static_cast(element->overload_multiplier)) * element->duration; node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. channeled_spell = (spell_t*)(spellnode->element); @@ -881,20 +1247,8 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool spellnode->size = sizeof(spell_t); ((spell_t*)spellnode->element)->caster = caster->getUID(); spellnode->deconstructor = &spellDeconstructor; - //if (newbie) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if (chance >= spellcasting / 10) - // { - // duration -= local_rng.rand() % (1000 / (spellcasting + 1)); - // } - // if (duration < 180) - // { - // duration = 180; //Range checking. - // } - //} - duration /= getCostOfSpell((spell_t*)spellnode->element); + + int duration = element->duration; channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. caster->setEffect(EFF_LEVITATING, true, duration, false); for ( int i = 0; i < MAXPLAYERS; ++i ) @@ -917,7 +1271,13 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool int tx = ghost.spawnX; int ty = ghost.spawnY; Entity* target = nullptr; - if ( ghost.teleportToPlayer >= MAXPLAYERS ) + if ( players[caster->skill[2]]->entity ) + { + target = players[caster->skill[2]]->entity; + tx = static_cast(target->x) / 16; + ty = static_cast(target->y) / 16; + } + else if ( ghost.teleportToPlayer >= MAXPLAYERS ) { ghost.teleportToPlayer = -1; tx = ghost.startRoomX; @@ -970,7 +1330,12 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool Entity* entityToTeleport = uidToEntity(caster->creatureShadowTaggedThisUid); if ( entityToTeleport ) { - caster->teleportAroundEntity(entityToTeleport, 3, 0); + if ( caster->teleportAroundEntity(entityToTeleport, 3, 0) ) + { + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, 1, allowedSkillup); + magicOnSpellCastEvent(caster, caster, nullptr, + SPELL_SHADOW_TAG, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } if ( caster->behavior == &actPlayer ) { achievementObserver.addEntityAchievementTimer(caster, AchievementObserver::BARONY_ACH_OHAI_MARK, 100, true, 0); @@ -978,12 +1343,47 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } else { - caster->teleportRandom(); + if ( caster->teleportRandom() ) + { + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, 1, allowedSkillup); + } } } else { - caster->teleportRandom(); + if ( caster->teleportRandom() ) + { + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, 1, allowedSkillup); + } + } + } + else if ( spell->ID == SPELL_JUMP ) + { + if ( caster && castSpellProps ) + { + real_t oldx = caster->x; + real_t oldy = caster->y; + createParticleErupt(caster, 625); + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_ERUPT, 625); + caster->x = castSpellProps->target_x; + caster->y = castSpellProps->target_y; + + if ( caster->teleportAroundEntity(caster, 1, SPELL_JUMP) ) + { + createParticleErupt(caster, 625); + // teleport success. + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_ERUPT, 625); + } + + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, 1, allowedSkillup); + } + else + { + caster->x = oldx; + caster->y = oldy; + } } } else if ( !strcmp(element->element_internal_name, spellElement_selfPolymorph.element_internal_name) ) @@ -991,6 +1391,13 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool if ( caster->behavior == &actPlayer ) { spellEffectPolymorph(caster, caster, true, TICKS_PER_SECOND * 60 * 2); // 2 minutes. + if ( caster->getStats() ) + { + if ( caster->getStats()->getEffectActive(EFF_POLYMORPH) ) + { + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_EFFECT | spellEventFlags, 1, allowedSkillup); + } + } } else if ( caster->behavior == &actMonster ) { @@ -1187,906 +1594,6490 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool playSoundEntity(caster, 167, 128 ); } + else if ( spell->ID == SPELL_ALTER_INSTRUMENT + || spell->ID == SPELL_METALLURGY + || spell->ID == SPELL_GEOMANCY + || spell->ID == SPELL_FORGE_KEY + || spell->ID == SPELL_FORGE_JEWEL + || spell->ID == SPELL_ENHANCE_WEAPON + || spell->ID == SPELL_RESHAPE_WEAPON + || spell->ID == SPELL_ALTER_ARROW + || spell->ID == SPELL_PUNCTURE_VOID + || spell->ID == SPELL_ADORCISM + || spell->ID == SPELL_RESTORE + || spell->ID == SPELL_VANDALISE + || spell->ID == SPELL_DESECRATE + || spell->ID == SPELL_SANCTIFY + || spell->ID == SPELL_SANCTIFY_WATER + || spell->ID == SPELL_CLEANSE_FOOD + || spell->ID == SPELL_ADORCISE_INSTRUMENT ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && (caster == players[i]->entity) ) + { + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + if ( i != 0 && !players[i]->isLocalPlayer() ) + { + //Tell the client to identify an item. + strcpy((char*)net_packet->data, "FXSP"); + if ( usingSpellbook ) + { + net_packet->data[4] = 1; + net_packet->data[5] = static_cast(spellBookBeatitude); + } + else + { + net_packet->data[4] = 0; + net_packet->data[5] = 0; + } + SDLNet_Write32(spell->ID, &net_packet->data[6]); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 10; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + else + { + //Identify an item. + if ( usingSpellbook ) + { + GenericGUI[i].openGUI(GUI_TYPE_ITEMFX, nullptr, spellBookBeatitude, getSpellbookFromSpellID(spell->ID), spell->ID); + } + else + { + GenericGUI[i].openGUI(GUI_TYPE_ITEMFX, nullptr, 0, SPELL_ITEM, spell->ID); + } + } + } + } + + playSoundEntity(caster, 167, 128); + } else if (!strcmp(element->element_internal_name, spellElement_magicmapping.element_internal_name)) { for ( int i = 0; i < MAXPLAYERS; ++i ) { if ( players[i] && caster && (caster == players[i]->entity) ) { - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); - spell_magicMap(i); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + int radius = getSpellDamageFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + radius = std::max(4, radius); + spell_magicMap(i, radius, caster->x / 16, caster->y / 16); + + if ( caster->isEntityPlayer() >= 0 ) + { + int pingx = caster->x / 16; + int pingy = caster->y / 16; + sendMinimapPing(caster->isEntityPlayer(), pingx, pingy, 0, true); + } + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + + } + //magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, 1, allowedSkillup); } } playSoundEntity(caster, 167, 128 ); } - else if ( !strcmp(element->element_internal_name, spellElement_detectFood.element_internal_name) ) + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_CONJURE_FOOD].element_internal_name) ) { for ( int i = 0; i < MAXPLAYERS; ++i ) { if ( players[i] && caster && (caster == players[i]->entity) ) { spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); - spell_detectFoodEffectOnMap(i); + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, 1, allowedSkillup); + //spell_magicMap(i); } } playSoundEntity(caster, 167, 128); } - else if ( !strcmp(element->element_internal_name, spellElement_salvageItem.element_internal_name) ) + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_MAGICIANS_ARMOR].element_internal_name) ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( caster ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + if ( Stat* casterStats = caster->getStats() ) { - int searchx = static_cast(caster->x + 32 * cos(caster->yaw)) >> 4; - int searchy = static_cast(caster->y + 32 * sin(caster->yaw)) >> 4; - std::vector itemsOnGround = TileEntityList.getEntitiesWithinRadius(searchx, searchy, 2); - int totalMetal = 0; - int totalMagic = 0; - int numItems = 0; - std::set> effectCoordinates; - for ( auto it = itemsOnGround.begin(); it != itemsOnGround.end(); ++it ) + Uint8 effectStrength = casterStats->getEffectActive(EFF_MAGICIANS_ARMOR); + if ( true /*effectStrength == 0*/ ) { - list_t* currentList = *it; - node_t* itemNode; - node_t* nextItemNode = nullptr; - for ( itemNode = currentList->first; itemNode != nullptr; itemNode = nextItemNode ) + int instances = getSpellDamageFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + instances *= (casterStats->getModifiedProficiency(spell->skillID) + statGetINT(casterStats, caster)) / std::max(1, element->getDurationSecondary()); + int maxInstances = getSpellDamageSecondaryFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + instances = std::min(std::max(1, instances), maxInstances); + + Uint8 applyStrength = std::min(effectStrength + 1, instances); + if ( caster->setEffect(EFF_MAGICIANS_ARMOR, (Uint8)applyStrength, element->duration, true, true, true) ) { - nextItemNode = itemNode->next; - Entity* itemEntity = (Entity*)itemNode->element; - if ( itemEntity && !itemEntity->flags[INVISIBLE] && itemEntity->behavior == &actItem && entityDist(itemEntity, caster) < TOUCHRANGE ) + if ( effectStrength == 0 ) { - Item* toSalvage = newItemFromEntity(itemEntity); - if ( toSalvage && GenericGUI[i].isItemSalvageable(toSalvage, i) ) - { - int metal = 0; - int magic = 0; - GenericGUIMenu::tinkeringGetItemValue(toSalvage, &metal, &magic); - totalMetal += metal; - totalMagic += magic; - ++numItems; - effectCoordinates.insert(std::make_pair(static_cast(itemEntity->x) >> 4, static_cast(itemEntity->y) >> 4)); - - // delete item on ground. - itemEntity->removeLightField(); - list_RemoveNode(itemEntity->mynode); - } - free(toSalvage); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6862)); + } + else if ( applyStrength >= instances && applyStrength == effectStrength ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(255, 255, 255), Language::get(6972)); + } + else + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6971)); } } + playSoundEntity(caster, 166, 128); } - if ( totalMetal == 0 && totalMagic == 0 ) + else { - messagePlayer(i, MESSAGE_COMBAT, Language::get(3713)); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(255, 255, 255), Language::get(6727), spell->getSpellName()); playSoundEntity(caster, 163, 128); } - else + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2212); + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_BLESS_FOOD].element_internal_name) ) + { + if ( caster ) + { + Uint8 effectStrength = caster->isEntityPlayer() >= 0 ? caster->skill[2] + 1 : MAXPLAYERS + 1; + if ( Stat* casterStats = caster->getStats() ) + { + //Uint8 effectStrength = casterStats->getEffectActive(EFF_BLESS_FOOD); + //if ( effectStrength == 0 ) { - messagePlayerColor(i, MESSAGE_INVENTORY, makeColorRGB(0, 255, 0), Language::get(3712), numItems); - playSoundEntity(caster, 167, 128); + if ( caster->setEffect(EFF_BLESS_FOOD, effectStrength, element->duration, false, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6486)); + } } + } - int pickedUpMetal = 0; - while ( totalMetal > 0 ) + for ( node_t* node = map.creatures->first; node /*&& false*/; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, HEAL_RADIUS, caster, false, TARGET_FRIEND) ) { - int metal = std::min(totalMetal, SCRAP_MAX_STACK_QTY - 1); - totalMetal -= metal; - Item* crafted = newItem(TOOL_METAL_SCRAP, DECREPIT, 0, metal, 0, true, nullptr); - if ( crafted ) + //Uint8 effectStrength = casterStats->getEffectActive(EFF_BLESS_FOOD); + //if ( effectStrength == 0 ) { - Item* pickedUp = itemPickup(player, crafted); - pickedUpMetal += metal; - if ( i == 0 || players[i]->isLocalPlayer() ) // server/singleplayer - { - free(crafted); // if player != clientnum, then crafted == pickedUp - } - if ( i != 0 && !players[i]->isLocalPlayer() ) + if ( entity->behavior == &actPlayer ) { - free(pickedUp); + if ( entity->setEffect(EFF_BLESS_FOOD, effectStrength, element->duration, false, true, true) ) + { + messagePlayerColor(entity->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6486)); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 174); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + entity->x, entity->y, 16, 2 * TICKS_PER_SECOND, entity) ) + { + + } + } } } } - if ( pickedUpMetal > 0 ) - { - messagePlayer(player, MESSAGE_INVENTORY, Language::get(3665), pickedUpMetal, items[TOOL_METAL_SCRAP].getIdentifiedName()); - } - int pickedUpMagic = 0; - while ( totalMagic > 0 ) + } + + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + } + playSoundEntity(caster, 166, 128); + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_MAGIC_WELL].element_internal_name) ) + { + if ( caster ) + { + if ( Stat* casterStats = caster->getStats() ) + { + Uint8 effectStrength = casterStats->getEffectActive(EFF_MAGIC_WELL); + if ( effectStrength == 0 ) { - int magic = std::min(totalMagic, SCRAP_MAX_STACK_QTY - 1); - totalMagic -= magic; - Item* crafted = newItem(TOOL_MAGIC_SCRAP, DECREPIT, 0, magic, 0, true, nullptr); - if ( crafted ) + if ( caster->setEffect(EFF_MAGIC_WELL, (Uint8)1, 30 * TICKS_PER_SECOND, false) ) { - Item* pickedUp = itemPickup(player, crafted); - pickedUpMagic += magic; - if ( i == 0 || players[i]->isLocalPlayer() ) // server/singleplayer - { - free(crafted); // if player != clientnum, then crafted == pickedUp - } - if ( i != 0 && !players[i]->isLocalPlayer() ) - { - free(pickedUp); - } + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6728)); } } - if ( pickedUpMagic > 0 ) + else { - messagePlayer(player, MESSAGE_INVENTORY, Language::get(3665), pickedUpMagic, items[TOOL_MAGIC_SCRAP].getIdentifiedName()); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(255, 255, 255), Language::get(6727), spell->getSpellName()); } - if ( !effectCoordinates.empty() ) + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + } + playSoundEntity(caster, 166, 128); + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_CRITICAL_SPELL].element_internal_name) ) + { + if ( caster ) + { + if ( Stat* casterStats = caster->getStats() ) + { + Uint8 effectStrength = casterStats->getEffectActive(EFF_CRITICAL_SPELL); + if ( effectStrength == 0 ) { - for ( auto it = effectCoordinates.begin(); it != effectCoordinates.end(); ++it ) + if ( caster->setEffect(EFF_CRITICAL_SPELL, (Uint8)1, 30 * TICKS_PER_SECOND, false) ) { - std::pair coords = *it; - spawnMagicEffectParticles(coords.first * 16 + 8, coords.second * 16 + 8, 7, 171); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6729)); } } - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); - break; + else + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(255, 255, 255), Language::get(6727), spell->getSpellName()); + } } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); } + playSoundEntity(caster, 166, 128); } - else if ( !strcmp(element->element_internal_name, spellElement_trollsBlood.element_internal_name) ) + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_ABSORB_MAGIC].element_internal_name) ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( caster ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + if ( Stat* casterStats = caster->getStats() ) { - int amount = element->duration; - amount += ((spellBookBonusPercent * 2 / 100.f) * amount); // 100-200% - - if ( overdrewIntoHP ) + Uint8 effectStrength = casterStats->getEffectActive(EFF_ABSORB_MAGIC); + if ( effectStrength == 0 ) { - amount /= 4; - messagePlayerColor(player, MESSAGE_COMBAT, makeColorRGB(255, 255, 255), Language::get(3400)); + if ( caster->setEffect(EFF_ABSORB_MAGIC, (Uint8)1, element->duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6735)); + } } + else + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(255, 255, 255), Language::get(6727), spell->getSpellName()); + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 1817); + } + playSoundEntity(caster, 166, 128); + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_FLAME_SHIELD].element_internal_name) ) + { + if ( caster ) + { + if ( Stat* casterStats = caster->getStats() ) + { + if ( caster->setEffect(EFF_FLAME_CLOAK, (Uint8)1, 30 * TICKS_PER_SECOND, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6731)); + } + caster->castOrbitingMagicMissile(SPELL_FIREBALL, 16.0, caster->yaw + 0 * PI / 3, 5 * TICKS_PER_SECOND); + caster->castOrbitingMagicMissile(SPELL_FIREBALL, 16.0, caster->yaw + 2 * PI / 3, 5 * TICKS_PER_SECOND); + caster->castOrbitingMagicMissile(SPELL_FIREBALL, 16.0, caster->yaw + 4 * PI / 3, 5 * TICKS_PER_SECOND); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + } + playSoundEntity(caster, 166, 128); + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_PROF_NIMBLENESS].element_internal_name) ) + { + if ( caster ) + { + if ( Stat* casterStats = caster->getStats() ) + { + int strength = getSpellDamageFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int maxStrength = getSpellDamageSecondaryFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + strength = std::min(strength, maxStrength); - caster->setEffect(EFF_TROLLS_BLOOD, true, amount, true); - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerColor(i, MESSAGE_HINT, color, Language::get(3490)); - for ( node = map.creatures->first; node; node = node->next ) + Uint8 effectStrength = strength; + effectStrength |= ((1 + ((caster->isEntityPlayer() >= 0) ? caster->skill[2] : MAXPLAYERS)) & 0xF) << 4; + + if ( caster->setEffect(EFF_NIMBLENESS, (Uint8)effectStrength, element->duration, false, true, true) ) { - Entity* entity = (Entity*)(node->element); - if ( !entity || entity == caster ) - { - continue; - } - if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) - { - continue; - } + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6476)); + playSoundEntity(caster, 167, 128); - if ( entityDist(entity, caster) <= HEAL_RADIUS && entity->checkFriend(caster) ) + createParticleFociLight(caster, spell->ID, true); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) { - entity->setEffect(EFF_TROLLS_BLOOD, true, amount, true); - playSoundEntity(entity, 168, 128); - spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); - if ( entity->behavior == &actPlayer ) - { - messagePlayerColor(entity->skill[2], MESSAGE_HINT, color, Language::get(3490)); - } } } - break; } } - if ( caster->behavior == &actMonster ) + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_PROF_GREATER_MIGHT].element_internal_name) ) + { + if ( caster ) { - caster->setEffect(EFF_TROLLS_BLOOD, true, element->duration, true); - } + if ( Stat* casterStats = caster->getStats() ) + { + int strength = getSpellDamageFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int maxStrength = getSpellDamageSecondaryFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + strength = std::min(strength, maxStrength); - playSoundEntity(caster, 168, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 169); + Uint8 effectStrength = strength; + effectStrength |= ((1 + ((caster->isEntityPlayer() >= 0) ? caster->skill[2] : MAXPLAYERS)) & 0xF) << 4; + + if ( caster->setEffect(EFF_GREATER_MIGHT, (Uint8)effectStrength, element->duration, false, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6477)); + playSoundEntity(caster, 167, 128); + + createParticleFociLight(caster, spell->ID, true); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + } + } + } + } } - else if ( !strcmp(element->element_internal_name, spellElement_flutter.element_internal_name) ) + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_PROF_COUNSEL].element_internal_name) ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( caster ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + if ( Stat* casterStats = caster->getStats() ) { - //Duration for flutter. - int duration = element->duration; + int strength = getSpellDamageFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int maxStrength = getSpellDamageSecondaryFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + strength = std::min(strength, maxStrength); + + Uint8 effectStrength = strength; + effectStrength |= ((1 + ((caster->isEntityPlayer() >= 0) ? caster->skill[2] : MAXPLAYERS)) & 0xF) << 4; - if ( caster->getStats() && !caster->getStats()->EFFECTS[EFF_FLUTTER] ) + if ( caster->setEffect(EFF_COUNSEL, (Uint8)effectStrength, element->duration, false, true, true) ) { - achievementObserver.playerAchievements[i].flutterShyCoordinates = std::make_pair(caster->x, caster->y); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6478)); + playSoundEntity(caster, 167, 128); + + createParticleFociLight(caster, spell->ID, true); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + } } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_PROF_STURDINESS].element_internal_name) ) + { + if ( caster ) + { + if ( Stat* casterStats = caster->getStats() ) + { + int strength = getSpellDamageFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int maxStrength = getSpellDamageSecondaryFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + strength = std::min(strength, maxStrength); - if ( caster->setEffect(EFF_FLUTTER, true, duration, true) ) + Uint8 effectStrength = strength; + effectStrength |= ((1 + ((caster->isEntityPlayer() >= 0) ? caster->skill[2] : MAXPLAYERS)) & 0xF) << 4; + + if ( caster->setEffect(EFF_STURDINESS, (Uint8)effectStrength, element->duration, false, true, true) ) { - messagePlayerColor(i, MESSAGE_STATUS, uint32ColorGreen, Language::get(3767)); - playSoundEntity(caster, 178, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6479)); + playSoundEntity(caster, 167, 128); + + createParticleFociLight(caster, spell->ID, true); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + } } - break; } } } - else if ( !strcmp(element->element_internal_name, spellElement_dash.element_internal_name) ) + else if ( spell->ID == SPELL_DELAY_PAIN ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( caster ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + if ( Stat* casterStats = caster->getStats() ) { - playSoundEntity(caster, 180, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); - caster->setEffect(EFF_DASH, true, 60, true); - if ( i > 0 && multiplayer == SERVER && !players[i]->isLocalPlayer() ) + if ( !casterStats->getEffectActive(EFF_DELAY_PAIN) ) { - strcpy((char*)net_packet->data, "DASH"); - net_packet->address.host = net_clients[i - 1].host; - net_packet->address.port = net_clients[i - 1].port; - net_packet->len = 4; - sendPacketSafe(net_sock, -1, net_packet, i - 1); + if ( caster->setEffect(EFF_DELAY_PAIN, (Uint8)1, element->duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6627)); + playSoundEntity(caster, 166, 128); + } } else { - real_t vel = sqrt(pow(caster->vel_y, 2) + pow(caster->vel_x, 2)); - caster->monsterKnockbackVelocity = std::min(2.25, std::max(1.0, vel)); - caster->monsterKnockbackTangentDir = atan2(caster->vel_y, caster->vel_x); - if ( vel < 0.01 ) + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(255, 255, 255), Language::get(6727), spell->getSpellName()); + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + } + for ( node_t* node = map.creatures->first; node && false; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, HEAL_RADIUS, caster, false, TARGET_FRIEND) ) + { + if ( entity->getStats() ) + { + if ( !entity->getStats()->getEffectActive(EFF_DELAY_PAIN) ) { - caster->monsterKnockbackTangentDir = caster->yaw + PI; + entity->setEffect(EFF_DELAY_PAIN, (Uint8)1, element->duration, false); + messagePlayerColor(entity->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6627)); + playSoundEntity(entity, 167, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 171); } } - break; } } - if ( caster->behavior == &actMonster ) + } + else if ( spell->ID == SPELL_SACRED_PATH ) + { + if ( caster ) { - playSoundEntity(caster, 180, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); - caster->setEffect(EFF_DASH, true, 30, true); + if ( Stat* casterStats = caster->getStats() ) + { + caster->setEffect(EFF_SACRED_PATH, (Uint8)element->getDamage(), element->duration, false); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6493)); + playSoundEntity(caster, 166, 128); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); } } - else if ( !strcmp(element->element_internal_name, spellElement_speed.element_internal_name) ) + else if ( spell->ID == SPELL_FORCE_SHIELD ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( caster ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + if ( Stat* casterStats = caster->getStats() ) { - //Duration for speed. - int duration = element->duration; - duration += ((spellBookBonusPercent * 2 / 100.f) * duration); // 100-200% - - //duration += (((element->mana + extramagic_to_use) - element->base_mana) / static_cast(element->overload_multiplier)) * element->duration; - //if ( newbie ) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if ( chance >= spellcasting / 10 ) - // { - // duration -= local_rng.rand() % (1000 / (spellcasting + 1)); - // } - // if ( duration < 100 ) - // { - // duration = 100; //Range checking. - // } - //} - - if ( stats[i]->EFFECTS[EFF_SLOW] ) - { - caster->setEffect(EFF_SLOW, false, 0, true); - } - caster->setEffect(EFF_FAST, true, duration, true); - messagePlayerColor(i, MESSAGE_STATUS, uint32ColorGreen, Language::get(768)); - for ( node = map.creatures->first; node; node = node->next ) - { - Entity* entity = (Entity*)(node->element); - if ( !entity || entity == caster ) - { - continue; - } - if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) - { - continue; - } - - if ( entityDist(entity, caster) <= HEAL_RADIUS && entity->checkFriend(caster) ) - { - entity->setEffect(EFF_FAST, true, duration, true); - playSoundEntity(entity, 178, 128); - spawnMagicEffectParticles(entity->x, entity->y, entity->z, 174); - if ( entity->behavior == &actPlayer ) - { - messagePlayerColor(entity->skill[2], MESSAGE_STATUS, uint32ColorGreen, Language::get(768)); - } - } - } - break; + Uint8 effectStrength = std::min(255, std::max(1, getSpellDamageFromID(SPELL_FORCE_SHIELD, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0))); + caster->setEffect(EFF_REFLECTOR_SHIELD, false, 0, false); + caster->setEffect(EFF_FORCE_SHIELD, effectStrength, element->duration, false); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6699)); } + playSoundEntity(caster, 166, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); } - if ( caster->behavior == &actMonster ) + } + else if ( spell->ID == SPELL_REFLECTOR ) + { + if ( caster ) { - if ( caster->getStats()->EFFECTS[EFF_SLOW] ) + if ( Stat* casterStats = caster->getStats() ) { - caster->setEffect(EFF_SLOW, false, 0, true); + caster->setEffect(EFF_FORCE_SHIELD, false, 0, false); + caster->setEffect(EFF_REFLECTOR_SHIELD, true, element->duration, false); + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6700)); } - caster->setEffect(EFF_FAST, true, element->duration, true); + playSoundEntity(caster, 166, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); } - - playSoundEntity(caster, 178, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); } - else if (!strcmp(element->element_internal_name, spellElement_heal.element_internal_name)) //TODO: Make it work for NPCs. + else if ( spell->ID == SPELL_MANIFEST_DESTINY ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( caster && caster->behavior == &actPlayer ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + auto& chests = achievementObserver.playerAchievements[caster->skill[2]].manifestDestinyChests; + auto& sequence = achievementObserver.playerAchievements[caster->skill[2]].manifestDestinyChestSequence; + bool found = false; + if ( chests.size() ) { - int amount = element->damage * (((element->mana + extramagic_to_use) / static_cast(element->base_mana)) * element->overload_multiplier); //Amount to heal. - if (newbie) + int visited = 0; + for ( int i = sequence + 1; visited < chests.size(); ++i ) { - //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - chance = local_rng.rand() % 10; - if (chance >= spellcasting / 10) + if ( i >= chests.size() ) { - amount -= local_rng.rand() % (1000 / (spellcasting + 1)); + i = 0; } - if (amount < 8) + ++visited; + if ( i < chests.size() ) { - amount = 8; //Range checking. + if ( chests[i] == 0 ) { continue; } + if ( !uidToEntity(chests[i]) ) + { + chests[i] = 0; // chest no longer exists + continue; + } + found = true; + sequence = i; + break; } } - // spellbook 100-150%, 50 INT = 200%. - real_t bonus = ((spellBookBonusPercent * 1 / 100.f) + getBonusFromCasterOfSpellElement(caster, nullptr, element, spell ? spell->ID : SPELL_NONE)); - amount += amount * bonus; - if ( caster && caster->behavior == &actPlayer ) - { - Compendium_t::Events_t::eventUpdateCodex(caster->skill[2], Compendium_t::CPDM_CLASS_PWR_MAX_CASTED, "pwr", - (Sint32)(bonus * 100.0)); - } + } - int totalHeal = 0; - int oldHP = players[i]->entity->getHP(); - if ( overdrewIntoHP ) - { - amount /= 2; - } - if ( oldHP > 0 ) - { - spell_changeHealth(players[i]->entity, amount, overdrewIntoHP); - } - totalHeal += std::max(players[i]->entity->getHP() - oldHP, 0); - if ( totalHeal > 0 ) + if ( found ) + { + Entity* spellTimer = createParticleTimer(caster, 50, 625); + spellTimer->particleTimerPreDelay = 0; // wait x ticks before animation. + spellTimer->particleTimerEndAction = PARTICLE_EFFECT_SHRINE_TELEPORT; // teleport behavior of timer. + spellTimer->particleTimerEndSprite = 625; // sprite to use for end of timer function. + spellTimer->particleTimerCountdownAction = 1; + spellTimer->particleTimerCountdownSprite = 625; + spellTimer->particleTimerTarget = static_cast(chests[sequence]); // get the target to teleport around. + spellTimer->particleTimerVariable1 = 1; // distance of teleport in tiles + spellTimer->particleTimerVariable2 = caster->getUID(); // which player to teleport + if ( multiplayer == SERVER ) { - spawnDamageGib(players[i]->entity, -totalHeal, DamageGib::DMG_HEAL, DamageGibDisplayType::DMG_GIB_NUMBER, true); + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_DESTINY_TELEPORT, 625, spellTimer->particleTimerDuration); } - playSoundEntity(caster, 168, 128); - - for ( node = map.creatures->first; node; node = node->next ) + //shrineActivateDelay = 250; + //serverUpdateEntitySkill(this, 7); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6494)); + } + else if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(3715)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + } + } + else if ( spell->ID == SPELL_SCRY_TRAPS ) + { + if ( caster ) + { + std::vector traps; + bool found = false; + bool foundExisting = false; + for ( node_t* node = map.entities->first; node; node = node->next ) + { + if ( Entity* ent = (Entity*)node->element ) { - Entity* entity = (Entity*)(node->element); - if ( !entity || entity == caster ) - { - continue; - } - if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) - { - continue; - } - - if ( entityDist(entity, caster) <= HEAL_RADIUS && entity->checkFriend(caster) ) + if ( ent->behavior == &actBoulderTrapHole || ent->behavior == &actArrowTrap + || ent->behavior == &actMagicTrap || ent->behavior == &actMagicTrapCeiling + || ent->behavior == &actSummonTrap || ent->behavior == &actSpearTrap ) { - oldHP = entity->getHP(); - spell_changeHealth(entity, amount); - int heal = std::max(entity->getHP() - oldHP, 0); - totalHeal += heal; - if ( heal > 0 ) + if ( entityDist(caster, ent) >= TOUCHRANGE * 8 ) { - spawnDamageGib(entity, -heal, DamageGib::DMG_HEAL, DamageGibDisplayType::DMG_GIB_NUMBER, true); + continue; } - playSoundEntity(entity, 168, 128); - spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); - } - } - if ( totalHeal > 0 ) - { - serverUpdatePlayerGameplayStats(i, STATISTICS_HEAL_BOT, totalHeal); - if ( spell && spell->ID > SPELL_NONE ) - { - if ( !using_magicstaff && !trap ) + + bool skip = false; + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(ent, 1); + for ( auto it : entLists ) { - if ( usingSpellbook ) + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) { - auto find = ItemTooltips.spellItems.find(spell->ID); - if ( find != ItemTooltips.spellItems.end() ) + if ( Entity* entity2 = (Entity*)node->element ) { - if ( find->second.spellbookId >= 0 && find->second.spellbookId < NUMITEMS && items[find->second.spellbookId].category == SPELLBOOK ) + if ( entity2->behavior == &actParticlePinpointTarget + && entity2->parent == ent->getUID() ) { - Compendium_t::Events_t::eventUpdate(i, Compendium_t::CPDM_SPELL_HEAL, (ItemType)find->second.spellbookId, totalHeal); + if ( entity2->skill[4] == spell->ID ) + { + foundExisting = true; + skip = true; + } } } } - else + } + + if ( !skip ) + { + found = true; + int duration = element->duration; + createParticleSpellPinpointTarget(ent, caster->getUID(), 1771, duration, spell->ID); + serverSpawnMiscParticles(ent, PARTICLE_EFFECT_PINPOINT, 1771, caster->getUID(), duration, spell->ID); + + if ( caster->behavior == &actPlayer ) { - Compendium_t::Events_t::eventUpdate(i, Compendium_t::CPDM_SPELL_HEAL, SPELL_ITEM, totalHeal, false, spell->ID); + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_SCRY_TRAPS, 10.0, 1.0, nullptr); } } } } - break; } - } - if ( caster->behavior == &actMonster ) - { - spell_changeHealth(caster, element->damage * element->mana); - } + if ( caster->isEntityPlayer() >= 0 ) + { + int pingx = caster->x / 16; + int pingy = caster->y / 16; + sendMinimapPing(caster->isEntityPlayer(), pingx, pingy, 0, true); + } - playSoundEntity(caster, 168, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 169); - } - else if ( !strcmp(element->element_internal_name, spellElement_shapeshift.element_internal_name) - && caster && caster->behavior == &actPlayer ) - { - Monster type = NOTHING; - switch ( spell->ID ) - { - case SPELL_RAT_FORM: - type = RAT; - break; - case SPELL_TROLL_FORM: - type = TROLL; - break; - case SPELL_SPIDER_FORM: - type = SPIDER; - break; - case SPELL_IMP_FORM: - type = CREATURE_IMP; - break; - case SPELL_REVERT_FORM: - break; - default: - break; - } - int duration = 120 * TICKS_PER_SECOND; - if ( type != NOTHING && caster->setEffect(EFF_SHAPESHIFT, true, duration, true) ) - { - spawnExplosion(caster->x, caster->y, caster->z); - playSoundEntity(caster, 400, 92); - createParticleDropRising(caster, 593, 1.f); - serverSpawnMiscParticles(caster, PARTICLE_EFFECT_RISING_DROP, 593); - - caster->effectShapeshift = type; - serverUpdateEntitySkill(caster, 53); - - for ( node = map.creatures->first; node && stat; node = node->next ) + if ( !found ) { - Entity* entity = (Entity*)(node->element); - if ( !entity || entity == caster ) - { - continue; - } - if ( entity->behavior != &actMonster ) + if ( foundExisting ) { - continue; + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6938)); } - if ( entity->monsterTarget == caster->getUID() && entity->checkEnemy(caster) ) + else { - Monster oldType = stat->type; - stat->type = type; - if ( !entity->checkEnemy(caster) ) // we're now friendly. - { - entity->monsterReleaseAttackTarget(); - } - stat->type = oldType; + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6937)); } } + else + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6483)); + } - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerColor(caster->skill[2], MESSAGE_STATUS, color, Language::get(3419), getMonsterLocalizedName((Monster)caster->effectShapeshift).c_str()); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + + } + playSoundEntity(caster, 167, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); } - else + } + else if ( spell->ID == SPELL_SCRY_TREASURES ) + { + if ( caster ) { - if ( spell->ID == SPELL_REVERT_FORM ) + std::vector treasures; + int found = 0; + bool foundExisting = false; + std::set mapTilesWithPinpoints; + for ( node_t* node = map.entities->first; node; node = node->next ) { - if ( stats[caster->skill[2]]->EFFECTS[EFF_SHAPESHIFT] ) + if ( Entity* ent = (Entity*)node->element ) { - int previousShapeshift = caster->effectShapeshift; - caster->setEffect(EFF_SHAPESHIFT, false, 0, true); - caster->effectShapeshift = 0; - serverUpdateEntitySkill(caster, 53); - if ( previousShapeshift == CREATURE_IMP && !isLevitating(stats[caster->skill[2]]) ) + if ( ent->behavior == &actChest || ent->isInertMimic() + || ent->behavior == &actItem || ent->behavior == &actGoldBag ) { - stats[caster->skill[2]]->EFFECTS[EFF_LEVITATING] = true; - stats[caster->skill[2]]->EFFECTS_TIMERS[EFF_LEVITATING] = 5; - } + if ( found >= 8 ) + { + break; + } - if ( stats[caster->skill[2]]->EFFECTS[EFF_POLYMORPH] ) - { - messagePlayer(caster->skill[2], MESSAGE_STATUS, Language::get(4302)); // return to your 'abnormal' form - } - else - { - messagePlayer(caster->skill[2], MESSAGE_STATUS, Language::get(3417)); + if ( ent->behavior == &actItem ) + { + if ( ent->skill[10] >= KEY_STONE && ent->skill[10] <= KEY_MACHINE ) + { + // good items + } + else + { + continue; + } + } + if ( entityDist(caster, ent) >= TOUCHRANGE * 8 ) + { + continue; + } + + if ( ent->behavior == &actChest ) + { + if ( ent->chestVoidState != 0 ) + { + continue; + } + if ( list_t* inventory = ent->getChestInventoryList() ) + { + if ( !inventory->first ) + { + continue; // already looted + } + } + } + + int x = ent->x / 16; + int y = ent->y / 16; + + bool skip = false; + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(ent, 1); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity2 = (Entity*)node->element ) + { + if ( entity2->behavior == &actParticlePinpointTarget ) + { + if ( entity2->skill[4] == spell->ID ) + { + mapTilesWithPinpoints.insert(std::max(0, x) + 10000 * std::max(0, y)); + if ( entity2->parent == ent->getUID() ) + { + foundExisting = true; + skip = true; + } + } + } + } + } + } + + if ( mapTilesWithPinpoints.find(std::max(0, x) + 10000 * std::max(0, y)) != mapTilesWithPinpoints.end() ) + { + continue; + } + + if ( !skip ) + { + found++; + int duration = element->duration; + createParticleSpellPinpointTarget(ent, caster->getUID(), 1772, duration, spell->ID); + serverSpawnMiscParticles(ent, PARTICLE_EFFECT_PINPOINT, 1772, caster->getUID(), duration, spell->ID); + + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_SCRY_TREASURES, 10.0, 1.0, nullptr); + } + } } - playSoundEntity(caster, 400, 92); - createParticleDropRising(caster, 593, 1.f); - serverSpawnMiscParticles(caster, PARTICLE_EFFECT_RISING_DROP, 593); } - else if ( stats[caster->skill[2]]->EFFECTS[EFF_POLYMORPH] ) - { - caster->setEffect(EFF_POLYMORPH, false, 0, true); - caster->effectPolymorph = 0; - serverUpdateEntitySkill(caster, 50); + } - messagePlayer(player, MESSAGE_STATUS, Language::get(3185)); - playSoundEntity(caster, 400, 92); - createParticleDropRising(caster, 593, 1.f); - serverSpawnMiscParticles(caster, PARTICLE_EFFECT_RISING_DROP, 593); + if ( caster->isEntityPlayer() >= 0 ) + { + int pingx = caster->x / 16; + int pingy = caster->y / 16; + sendMinimapPing(caster->isEntityPlayer(), pingx, pingy, 0, true); + } + + if ( !found ) + { + if ( foundExisting ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6940)); } else { - messagePlayer(caster->skill[2], MESSAGE_HINT, Language::get(3715)); - playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6939)); } } else { - messagePlayer(caster->skill[2], MESSAGE_HINT, Language::get(3420)); + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6485)); + } + + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + } + playSoundEntity(caster, 167, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); } } - else if (!strcmp(element->element_internal_name, spellElement_cure_ailment.element_internal_name)) //TODO: Generalize it for NPCs too? + else if ( spell->ID == SPELL_DONATION ) { - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( caster && caster->behavior == &actPlayer ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + bool found = false; + + if ( players[caster->skill[2]]->mechanics.donationRevealedOnFloor == 0 ) { - int c = 0; - int numEffectsCured = 0; - for (c = 0; c < NUMEFFECTS; ++c) //This does a whole lot more than just cure ailments. + std::vector breakables; + for ( node_t* node = map.entities->first; node; node = node->next ) { - if ( stats[i] && stats[i]->statusEffectRemovedByCureAilment(c, players[i]->entity) ) + if ( Entity* entity = (Entity*)node->element ) { - if ( stats[i]->EFFECTS[c] ) + if ( entity->isColliderBreakableContainer() ) { - stats[i]->EFFECTS[c] = false; - if ( stats[i]->EFFECTS_TIMERS[c] > 0 ) + if ( entity->colliderHideMonster == 0 && entity->colliderContainedEntity == 0 ) { - stats[i]->EFFECTS_TIMERS[c] = 1; + if ( entityDist(caster, entity) < 10000.0 ) + { + breakables.push_back(entity); + } } - ++numEffectsCured; } } } - if ( stats[i]->EFFECTS[EFF_WITHDRAWAL] ) + + std::vector>> donations = { - ++numEffectsCured; - players[i]->entity->setEffect(EFF_WITHDRAWAL, false, EFFECT_WITHDRAWAL_BASE_TIME, true); - serverUpdatePlayerGameplayStats(i, STATISTICS_FUNCTIONAL, 1); - } - if ( players[i]->entity->flags[BURNING] ) + {0, { GEM_ROCK, 20 }}, + {1, { POTION_JUICE, 1 }}, + {2, { FOOD_APPLE, 1}}, + {3, { GEM_ROCK, 40 }}, + {4, { POTION_CUREAILMENT, 1 }}, + + {5, { FOOD_BREAD, 1}}, + {6, { POTION_WATER, 1 }}, + {7, { GEM_ROCK, 60 }}, + {8, { POTION_HEALING, 1 }}, + {9, { POTION_RESTOREMAGIC, 1 }}, + + {10, { FOOD_BREAD, 1}}, + {11, { GEM_ROCK, 100 }}, + {12, { SCROLL_REMOVECURSE, 1 }}, + {13, { SCROLL_IDENTIFY, 1 }}, + {14, { POTION_HEALING, 2 }}, + {15, { POTION_RESTOREMAGIC, 1 }}, + + {16, {POTION_RESTOREMAGIC, 2}}, + {17, { GEM_ROCK, 150 }}, + {18, { FOOD_BREAD, 2 }}, + {19, { POTION_EXTRAHEALING, 1 }}, + {20, { POTION_RESTOREMAGIC, 3 }} + }; + + int strength = getSpellDamageFromID(SPELL_DONATION, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + strength = std::max(0, strength - 1); + strength = std::min(100, strength); + int minTier = std::max(0, std::min(12, strength * 12 / 100)); + int maxTier = std::max(0, (strength * 12 / 100)) + 4; + std::vector chances; + chances.resize(donations.size()); + bool anyChances = false; + for ( int i = 0; i < chances.size(); ++i ) { - ++numEffectsCured; - players[i]->entity->flags[BURNING] = false; - serverUpdateEntityFlag(players[i]->entity, BURNING); + chances[i] = 0; + if ( i >= minTier && i <= maxTier ) + { + chances[i] = 1; + anyChances = true; + } } - - bool regenEffect = spellBookBonusPercent >= 25; - if ( regenEffect ) + if ( !anyChances ) { - int bonus = 10 * ((spellBookBonusPercent * 4) / 100.f); // 25% = 10 seconds, 50% = 20 seconds. - caster->setEffect(EFF_HP_REGEN, true, std::max(stats[i]->EFFECTS_TIMERS[EFF_HP_REGEN], bonus * TICKS_PER_SECOND), true); + chances[0] = 1; // just in case } - if ( numEffectsCured > 0 || regenEffect ) + while ( breakables.size() ) { - serverUpdateEffects(player); - } - playSoundEntity(players[i]->entity, 168, 128); + int pick = local_rng.rand() % breakables.size(); + auto entity = breakables[pick]; + int pickDonation = local_rng.discrete(chances.data(), chances.size()); - int numAlliesEffectsCured = 0; - for ( node = map.creatures->first; node; node = node->next ) - { - Entity* entity = (Entity*)(node->element); - if ( !entity || entity == caster ) - { - continue; - } - if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) + Entity* item = newEntity(-1, 1, map.entities, nullptr); //Rock entity. + item->flags[INVISIBLE] = true; + item->flags[UPDATENEEDED] = true; + item->x = entity->x; + item->y = entity->y; + + auto& donation = donations.at(pickDonation); + if ( donation.second.first == GEM_ROCK ) { - continue; + item->behavior = &actGoldBag; + item->sprite = 130; + item->z = 6.25; + item->goldBouncing = 1; + item->goldAmount = donation.second.second; + item->goldInContainer = entity->getUID(); + entity->colliderContainedEntity = item->getUID(); + serverUpdateEntitySkill(entity, 15); // update colliderContainedEntity } - int entityEffectsCured = 0; - Stat* target_stat = entity->getStats(); - if ( target_stat ) + else { - if (entityDist(entity, caster) <= HEAL_RADIUS && entity->checkFriend(caster)) + item->behavior = &actItem; + item->z = 0.0; + item->vel_z = -.5; // important to not immediately be on ground and set NOUPDATE + item->roll = PI / 2.0; + item->itemContainer = entity->getUID(); + entity->colliderContainedEntity = item->getUID(); + serverUpdateEntitySkill(entity, 15); // update colliderContainedEntity + + item->skill[10] = donation.second.first; // type + item->skill[11] = EXCELLENT; // status + item->skill[12] = 0; // beatitude + if ( donation.second.first == POTION_WATER ) { - for (c = 0; c < NUMEFFECTS; ++c) //This does a whole lot more than just cure ailments. - { - if ( target_stat->statusEffectRemovedByCureAilment(c, entity) ) - { - if ( target_stat->EFFECTS[c] ) - { - target_stat->EFFECTS[c] = false; - if ( target_stat->EFFECTS_TIMERS[c] > 0 ) - { - target_stat->EFFECTS_TIMERS[c] = 1; - } - ++numAlliesEffectsCured; - ++entityEffectsCured; - } - } - } - if ( target_stat->EFFECTS[EFF_WITHDRAWAL] ) - { - ++numAlliesEffectsCured; - ++entityEffectsCured; - entity->setEffect(EFF_WITHDRAWAL, false, EFFECT_WITHDRAWAL_BASE_TIME, true); - serverUpdatePlayerGameplayStats(i, STATISTICS_FUNCTIONAL, 1); - } - if ( regenEffect ) - { - int bonus = 10 * ((spellBookBonusPercent * 4) / 100.f); // 25% = 10 seconds, 50% = 20 seconds. - entity->setEffect(EFF_HP_REGEN, true, std::max(target_stat->EFFECTS_TIMERS[EFF_HP_REGEN], bonus * TICKS_PER_SECOND), true); - } - if ( entity->flags[BURNING] ) + item->skill[12] = 4; // beatitude + } + if ( donation.second.first == FOOD_BREAD ) + { + if ( donation.first >= 15 ) { - ++numAlliesEffectsCured; - ++entityEffectsCured; - entity->flags[BURNING] = false; - serverUpdateEntityFlag(entity, BURNING); + item->skill[12] = 2; // beatitude } - if ( entity->behavior == &actPlayer && (entityEffectsCured > 0 || regenEffect) ) + else if ( donation.first >= 10 ) { - serverUpdateEffects(entity->skill[2]); - messagePlayerColor(entity->skill[2], MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(411)); + item->skill[12] = 1; // beatitude } - - playSoundEntity(entity, 168, 128); - spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); } + item->skill[13] = 1; // count + item->skill[14] = 0; // appearance + item->skill[15] = 1; // identified } - } - if ( regenEffect || numEffectsCured > 0 ) + item->sizex = 4; + item->sizey = 4; + item->yaw = local_rng.rand() % 360 * PI / 180; + item->flags[PASSABLE] = true; + item->flags[USERFLAG1] = true; // no collision: helps performance + players[caster->skill[2]]->mechanics.donationRevealedOnFloor = item->getUID(); + + int duration = element->duration; + createParticleSpellPinpointTarget(entity, caster->getUID(), 1768, duration, spell->ID); + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_PINPOINT, 1768, caster->getUID(), duration, spell->ID); + + playSoundEntity(entity, 167, 128); + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_DONATION, 100.0, 1.0, nullptr); + + found = true; + break; + + if ( !found ) + { + breakables.erase(breakables.begin() + pick); + } + } + if ( !found ) { - messagePlayerColor(i, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(411)); // your body feels cleansed + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6942)); } - if ( numAlliesEffectsCured > 0 ) + else { - messagePlayerColor(i, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(4313)); // your allies feel cleansed + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6488)); } - if ( !regenEffect && numEffectsCured == 0 && numAlliesEffectsCured == 0 ) + + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) { - messagePlayer(i, MESSAGE_STATUS, Language::get(3715)); // had no effect. + } - break; + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + playSoundEntity(caster, 167, 128); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6941)); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + playSoundEntity(caster, 163, 128); } } - - playSoundEntity(caster, 168, 128 ); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 169); } - else if ( !strcmp(element->element_internal_name, spellElement_summon.element_internal_name) ) + else if ( spell->ID == SPELL_SCRY_ALLIES ) { - playSoundEntity(caster, 251, 128); - playSoundEntity(caster, 252, 128); - if ( caster->behavior == &actPlayer && stats[caster->skill[2]] ) + bool found = false; + bool foundExisting = false; + if ( caster && caster->behavior == &actPlayer ) { - // kill old summons. - for ( node = stats[caster->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + std::vector allies; + for ( node_t* node = map.creatures->first; node; node = node->next ) { - Entity* follower = nullptr; - if ( (Uint32*)(node)->element ) + if ( Entity* entity = getSpellTarget(node, 10000, caster, false, TARGET_FRIEND) ) { - follower = uidToEntity(*((Uint32*)(node)->element)); + if ( !entity->monsterAllyGetPlayerLeader() && entity->behavior != &actPlayer ) + { + if ( Stat* stats = entity->getStats() ) + { + //if ( stats->leader_uid == 0 ) + if ( stats->type != SHOPKEEPER && !entity->monsterCanTradeWith(-1) ) + { + if ( stats->leader_uid == 0 || (stats->leader_uid && achievementObserver.checkUidIsFromPlayer(stats->leader_uid) < 0) ) + { + bool skip = false; + std::vector entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(entity, 1); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity2 = (Entity*)node->element ) + { + if ( entity2->behavior == &actParticlePinpointTarget + && entity2->parent == entity->getUID() ) + { + if ( entity2->skill[4] == spell->ID ) + { + foundExisting = true; + skip = true; + } + } + } + } + } + + if ( !skip ) + { + found = true; + int duration = element->duration; + createParticleSpellPinpointTarget(entity, caster->getUID(), 1769, duration, spell->ID); + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_PINPOINT, 1769, caster->getUID(), duration, spell->ID); + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spellEventFlags | spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + break; + } + } + } + } + } } - if ( follower && follower->monsterAllySummonRank != 0 ) + } + if ( !found ) + { + if ( foundExisting ) { - Stat* followerStats = follower->getStats(); - if ( followerStats ) + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6899)); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6936)); + } + } + else + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6481)); + } + + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_CALL_ALLIES ) + { + bool found = false; + if ( caster && caster->behavior == &actPlayer ) + { + std::vector allies; + for ( node_t* node = map.creatures->first; node; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, 10000, caster, false, TARGET_FRIEND) ) + { + if ( !entity->monsterAllyGetPlayerLeader() && entity->behavior != &actPlayer ) { - follower->setMP(followerStats->MAXMP * (followerStats->HP / static_cast(followerStats->MAXHP))); - follower->setHP(0); + if ( entity->monsterState == MONSTER_STATE_WAIT ) + { + if ( Stat* stats = entity->getStats() ) + { + //if ( stats->leader_uid == 0 ) + if ( stats->type != SHOPKEEPER && !entity->isInertMimic() && !entity->monsterCanTradeWith(-1) ) + { + if ( entity->monsterSetPathToLocation(caster->x / 16, caster->y / 16, + 3, GeneratePathTypes::GENERATE_PATH_TO_HUNT_MONSTER_TARGET, true) ) + { + entity->monsterState = MONSTER_STATE_HUNT; + found = true; + } + } + } + } + //createParticleSpellPinpointTarget(entity, caster->getUID(), 1769, 30 * TICKS_PER_SECOND, spell->ID); + //serverSpawnMiscParticles(entity, PARTICLE_EFFECT_PINPOINT, 1769, caster->getUID()); } } } + } - real_t startx = caster->x; - real_t starty = caster->y; - real_t startz = -4; - real_t pitch = caster->pitch; - if ( pitch < 0 ) + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(3715)); + } + else + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6489)); + } + if ( caster ) + { + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) { - pitch = 0; + } - // draw line from the players height and direction until we hit the ground. - real_t previousx = startx; - real_t previousy = starty; - int index = 0; - for ( ; startz < 0.f; startz += abs(0.05 * tan(pitch)) ) + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + } + playSoundEntity(caster, 167, 128); + } + else if ( spell->ID == SPELL_SEEK_ALLY ) + { + bool foundTarget = false; + if ( caster && caster->behavior == &actPlayer ) + { + if ( castSpellProps && castSpellProps->targetUID != 0 ) { - startx += 0.1 * cos(caster->yaw); - starty += 0.1 * sin(caster->yaw); - index = (static_cast(starty + 16 * sin(caster->yaw)) >> 4) * MAPLAYERS - + (static_cast(startx + 16 * cos(caster->yaw)) >> 4) * MAPLAYERS * map.height; - if ( map.tiles[index] && !map.tiles[OBSTACLELAYER + index] ) + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) { - // store the last known good coordinate - previousx = startx; - previousy = starty; + if ( target->getStats() ) + { + spawnMagicEffectParticles(target->x, target->y, target->z, 2341); + + foundTarget = true; + std::vector allies; + std::vector alliesGood; + bool foundAlly = false; + if ( target->setEffect(EFF_SEEK_CREATURE, true, element->duration, false) ) + { + for ( node_t* node = map.creatures->first; node; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, 10000, target, false, TARGET_FRIEND) ) + { + if ( !entity->monsterAllyGetPlayerLeader() && entity->behavior != &actPlayer && entity != caster ) + { + if ( entity->monsterTarget != caster->getUID() ) + { + alliesGood.push_back(entity); + } + else + { + allies.push_back(entity); + } + } + } + } + + while ( alliesGood.size() && !foundAlly ) + { + int pick = local_rng.rand() % alliesGood.size(); + Entity* ally = alliesGood[pick]; + alliesGood.erase(alliesGood.begin() + pick); + if ( target->monsterSetPathToLocation(ally->x / 16, ally->y / 16, + 1, GeneratePathTypes::GENERATE_PATH_TO_HUNT_MONSTER_TARGET, true) ) + { + target->monsterReleaseAttackTarget(); + target->monsterState = MONSTER_STATE_HUNT; + foundAlly = true; + break; + } + } + while ( allies.size() && !foundAlly ) + { + int pick = local_rng.rand() % allies.size(); + Entity* ally = allies[pick]; + allies.erase(allies.begin() + pick); + if ( target->monsterSetPathToLocation(ally->x / 16, ally->y / 16, + 1, GeneratePathTypes::GENERATE_PATH_TO_HUNT_MONSTER_TARGET, true) ) + { + target->monsterReleaseAttackTarget(); + target->monsterState = MONSTER_STATE_HUNT; + foundAlly = true; + break; + } + } + + if ( !foundAlly ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 255, 255), + *target->getStats(), Language::get(6630), Language::get(6631), MSG_COMBAT); + target->setEffect(EFF_SEEK_CREATURE, false, 0, false); + playSoundEntity(caster, 163, 128); + + applyGenericMagicDamage(caster, target, *caster, spell->ID, 0, true, false); // alert the target + } + else + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *target->getStats(), Language::get(6633), Language::get(6634), MSG_COMBAT); + playSoundEntity(caster, 167, 128); + + magicOnEntityHit(caster, caster, target, target->getStats(), 0, 0, 0, spell ? spell->ID : SPELL_NONE, usingSpellbook ? SPELLBOOK_SEEK_ALLY : 0); + + /*serverSpawnMiscParticles(target, PARTICLE_EFFECT_CONTROL, 2341); + for ( int i = 0; i < 4; ++i ) + { + Entity* fx = spawnMagicParticle(target); + fx->sprite = 2341; + fx->yaw = target->yaw + i * PI / 2; + fx->scalex = 0.7; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->vel_x = 0.5 * cos(target->yaw + i * PI / 2); + fx->vel_y = 0.5 * sin(target->yaw + i * PI / 2); + }*/ + + //createParticleSpellPinpointTarget(target, caster->getUID(), 1777, 2 * TICKS_PER_SECOND, spell->ID); + //serverSpawnMiscParticles(target, PARTICLE_EFFECT_PINPOINT, 1777, caster->getUID(), 2 * TICKS_PER_SECOND, spell->ID); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + target->x, target->y, 16, 2 * TICKS_PER_SECOND, target) ) + { + } + } + } + else + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *target->getStats(), Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + } + } } - if ( map.tiles[OBSTACLELAYER + index] ) + } + } + + if ( !foundTarget ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + if ( caster ) + { + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2341); + } + //playSoundEntity(caster, 167, 128); + } + else if ( spell->ID == SPELL_SEEK_FOE ) + { + bool foundTarget = false; + if ( caster && caster->behavior == &actPlayer ) + { + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) { - break; + if ( target->getStats() ) + { + spawnMagicEffectParticles(target->x, target->y, target->z, 2341); + playSoundEntity(target, 167, 128); + foundTarget = true; + std::vector enemies; + bool foundEnemy = false; + if ( target->setEffect(EFF_SEEK_CREATURE, true, 10 * TICKS_PER_SECOND, false) ) + { + for ( node_t* node = map.creatures->first; node; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, 10000, target, false, TARGET_ENEMY) ) + { + if ( !entity->monsterAllyGetPlayerLeader() && entity->behavior != &actPlayer && entity != caster ) + { + enemies.push_back(entity); + } + } + } + while ( enemies.size() ) + { + int pick = local_rng.rand() % enemies.size(); + Entity* enemy = enemies[pick]; + enemies.erase(enemies.begin() + pick); + if ( target->monsterSetPathToLocation(enemy->x / 16, enemy->y / 16, + 1, GeneratePathTypes::GENERATE_PATH_TO_HUNT_MONSTER_TARGET, true) ) + { + target->monsterReleaseAttackTarget(); + target->monsterAcquireAttackTarget(*enemy, MONSTER_STATE_HUNT, false); + target->monsterState = MONSTER_STATE_HUNT; + foundEnemy = true; + break; + } + } + + if ( !foundEnemy ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 255, 255), + *target->getStats(), Language::get(6635), Language::get(6636), MSG_COMBAT); + target->setEffect(EFF_SEEK_CREATURE, false, 0, false); + } + else + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *target->getStats(), Language::get(6638), Language::get(6639), MSG_COMBAT); + + //createParticleSpellPinpointTarget(target, caster->getUID(), 1778, 2 * TICKS_PER_SECOND, spell->ID); + //serverSpawnMiscParticles(target, PARTICLE_EFFECT_PINPOINT, 1778, caster->getUID(), 2 * TICKS_PER_SECOND, spell->ID); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + target->x, target->y, 16, 2 * TICKS_PER_SECOND, target) ) + { + } + } + } + else + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *target->getStats(), Language::get(2905), Language::get(2906), MSG_COMBAT); + } + } } } + } - Entity* timer = createParticleTimer(caster, 55, 0); - timer->x = static_cast(previousx / 16) * 16 + 8; - timer->y = static_cast(previousy / 16) * 16 + 8; - timer->sizex = 4; - timer->sizey = 4; - timer->particleTimerCountdownSprite = 791; - timer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SPELL_SUMMON; - timer->particleTimerPreDelay = 40; - timer->particleTimerEndAction = PARTICLE_EFFECT_SPELL_SUMMON; - timer->z = 0; - Entity* sapParticle = createParticleSapCenter(caster, caster, SPELL_SUMMON, 599, 599); - sapParticle->parent = 0; - sapParticle->yaw = caster->yaw; - sapParticle->skill[7] = caster->getUID(); - sapParticle->skill[8] = timer->x; - sapParticle->skill[9] = timer->y; - serverSpawnMiscParticlesAtLocation(previousx / 16, previousy / 16, 0, PARTICLE_EFFECT_SPELL_SUMMON, 791); + if ( !foundTarget ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + if ( caster ) + { + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2341); + } + playSoundEntity(caster, 167, 128); + } + else if ( spell->ID == SPELL_TURN_UNDEAD ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + if ( Entity* fx = createRadiusMagic(SPELL_TURN_UNDEAD, caster, + castSpellProps->target_x, castSpellProps->target_y, 32, + getSpellEffectDurationFromID(SPELL_TURN_UNDEAD, caster, nullptr, caster), nullptr) ) + { + playSoundEntity(fx, 166, 128); + if ( spellBookBonusPercent > 0 ) + { + fx->actmagicSpellbookBonus = spellBookBonusPercent; + } + fx->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + } + } + /*if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + }*/ + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + playSoundEntity(caster, 166, 64); + } + } + else if ( spell->ID == SPELL_MAXIMISE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( Stat* targetStats = target->getStats() ) + { + int strength = getSpellDamageFromID(SPELL_MAXIMISE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int maxStrength = getSpellDamageSecondaryFromID(SPELL_MAXIMISE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + + Uint8 prevStrength = targetStats->getEffectActive(EFF_MAXIMISE) & 0xF; + strength += prevStrength; + strength = std::min(maxStrength, strength); + + Uint8 effectStrength = strength; + effectStrength |= ((1 + ((caster->isEntityPlayer() >= 0) ? caster->skill[2] : MAXPLAYERS)) & 0xF) << 4; + + if ( target->setEffect(EFF_MAXIMISE, effectStrength, element->duration, true, true, true) ) + { + if ( targetStats->getEffectActive(EFF_MINIMISE) ) + { + target->setEffect(EFF_MINIMISE, false, 0, true); + } + playSoundEntity(caster, 167, 128); + effect = true; + if ( strength == prevStrength ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 255, 255), + *targetStats, Language::get(6891), Language::get(6892), MSG_COMBAT); + } + else + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6516), Language::get(6890), MSG_COMBAT); + messagePlayer(target->isEntityPlayer(), makeColorRGB(255, 255, 255), Language::get(6896)); + } + + spawnMagicEffectParticles(target->x, target->y, target->z, 2335); + + magicOnEntityHit(caster, caster, target, targetStats, 0, 0, 0, spell ? spell->ID : SPELL_NONE, usingSpellbook ? SPELLBOOK_MAXIMISE : 0); + + if ( !caster->checkFriend(target) ) + { + applyGenericMagicDamage(caster, target, *caster, spell->ID, 0, true, false); // alert the target + } + + for ( int i = 0; i < 3; ++i ) + { + Entity* fx = createParticleAestheticOrbit(target, 2335, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * i; + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += i * 2 * PI / 3; + fx->actmagicNoLight = (i == 0 ? 0 : 1); + } + + serverSpawnMiscParticles(target, PARTICLE_EFFECT_STATIC_MAXIMISE, 2335); + } + found = true; + + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2335); + //playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_MINIMISE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( Stat* targetStats = target->getStats() ) + { + int strength = getSpellDamageFromID(SPELL_MINIMISE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int maxStrength = getSpellDamageSecondaryFromID(SPELL_MINIMISE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + + Uint8 prevStrength = targetStats->getEffectActive(EFF_MINIMISE) & 0xF; + strength += prevStrength; + strength = std::min(maxStrength, strength); + + Uint8 effectStrength = strength; + effectStrength |= ((1 + ((caster->isEntityPlayer() >= 0) ? caster->skill[2] : MAXPLAYERS)) & 0xF) << 4; + + if ( target->setEffect(EFF_MINIMISE, effectStrength, element->duration, true, true, true) ) + { + if ( targetStats->getEffectActive(EFF_MAXIMISE) ) + { + target->setEffect(EFF_MAXIMISE, false, 0, true); + } + playSoundEntity(caster, 167, 128); + effect = true; + if ( strength == prevStrength ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 255, 255), + *targetStats, Language::get(6894), Language::get(6895), MSG_COMBAT); + } + else + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6517), Language::get(6893), MSG_COMBAT); + messagePlayer(target->isEntityPlayer(), makeColorRGB(255, 255, 255), Language::get(6897)); + } + + spawnMagicEffectParticles(target->x, target->y, target->z, 2341); + + magicOnEntityHit(caster, caster, target, targetStats, 0, 0, 0, spell ? spell->ID : SPELL_NONE, usingSpellbook ? SPELLBOOK_MINIMISE : 0); + + if ( !caster->checkFriend(target) ) + { + applyGenericMagicDamage(caster, target, *caster, spell->ID, 0, true, false); // alert the target + } + + for ( int i = 0; i < 3; ++i ) + { + Entity* fx = createParticleAestheticOrbit(target, 2341, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * i; + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += i * 2 * PI / 3; + fx->actmagicNoLight = (i == 0 ? 0 : 1); + } + + serverSpawnMiscParticles(target, PARTICLE_EFFECT_STATIC_MAXIMISE, 2341); + } + found = true; + + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2341); + //playSoundEntity(caster, 167, 128); + } + } + /*else if ( spell->ID == SPELL_INCOHERENCE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( Stat* targetStats = target->getStats() ) + { + if ( target->setEffect(EFF_INCOHERENCE, true, TICKS_PER_SECOND * 5, false) ) + { + playSoundEntity(caster, 167, 128); + effect = true; + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6519)); + spawnMagicEffectParticles(target->x, target->y, target->z, 171); + } + found = true; + + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); + } + }*/ + else if ( spell->ID == SPELL_COWARDICE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( Stat* targetStats = target->getStats() ) + { + int strength = getSpellDamageFromID(SPELL_COWARDICE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int maxStrength = getSpellDamageSecondaryFromID(SPELL_COWARDICE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + + Uint8 effectStrength = std::min(strength, maxStrength); + if ( target->setEffect(EFF_COWARDICE, effectStrength, element->duration, true, true, true) ) + { + if ( targetStats->getEffectActive(EFF_COURAGE) ) + { + target->setEffect(EFF_COURAGE, false, 0, false); + } + playSoundEntity(caster, 687, 128); + effect = true; + + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6632), Language::get(6900), MSG_COMBAT); + + magicOnEntityHit(caster, caster, target, targetStats, 0, 0, 0, spell ? spell->ID : SPELL_NONE, usingSpellbook ? SPELLBOOK_COWARDICE : 0); + + applyGenericMagicDamage(caster, target, *caster, spell->ID, 0, true, false); // alert the target + + spawnMagicEffectParticles(target->x, target->y, target->z, 791); + createParticleDropRising(target, 2341, 0.5); + serverSpawnMiscParticles(target, PARTICLE_EFFECT_VAMPIRIC_AURA, 2341); // for half size instead of PARTICLE_EFFECT_RISING_DROP + } + found = true; + + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 791); + //playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_COURAGE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( Stat* targetStats = target->getStats() ) + { + int strength = getSpellDamageFromID(SPELL_COURAGE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int maxStrength = getSpellDamageSecondaryFromID(SPELL_COURAGE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + + Uint8 effectStrength = std::min(strength, maxStrength); + + if ( target->setEffect(EFF_COURAGE, effectStrength, element->duration, true, true, true) ) + { + if ( targetStats->getEffectActive(EFF_COWARDICE) ) + { + target->setEffect(EFF_COWARDICE, false, 0, false); + } + playSoundEntity(caster, 167, 128); + effect = true; + + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6637), Language::get(6901), MSG_COMBAT); + + magicOnEntityHit(caster, caster, target, targetStats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + + spawnMagicEffectParticles(target->x, target->y, target->z, 2335); + createParticleDropRising(target, 2335, 0.5); + serverSpawnMiscParticles(target, PARTICLE_EFFECT_VAMPIRIC_AURA, 2335); // for half size instead of PARTICLE_EFFECT_RISING_DROP + } + found = true; + + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + //playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_TABOO ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + spawnMagicEffectParticles(target->x, target->y, target->z, 2341); + + if ( Stat* targetStats = target->getStats() ) + { + int duration = element->duration; + if ( target->setEffect(EFF_TABOO, (Uint8)(caster->behavior != &actPlayer ? MAXPLAYERS + 1 : caster->skill[2] + 1), duration, true, true, true) ) + { + playSoundEntity(caster, 167, 128); + effect = true; + if ( target->behavior == &actPlayer ) + { + messagePlayerColor(target->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(255, 0, 0), Language::get(6641)); + } + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6640), Language::get(6903), MSG_COMBAT); + + //createParticleSpellPinpointTarget(target, caster->getUID(), 1776, duration, spell->ID); + //serverSpawnMiscParticles(target, PARTICLE_EFFECT_PINPOINT, 1776, caster->getUID(), duration, spell->ID); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + target->x, target->y, 16, duration, target) ) + { + } + + for ( node_t* node = map.creatures->first; node; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, 10000, caster, false, TARGET_ENEMY) ) + { + if ( entity != target ) + { + if ( entityDist(entity, target) < 16.0 * 4 ) + { + Entity* entityTarget = uidToEntity(entity->monsterTarget); + if ( !entityTarget || entityTarget == caster || + caster->checkFriend(entityTarget) ) + { + // other nearby enemies locate the taboo target + // if target attacking caster / caster allies then it gets redirected too + Entity* ohit = hit.entity; + real_t tangent = atan2(target->y - entity->y, target->x - entity->x); + lineTraceTarget(entity, entity->x, entity->y, tangent, 16.0 * 4, 0, false, target); + if ( hit.entity == target ) + { + if ( entity->checkEnemy(target) ) + { + entity->monsterAcquireAttackTarget(*target, MONSTER_STATE_PATH); + } + } + hit.entity = ohit; + } + } + } + } + } + } + found = true; + + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2341); + //playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_BOOBY_TRAP ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + found = true; + + Entity* spellTimer = createParticleBoobyTrapExplode(caster, target->x, target->y); + spellTimer->particleTimerVariable1 = getSpellDamageFromID(SPELL_BOOBY_TRAP, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + spellTimer->particleTimerVariable2 = SPELL_BOOBY_TRAP; + spellTimer->particleTimerTarget = target->getUID(); + + serverSpawnMiscParticlesAtLocation(spellTimer->x, spellTimer->y, 0, PARTICLE_EFFECT_BOOBY_TRAP, 0); + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_DEMESNE_DOOR ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + found = true; + + Entity* door = spellEffectDemesneDoor(*caster, *target); + if ( door ) + { + door->skill[0] = element->duration; + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6694)); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6692)); + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_SPIRIT_WEAPON ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + real_t x = floor(castSpellProps->target_x / 16) * 16 + 8.0; + real_t y = floor(castSpellProps->target_y / 16) * 16 + 8.0; + if ( Entity* monster = spellEffectAdorcise(*caster, spellElementMap[SPELL_SPIRIT_WEAPON], x, y, nullptr) ) + { + if ( usingSpellbook ) + { + if ( Stat* monsterStats = monster->getStats() ) + { + monsterStats->setAttribute("SUMMON_BY_SPELLBOOK", std::to_string(SPELL_SPIRIT_WEAPON)); + } + } + } + else + { + if ( caster->behavior == &actPlayer ) + { + // no room to spawn! + messagePlayer(caster->skill[2], MESSAGE_MISC, Language::get(6578)); + } + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + } + } + else if ( spell->ID == SPELL_FIRE_SPRITE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + real_t x = floor(castSpellProps->target_x / 16) * 16 + 8.0; + real_t y = floor(castSpellProps->target_y / 16) * 16 + 8.0; + if ( Entity* monster = spellEffectFlameSprite(*caster, spellElementMap[SPELL_FIRE_SPRITE], x, y) ) + { + if ( caster->behavior == &actPlayer ) + { + for ( node = stats[caster->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + { + Entity* follower = nullptr; + if ( (Uint32*)(node)->element ) + { + follower = uidToEntity(*((Uint32*)(node)->element)); + } + if ( follower && follower != monster ) + { + Stat* followerStats = follower->getStats(); + if ( followerStats && followerStats->type == MOTH_SMALL && followerStats->getAttribute("fire_sprite") != "" ) + { + follower->setMP(0); + follower->setHP(0); + followerStats->setAttribute("skip_obituary", "1"); + } + } + } + } + } + else + { + if ( caster->behavior == &actPlayer ) + { + // no room to spawn! + messagePlayer(caster->skill[2], MESSAGE_MISC, Language::get(6578)); + } + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2207); + playSoundEntity(caster, 164, 128); + } + } + else if ( spell->ID == SPELL_FLAME_ELEMENTAL ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + real_t x = floor(castSpellProps->target_x / 16) * 16 + 8.0; + real_t y = floor(castSpellProps->target_y / 16) * 16 + 8.0; + if ( Entity* monster = spellEffectFlameSprite(*caster, spellElementMap[SPELL_FLAME_ELEMENTAL], x, y) ) + { + if ( caster->behavior == &actPlayer ) + { + for ( node = stats[caster->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + { + Entity* follower = nullptr; + if ( (Uint32*)(node)->element ) + { + follower = uidToEntity(*((Uint32*)(node)->element)); + } + if ( follower && follower != monster ) + { + Stat* followerStats = follower->getStats(); + if ( followerStats && followerStats->type == FLAME_ELEMENTAL ) + { + follower->setMP(0); + follower->setHP(0); + followerStats->setAttribute("skip_obituary", "1"); + } + } + } + } + } + else + { + if ( caster->behavior == &actPlayer ) + { + // no room to spawn! + messagePlayer(caster->skill[2], MESSAGE_MISC, Language::get(6578)); + } + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 164, 128); + } + } + else if ( spell->ID == SPELL_NULL_AREA ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + createRadiusMagic(SPELL_NULL_AREA, caster, + castSpellProps->target_x, castSpellProps->target_y, + std::max(16, std::min(255, getSpellEffectDurationSecondaryFromID(SPELL_NULL_AREA, caster, nullptr, caster))), + getSpellEffectDurationFromID(SPELL_NULL_AREA, caster, nullptr, caster), nullptr); + + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + } + } + else if ( spell->ID == SPELL_HEAL_PULSE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + createRadiusMagic(SPELL_HEAL_PULSE, caster, + castSpellProps->target_x, castSpellProps->target_y, + std::max(16, std::min(255, getSpellEffectDurationSecondaryFromID(SPELL_HEAL_PULSE, caster, nullptr, caster))), + getSpellEffectDurationFromID(SPELL_HEAL_PULSE, caster, nullptr, caster), nullptr); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + } + } + else if ( spell->ID == SPELL_LIGHTNING_BOLT ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + + Uint32 lifetime = spell->life_time; + + Entity* spellTimer = createParticleTimer(caster, lifetime + TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_LIGHTNING; + spellTimer->particleTimerCountdownSprite = 1757; + spellTimer->yaw = caster->yaw; + spellTimer->x = castSpellProps->target_x; + spellTimer->y = castSpellProps->target_y; + spellTimer->flags[NOUPDATE] = false; // spawn for client + spellTimer->flags[UPDATENEEDED] = true; + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + spellTimer->particleTimerEffectLifetime = lifetime; + if ( spellBookBonusPercent > 0 ) + { + spellTimer->actmagicSpellbookBonus = spellBookBonusPercent; + } + spellTimer->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + floorMagicCreateLightningSequence(spellTimer, 0); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 806, 128); + } + } + else if ( spell->ID == SPELL_ETERNALS_GAZE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + + Entity* spellTimer = createParticleTimer(caster, 3 * TICKS_PER_SECOND + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_ETERNALS_GAZE; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->yaw = caster->yaw; + spellTimer->x = castSpellProps->target_x; + spellTimer->y = castSpellProps->target_y; + spellTimer->flags[NOUPDATE] = false; // spawn for client + spellTimer->flags[UPDATENEEDED] = true; + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + } + } + else if ( spell->ID == SPELL_SHATTER_EARTH ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + int x = castSpellProps->target_x / 16; + int y = castSpellProps->target_y / 16; + bool noroom = false; + if ( x < 0 || x >= map.width || y < 0 || y >= map.height ) + { + noroom = true; + } + else + { + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( map.tiles[OBSTACLELAYER + mapIndex] ) + { + noroom = true; + } + else if ( map.skybox != 0 ) + { + if ( !map.tiles[(MAPLAYERS - 1) + mapIndex] ) + { + noroom = true; + } + } + } + + if ( !noroom ) + { + found = true; + Entity* spellTimer = createParticleTimer(caster, 5 * TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SHATTER_EARTH; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->yaw = caster->yaw; + spellTimer->x = x * 16.0 + 8.0; + spellTimer->y = y * 16.0 + 8.0; + spellTimer->flags[NOUPDATE] = false; // spawn for client + spellTimer->flags[UPDATENEEDED] = true; + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + + spawnMagicEffectParticles(spellTimer->x, spellTimer->y, 7.5, 171); + } + } + + if ( found ) + { + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 799, 128); + } + else + { + playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6736)); + } + } + } + else if ( spell->ID == SPELL_EARTH_ELEMENTAL ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + int x = castSpellProps->target_x / 16; + int y = castSpellProps->target_y / 16; + bool noroom = false; + if ( x < 0 || x >= map.width || y < 0 || y >= map.height ) + { + noroom = true; + } + else + { + int mapIndex = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( map.tiles[OBSTACLELAYER + mapIndex] ) + { + noroom = true; + } + else if ( map.skybox != 0 ) + { + if ( !map.tiles[(MAPLAYERS - 1) + mapIndex] ) + { + noroom = true; + } + } + } + + if ( !noroom ) + { + found = true; + Entity* spellTimer = createParticleTimer(caster, 5 * TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->yaw = caster->yaw; + spellTimer->x = x * 16.0 + 8.0; + spellTimer->y = y * 16.0 + 8.0; + spellTimer->flags[NOUPDATE] = false; // spawn for client + spellTimer->flags[UPDATENEEDED] = true; + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + + spawnMagicEffectParticles(spellTimer->x, spellTimer->y, 7.5, 171); + + // kill old summons. + if ( caster->behavior == &actPlayer ) + { + for ( node = stats[caster->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + { + Entity* follower = nullptr; + if ( (Uint32*)(node)->element ) + { + follower = uidToEntity(*((Uint32*)(node)->element)); + } + if ( follower && follower->monsterAllySummonRank != 0 ) + { + Stat* followerStats = follower->getStats(); + if ( followerStats ) + { + follower->setMP(followerStats->MAXMP * (followerStats->HP / static_cast(followerStats->MAXHP))); + follower->setHP(0); + } + } + } + } + } + } + + if ( found ) + { + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 799, 128); + } + else + { + playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6736)); + } + } + } + else if ( spell->ID == SPELL_FIRE_WALL ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + + for ( int i = 0; i < 3; ++i ) + { + if ( i == 0 || i == 2 ) { continue; } + bool light = false; + if ( i == 1 ) + { + light = true; + } + + int duration = spell->life_time; + Entity* spellTimer = createParticleTimer(caster, duration, -1); + spellTimer->x = castSpellProps->target_x; + spellTimer->y = castSpellProps->target_y; + + real_t tangent = atan2(castSpellProps->target_y - castSpellProps->caster_y, + castSpellProps->target_x - castSpellProps->caster_x); + + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_FIRE_WAVE, + 1733, castSpellProps->target_x, castSpellProps->target_y, 2.75, + -PI / 2 + tangent - PI / 3 + i * PI / 3, + duration, light); + real_t grouping = 13.75; + wave->x -= grouping * cos(tangent); + wave->y -= grouping * sin(tangent); + real_t scale = 1.0; + wave->skill[1] = 6; // frames + wave->skill[5] = 4; // frame time + wave->ditheringOverride = 6; + wave->parent = spellTimer->getUID(); + real_t startScale = 0.1; + wave->scalex = startScale; + wave->scaley = startScale; + wave->scalez = startScale; + wave->focaly = startScale * grouping; + wave->fskill[0] = scale; // final scale + wave->fskill[1] = grouping; // final grouping + wave->skill[6] = 1; // grow to scale + if ( spellBookBonusPercent > 0 ) + { + wave->actmagicSpellbookBonus = spellBookBonusPercent; + } + wave->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + wave->flags[UPDATENEEDED] = true; + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + //playSoundEntity(caster, 164, 128); + } + } + else if ( spell->ID == SPELL_KINETIC_FIELD ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + + int duration = element->duration; + Entity* spellTimer = createParticleTimer(caster, duration, -1); + spellTimer->x = castSpellProps->target_x; + spellTimer->y = castSpellProps->target_y; + + real_t tangent = atan2(castSpellProps->target_y - castSpellProps->caster_y, + castSpellProps->target_x - castSpellProps->caster_x); + + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_KINETIC_FIELD, + 1739, castSpellProps->target_x, castSpellProps->target_y, 6.25, + PI / 2 + tangent, duration, true); + wave->skill[1] = 12; // frames + wave->skill[5] = 4; // frame time + wave->ditheringOverride = 6; + wave->sizex = 8; + wave->sizey = 8; + wave->parent = spellTimer->getUID(); + real_t startScale = 0.1; + real_t scale = 1.0; + wave->scalex = startScale; + wave->scaley = startScale; + wave->scalez = startScale; + wave->fskill[0] = scale; // final scale + wave->skill[6] = 1; // grow to scale + wave->flags[UPDATENEEDED] = true; + spawnMagicEffectParticles(wave->x, wave->y, wave->z, 171); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + } + } + else if ( spell->ID == SPELL_CHRONOMIC_FIELD ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + + int duration = element->duration; + Entity* spellTimer = createParticleTimer(caster, duration, -1); + spellTimer->x = castSpellProps->target_x; + spellTimer->y = castSpellProps->target_y; + + real_t tangent = atan2(castSpellProps->target_y - castSpellProps->caster_y, + castSpellProps->target_x - castSpellProps->caster_x); + + Entity* wave = createParticleWave(ParticleTimerEffect_t::EFFECT_CHRONOMIC_FIELD, + 1857, castSpellProps->target_x, castSpellProps->target_y, 5.25, + PI / 2 + tangent, duration, true); + wave->skill[1] = 8; // frames + wave->skill[5] = 4; // frame time + wave->ditheringOverride = 6; + wave->sizex = 8; + wave->sizey = 8; + wave->parent = spellTimer->getUID(); + real_t startScale = 0.1; + real_t scale = 1.0; + wave->scalex = startScale; + wave->scaley = startScale; + wave->scalez = startScale; + wave->fskill[0] = scale; // final scale + wave->skill[6] = 1; // grow to scale + wave->flags[UPDATENEEDED] = true; + spawnMagicEffectParticles(wave->x, wave->y, wave->z, 171); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + } + } + else if ( spell->ID == SPELL_ICE_WAVE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + + real_t tangent = atan2(castSpellProps->target_y - castSpellProps->caster_y, + castSpellProps->target_x - castSpellProps->caster_x); + + int lifetime = spell->life_time; + Entity* spellTimer = createParticleTimer(caster, lifetime + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_MAGIC_WAVE; + spellTimer->particleTimerCountdownSprite = 1718; + spellTimer->yaw = tangent; + spellTimer->x = castSpellProps->target_x; + spellTimer->y = castSpellProps->target_y; + if ( spellBookBonusPercent > 0 ) + { + spellTimer->actmagicSpellbookBonus = spellBookBonusPercent; + } + spellTimer->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + int lifetime_tick = 0; + auto& timerEffects = particleTimerEffects[spellTimer->getUID()]; + + static std::vector effLocations = { + {0.0, 0.0, 0.25, -10.0, 172}, + {PI / 16, 8.0, 0.0, 0.0, 172}, + {-PI / 16, -8.0, 0.0, 0.0, 0}, + {0.0, 0.0, 0.25, 0.0, 0}, + {PI / 16, 10.0, 0.0, 10.0, 0}, + {-PI / 16, -10.0, 0.0, 10.0, 172} + }; + + int index = -1; + while ( lifetime_tick <= lifetime ) + { + ++index; + auto& effect = timerEffects.effectMap[lifetime_tick == 0 ? 1 : lifetime_tick]; // first behavior tick only occurs at 1 + effect.effectType = ParticleTimerEffect_t::EffectType::EFFECT_ICE_WAVE; + auto& data = effLocations[index]; + effect.sfx = data.sfx; + effect.yaw = tangent + data.yawOffset; + effect.x = spellTimer->x + (data.dist) * cos(tangent); + effect.y = spellTimer->y + (data.dist) * sin(tangent); + effect.x += data.xOffset * cos(tangent + PI / 2); + effect.y += data.xOffset * sin(tangent + PI / 2); + lifetime_tick += std::max(1.0, TICKS_PER_SECOND * data.seconds); + if ( index + 1 >= effLocations.size() ) + { + break; + } + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + } + } + else if ( spell->ID == SPELL_DISRUPT_EARTH + || spell->ID == SPELL_EARTH_SPINES ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + + real_t tangent = atan2(castSpellProps->target_y - castSpellProps->caster_y, + castSpellProps->target_x - castSpellProps->caster_x); + + int lifetime = spell->life_time; + Entity* spellTimer = createParticleTimer(caster, lifetime + 10, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_MAGIC_WAVE; + spellTimer->particleTimerCountdownSprite = spell->ID == SPELL_DISRUPT_EARTH ? 1814 : 1815; + spellTimer->yaw = tangent; + spellTimer->x = castSpellProps->target_x; + spellTimer->y = castSpellProps->target_y; + spellTimer->particleTimerVariable3 = spell->ID; + if ( spellBookBonusPercent > 0 ) + { + spellTimer->actmagicSpellbookBonus = spellBookBonusPercent; + } + spellTimer->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + int lifetime_tick = 0; + auto& timerEffects = particleTimerEffects[spellTimer->getUID()]; + + std::vector effLocations; + int roll = local_rng.rand() % 8; + int numSprites = 8; + for ( int i = 0; i < numSprites; ++i ) + { + effLocations.push_back(ParticleTimerEffect_t::EffectLocations_t()); + auto& data = effLocations.back(); + if ( i == 0 ) + { + data.sfx = 799; + } + data.seconds = 1 / (real_t)numSprites; + data.dist = 0.25 + (0.75 * i / (real_t)numSprites); + real_t angle = (i / ((real_t)numSprites / 2)) * PI + ((roll) / ((real_t)numSprites / 2)) * PI; + data.xOffset = 8.0 * sin(angle); + data.xOffset += 2.0 * (local_rng.rand() % 16) / 16.0; + data.yawOffset = cos(angle) + ((local_rng.rand() % 4) / 4.0) * 2 * PI; + } + + int index = -1; + real_t dist = sqrt(pow(castSpellProps->target_x - castSpellProps->caster_x, 2) + + pow(castSpellProps->target_y - castSpellProps->caster_y, 2)); + dist = std::min(getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_DISTANCE, spell->ID, caster, nullptr, caster) + 16.0, dist + 16.0); + real_t minDist = 20.0; + while ( lifetime_tick <= lifetime ) + { + ++index; + auto& effect = timerEffects.effectMap[lifetime_tick == 0 ? 1 : lifetime_tick]; // first behavior tick only occurs at 1 + effect.effectType = ParticleTimerEffect_t::EffectType::EFFECT_DISRUPT_EARTH; + auto& data = effLocations[index]; + effect.sfx = data.sfx; + effect.yaw = tangent + data.yawOffset; + effect.x = castSpellProps->caster_x + std::max(minDist, dist * (data.dist)) * cos(tangent); + effect.y = castSpellProps->caster_y + std::max(minDist, dist * (data.dist)) * sin(tangent); + effect.x += data.xOffset * cos(tangent + PI / 2); + effect.y += data.xOffset * sin(tangent + PI / 2); + lifetime_tick += std::max(1.0, TICKS_PER_SECOND * data.seconds); + if ( index + 1 >= effLocations.size() ) + { + break; + } + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 799, 128); + } + } + else if ( spell->ID == SPELL_SLAM ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + found = true; + + real_t tangent = atan2(castSpellProps->target_y - castSpellProps->caster_y, + castSpellProps->target_x - castSpellProps->caster_x); + + int duration = getSpellEffectDurationFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + Entity* spellTimer = createParticleTimer(caster, duration, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_VORTEX; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->particleTimerVariable2 = spell->ID; + spellTimer->flags[UPDATENEEDED] = true; + spellTimer->flags[NOUPDATE] = false; + spellTimer->yaw = tangent; + spellTimer->x = castSpellProps->target_x; + spellTimer->y = castSpellProps->target_y; + if ( spellBookBonusPercent > 0 ) + { + spellTimer->actmagicSpellbookBonus = spellBookBonusPercent; + } + spellTimer->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + + if ( caster->behavior == &actMonster ) + { + spellTimer->x = castSpellProps->caster_x + 8.0 * cos(tangent); + spellTimer->y = castSpellProps->caster_y + 8.0 * sin(tangent); + spellTimer->vel_x = 3.0 * cos(spellTimer->yaw); + spellTimer->vel_y = 3.0 * sin(spellTimer->yaw); + } + spellTimer->particleTimerDuration = std::min(spellTimer->particleTimerDuration, 0xFFF); + Sint32 val = (1 << 31); + val |= (Uint8)(19); + val |= (((Uint16)(spellTimer->particleTimerDuration) & 0xFFF) << 8); + val |= (Uint8)(spellTimer->particleTimerCountdownAction & 0xFF) << 20; + spellTimer->skill[2] = val; + } + + //playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 1719); + } + } + else if ( spell->ID == SPELL_ROOTS ) + { + if ( caster ) + { + int damage = 0; // calculate later monsters + if ( caster->behavior == &actPlayer ) + { + damage = getSpellDamageFromID(SPELL_ROOTS, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + } + + if ( floorMagicCreateRoots(caster->x, caster->y, caster, damage, SPELL_ROOTS, 5 * TICKS_PER_SECOND, PARTICLE_TIMER_ACTION_ROOTS1) ) + { + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + } + else + { + // no room + playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6748)); + } + } + } + else if ( spell->ID == SPELL_BASTION_MUSHROOM ) + { + if ( caster && caster->behavior == &actPlayer ) + { + int duration = element->duration; + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + if ( caster->setEffect(EFF_BASTION_MUSHROOM, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6799)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + + Entity* spellTimer = createParticleTimer(caster, 3 * TICKS_PER_SECOND, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_BASTION_MUSHROOM; + spellTimer->particleTimerCountdownSprite = -1; + spellTimer->yaw = caster->yaw; + spellTimer->x = caster->x; + spellTimer->y = caster->y; + spellTimer->flags[NOUPDATE] = true; + spellTimer->flags[UPDATENEEDED] = false; + + spellTimer->particleTimerVariable3 = SPELL_SPORES; + if ( castSpellProps ) + { + if ( castSpellProps->optionalData == 2 ) + { + spellTimer->particleTimerVariable3 = SPELL_SPORE_BOMB; + } + } + } + } + } + else if ( spell->ID == SPELL_BASTION_ROOTS ) + { + if ( caster && caster->behavior == &actPlayer ) + { + int damage = 0; + if ( caster->behavior == &actPlayer ) + { + damage = getSpellDamageFromID(SPELL_BASTION_ROOTS, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + if ( castSpellProps ) + { + if ( castSpellProps->optionalData == 1 ) + { + damage = getSpellDamageFromID(SPELL_THORNS, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + } + else if ( castSpellProps->optionalData == 2 ) + { + damage = getSpellDamageFromID(SPELL_BLADEVINES, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + } + } + } + + if ( Entity* spellTimer = floorMagicCreateRoots(caster->x, caster->y, caster, damage, SPELL_BASTION_ROOTS, 5 * TICKS_PER_SECOND, PARTICLE_TIMER_ACTION_ROOTS_SUSTAIN) ) + { + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + + spellTimer->particleTimerVariable3 = SPELL_THORNS; + if ( castSpellProps->optionalData == 2 ) + { + spellTimer->particleTimerVariable3 = SPELL_BLADEVINES; + } + + int duration = element->duration; + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + if ( caster->setEffect(EFF_BASTION_ROOTS, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6800)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + else + { + // no room + playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6748)); + } + } + } + else if ( spell->ID == SPELL_MUSHROOM ) + { + if ( caster ) + { + if ( castSpellProps ) + { + bool hasGrowth = true; + if ( caster->behavior == &actPlayer ) + { + if ( Stat* casterStats = caster->getStats() ) + { + if ( casterStats->getEffectActive(EFF_GROWTH) <= (Uint8)1 ) + { + hasGrowth = false; + playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6794)); + } + } + } + + if ( hasGrowth ) + { + if ( Entity* breakable = Entity::createBreakableCollider(EditorEntityData_t::getColliderIndexFromName("mushroom_spell_casted"), + castSpellProps->target_x, castSpellProps->target_y, caster) ) + { + spawnPoof(breakable->x, breakable->y, 7.5, 1.0, true); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + + if ( castSpellProps->optionalData == 2 ) + { + breakable->colliderSpellEvent = 1007; + } + else + { + breakable->colliderSpellEvent = 1006; + } + breakable->colliderSetServerSkillOnSpawned(); // to update the variables modified from create() + + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6885)); + + if ( caster->behavior == &actPlayer ) + { + steamStatisticUpdateClient(caster->skill[2], STEAM_STAT_COLONIST, STEAM_STAT_INT, 1); + if ( Stat* casterStats = caster->getStats() ) + { + if ( Uint8 effectStrength = casterStats->getEffectActive(EFF_GROWTH) ) + { + breakable->colliderDropVariable = std::max(1, effectStrength - 1); + casterStats->setEffectValueUnsafe(EFF_GROWTH, effectStrength - 1); + serverUpdateEffects(caster->isEntityPlayer()); + } + } + } + } + else + { + // no room + playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6750)); + } + } + } + } + } + else if ( spell->ID == SPELL_SHRUB ) + { + if ( caster ) + { + if ( castSpellProps ) + { + bool hasGrowth = true; + if ( caster->behavior == &actPlayer ) + { + if ( Stat* casterStats = caster->getStats() ) + { + if ( casterStats->getEffectActive(EFF_GROWTH) <= (Uint8)1 ) + { + hasGrowth = false; + playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6794)); + } + } + } + + if ( hasGrowth ) + { + if ( Entity* breakable = Entity::createBreakableCollider(EditorEntityData_t::getColliderIndexFromName("germinate_spell_casted"), + castSpellProps->target_x, castSpellProps->target_y, caster) ) + { + spawnPoof(breakable->x, breakable->y, 7.5, 1.0, true); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + + if ( castSpellProps->optionalData == 2 ) + { + breakable->colliderSpellEvent = 1009; + breakable->colliderMaxHP *= 2; + breakable->colliderCurrentHP = breakable->colliderMaxHP; + breakable->colliderOldHP = breakable->colliderMaxHP; + } + else + { + breakable->colliderSpellEvent = 1008; + } + breakable->colliderSetServerSkillOnSpawned(); // to update the variables modified from create() + + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6884)); + + if ( caster->behavior == &actPlayer ) + { + steamStatisticUpdateClient(caster->skill[2], STEAM_STAT_COLONIST, STEAM_STAT_INT, 1); + if ( Stat* casterStats = caster->getStats() ) + { + if ( Uint8 effectStrength = casterStats->getEffectActive(EFF_GROWTH) ) + { + breakable->colliderDropVariable = std::max(1, effectStrength - 1); + casterStats->setEffectValueUnsafe(EFF_GROWTH, effectStrength - 1); + serverUpdateEffects(caster->isEntityPlayer()); + if ( caster->flags[BURNING] ) + { + caster->flags[BURNING] = false; + serverUpdateEntityFlag(caster, BURNING); + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6883)); + steamAchievementClient(caster->isEntityPlayer(), "BARONY_ACH_MAKE_SMOKEY_PROUD"); + } + } + } + } + } + else + { + // no room + playSoundEntity(caster, 163, 128); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6750)); + } + } + } + } + } + else if ( spell->ID == SPELL_VOID_CHEST ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( target->behavior == &actChest ) + { + found = true; + if ( target->chestStatus == 1 ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(255, 255, 255), Language::get(6558)); + } + else + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6557)); + if ( target->chestVoidState == 0 ) + { + magicOnEntityHit(caster, caster, target, nullptr, 0, 0, 0, spell ? spell->ID : SPELL_NONE, usingSpellbook ? SPELLBOOK_VOID_CHEST : 0); + } + target->chestVoidState = TICKS_PER_SECOND * 5; + serverUpdateEntitySkill(target, 17); + createParticleErupt(target, 625); + serverSpawnMiscParticles(target, PARTICLE_EFFECT_ERUPT, 625); + + } + } + else if ( target->behavior == &actMonster && target->getStats() && target->getStats()->type == MIMIC ) + { + found = true; + bool prevEffect = target->getStats()->getEffectActive(EFF_MIMIC_VOID); + target->setEffect(EFF_MIMIC_VOID, true, TICKS_PER_SECOND * 30, false); + if ( target->isInertMimic() ) + { + target->disturbMimic(caster, false, true); + } + else + { + if ( !uidToEntity(target->monsterTarget) ) + { + target->monsterAcquireAttackTarget(*caster, MONSTER_STATE_PATH, true); + } + } + createParticleErupt(target, 625); + serverSpawnMiscParticles(target, PARTICLE_EFFECT_ERUPT, 625); + + if ( !prevEffect ) + { + magicOnEntityHit(caster, caster, target, target->getStats(), 0, 0, 0, spell ? spell->ID : SPELL_NONE, usingSpellbook ? SPELLBOOK_VOID_CHEST : 0); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_COMMAND ) + { + if ( caster && caster->behavior == &actPlayer && caster->getStats() ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + spawnMagicEffectParticles(target->x, target->y, target->z, 2341); + + if ( Stat* targetStats = target->getStats() ) + { + int player = caster->skill[2]; + int duration = element->duration; + Uint8 previousEffect = targetStats->getEffectActive(EFF_COMMAND); + + int difficulty = getCharmMonsterDifficulty(*target, *targetStats); + int casterAbility = caster->getCHR() + + std::max(caster->getStats()->getModifiedProficiency(PRO_MYSTICISM), caster->getStats()->getModifiedProficiency(PRO_LEADERSHIP)); + if ( target->behavior == &actPlayer ) + { + difficulty = 666; + } + bool refusedEffect = false; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && players[i]->entity ) + { + if ( players[i]->mechanics.targetsRefuseCompel.find(target->getUID()) + != players[i]->mechanics.targetsRefuseCompel.end() ) + { + refusedEffect = true; + break; + } + } + } + + if ( casterAbility >= ((difficulty * 15) + 60) && !refusedEffect ) + { + if ( target->setEffect(EFF_COMMAND, (Uint8)(player + 1), duration, true, true, true) ) + { + playSoundEntity(caster, 167, 128); + effect = true; + + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6537), Language::get(6902), MSG_COMBAT); + + if ( !previousEffect ) + { + magicOnEntityHit(caster, caster, target, targetStats, 0, 0, 0, spell ? spell->ID : SPELL_NONE, usingSpellbook ? SPELLBOOK_COMMAND : 0); + } + + //createParticleSpellPinpointTarget(target, caster->getUID(), 1774, duration, spell->ID); + //serverSpawnMiscParticles(target, PARTICLE_EFFECT_PINPOINT, 1774, caster->getUID(), duration, spell->ID); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + target->x, target->y, 16, duration, target) ) + { + } + if ( players[player]->isLocalPlayer() ) + { + FollowerMenu[player].followerToCommand = target; + FollowerMenu[player].initfollowerMenuGUICursor(true); // set gui_mode to follower menu + } + else + { + if ( player >= 1 && player < MAXPLAYERS ) + { + strcpy((char*)net_packet->data, "COMD"); + SDLNet_Write32(target->getUID(), &net_packet->data[4]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + } + target->monsterReleaseAttackTarget(); + for ( node_t* node = map.creatures->first; node != nullptr; node = node->next ) + { + Entity* entity2 = (Entity*)node->element; + if ( !entity2 ) { continue; } + if ( entity2->behavior == &actMonster && entity2 != target ) + { + if ( entity2->monsterAllyGetPlayerLeader() && ((Uint32)entity2->monsterTarget == target->getUID()) ) + { + entity2->monsterReleaseAttackTarget(); // player allies stop attacking this target + } + } + } + } + found = true; + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + } + } + else + { + found = true; + if ( difficulty >= 666 ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + + applyGenericMagicDamage(caster, target, *caster, SPELL_COMMAND, 0, true, true); // alert the target + } + else if ( refusedEffect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(6968), Language::get(6969), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + + applyGenericMagicDamage(caster, target, *caster, SPELL_COMMAND, 0, true, true); // alert the target + } + else + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(6966), Language::get(6967), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + + applyGenericMagicDamage(caster, target, *caster, SPELL_COMMAND, 0, true, true); // alert the target + } + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2341); + //playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_CURSE_FLESH || spell->ID == SPELL_REVENANT_CURSE ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( Stat* targetStats = target->getStats() ) + { + int duration = element->duration; + Uint8 effectStrength = (Uint8)(caster->behavior != &actPlayer ? MAXPLAYERS + 1 : caster->skill[2] + 1); + if ( spell->ID == SPELL_REVENANT_CURSE ) + { + effectStrength |= (1 << 7); + } + if ( target->setEffect(EFF_CURSE_FLESH, effectStrength, duration, false, true, true) ) + { + playSoundEntity(caster, 167, 128); + effect = true; + + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6576), Language::get(6904), MSG_COMBAT); + + spawnMagicEffectParticles(target->x, target->y, target->z, 2353); + + if ( Entity* fx = createParticleAestheticOrbit(target, 2353, 2.5 * TICKS_PER_SECOND, PARTICLE_EFFECT_REVENANT_CURSE) ) + { + fx->z = 7.5; + fx->yaw = target->yaw; + fx->ditheringOverride = 6; + } + + applyGenericMagicDamage(caster, target, *caster, spell->ID, 0, true, true); // alert the target + if ( !using_magicstaff && !trap && caster->behavior == &actPlayer ) + { + if ( usingSpellbook && spell->ID == SPELL_CURSE_FLESH ) + { + Compendium_t::Events_t::eventUpdate(caster->skill[2], Compendium_t::CPDM_SPELL_TARGETS, SPELLBOOK_CURSE_FLESH, 1); + } + else + { + Compendium_t::Events_t::eventUpdate(caster->skill[2], Compendium_t::CPDM_SPELL_TARGETS, SPELL_ITEM, 1, false, spell->ID); + } + } + + serverSpawnMiscParticles(target, PARTICLE_EFFECT_REVENANT_CURSE, 2353, 0, 2.5 * TICKS_PER_SECOND); + } + found = true; + + + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + playSoundEntity(caster, 163, 128); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2353); + //playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_WINDGATE ) + { + if ( caster ) + { + bool found = false; + if ( castSpellProps && castSpellProps->wallDir >= 1 ) + { + found = true; + + int duration = element->duration; + int maxLen = getSpellEffectDurationSecondaryFromID(SPELL_WINDGATE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + int length = std::max(2, std::min(std::min(0xF, maxLen), getSpellDamageFromID(SPELL_WINDGATE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0))); + createWindMagic(caster->getUID(), castSpellProps->target_x, castSpellProps->target_y, duration, castSpellProps->wallDir, length); + Uint32 data = (castSpellProps->wallDir) & 0xF; + data |= ((length) & 0xF) << 4; + serverSpawnMiscParticlesAtLocation(castSpellProps->target_x, castSpellProps->target_y, 0, + PARTICLE_EFFECT_WINDGATE, 982, duration, data); + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_TUNNEL ) + { + if ( caster ) + { + bool found = false; + if ( castSpellProps && castSpellProps->wallDir >= 1 ) + { + found = true; + + int duration = element->duration; + Entity* result = createTunnelPortal(castSpellProps->target_x, castSpellProps->target_y, duration, castSpellProps->wallDir, caster); + if ( !result ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6814)); + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_SABOTAGE || spell->ID == SPELL_HARVEST_TRAP ) + { + if ( caster ) + { + bool found = false; + int scrapMetal = 0; + int scrapMagic = 0; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( castSpellProps->wallDir > 0 && (target->behavior == &actArrowTrap || target->behavior == &actMagicTrap) ) + { + real_t x = static_cast(target->x / 16); + real_t y = static_cast(target->y / 16); + found = true; + if ( castSpellProps->wallDir == 1 ) + { + x = x * 16.0 + 16.001 + 2.0; + y = y * 16.0 + 8.0; + } + else if ( castSpellProps->wallDir == 3 ) + { + x = x * 16.0 - 0.001 - 2.0; + y = y * 16.0 + 8.0; + } + else if ( castSpellProps->wallDir == 2 ) + { + x = x * 16.0 + 8.0; + y = y * 16.0 + 16.001 + 2.0; + } + else if ( castSpellProps->wallDir == 4 ) + { + x = x * 16.0 + 8.0; + y = y * 16.0 - 0.001 - 2.0; + } + + if ( target->actTrapSabotaged == 0 ) + { + spawnExplosion(x, y, target->z); + target->actTrapSabotaged = (caster->isEntityPlayer() >= 0) ? caster->skill[2] + 1 : MAXPLAYERS + 1; + serverUpdateEntitySkill(target, 30); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6659)); + if ( spell->ID == SPELL_HARVEST_TRAP ) + { + scrapMetal = std::max(1, getSpellDamageFromID(SPELL_HARVEST_TRAP, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0)); + scrapMagic = std::max(1, getSpellDamageSecondaryFromID(SPELL_HARVEST_TRAP, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0)); + } + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + + Entity* spellTimer = createParticleTimer(nullptr, TICKS_PER_SECOND, -1); + spellTimer->x = x; + spellTimer->y = y; + spellTimer->z = 0.0; + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_TRAP_SABOTAGED; + serverSpawnMiscParticlesAtLocation(spellTimer->x, spellTimer->y, spellTimer->z, PARTICLE_EFFECT_SABOTAGE_TRAP, 0); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6658)); + } + } + else if ( target->behavior == &actBoulderTrapHole + || target->behavior == &actMagicTrapCeiling + || target->behavior == &actSpearTrap ) + { + if ( target->behavior == &actBoulderTrapHole ) + { + if ( Entity* parent = uidToEntity(target->parent) ) + { + found = true; + if ( parent->actTrapSabotaged == 0 ) + { + spawnExplosion(target->x, target->y, target->z); + parent->actTrapSabotaged = (caster->isEntityPlayer() >= 0) ? caster->skill[2] + 1 : MAXPLAYERS + 1; + serverUpdateEntitySkill(parent, 30); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6659)); + if ( spell->ID == SPELL_HARVEST_TRAP ) + { + scrapMetal = std::max(1, getSpellDamageFromID(SPELL_HARVEST_TRAP, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0)); + scrapMagic = std::max(1, getSpellDamageSecondaryFromID(SPELL_HARVEST_TRAP, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0)); + } + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + + Entity* spellTimer = createParticleTimer(nullptr, TICKS_PER_SECOND, -1); + spellTimer->x = target->x; + spellTimer->y = target->y; + spellTimer->z = target->z; + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_TRAP_SABOTAGED; + serverSpawnMiscParticlesAtLocation(spellTimer->x, spellTimer->y, spellTimer->z, PARTICLE_EFFECT_SABOTAGE_TRAP, 0); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6658)); + } + } + } + else + { + found = true; + if ( target->actTrapSabotaged == 0 ) + { + if ( target->behavior == &actSpearTrap ) + { + spawnExplosion(target->x, target->y, 7.5); + } + else if ( target->behavior == &actMagicTrapCeiling ) + { + real_t z = -7.0; + int x = target->x / 16; + int y = target->y / 16; + if ( x >= 0 && y >= 0 && x < map.width && y < map.height ) + { + if ( !map.tiles[(MAPLAYERS - 1) + y * MAPLAYERS + x * MAPLAYERS * map.height] ) + { + z = -23; + } + } + spawnExplosion(target->x, target->y, z); + } + else + { + spawnExplosion(target->x, target->y, target->z); + } + target->actTrapSabotaged = (caster->isEntityPlayer() >= 0) ? caster->skill[2] + 1 : MAXPLAYERS + 1; + serverUpdateEntitySkill(target, 30); + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6659)); + if ( spell->ID == SPELL_HARVEST_TRAP ) + { + scrapMetal = std::max(1, getSpellDamageFromID(SPELL_HARVEST_TRAP, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0)); + scrapMagic = std::max(1, getSpellDamageSecondaryFromID(SPELL_HARVEST_TRAP, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0)); + } + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + + Entity* spellTimer = createParticleTimer(nullptr, TICKS_PER_SECOND, -1); + spellTimer->x = target->x; + spellTimer->y = target->y; + spellTimer->z = target->z; + if ( target->behavior == &actSpearTrap ) + { + spellTimer->z = 5.0; + } + else if ( target->behavior == &actMagicTrapCeiling ) + { + spellTimer->z = -7.0; + int x = target->x / 16; + int y = target->y / 16; + if ( x >= 0 && y >= 0 && x < map.width && y < map.height ) + { + if ( !map.tiles[(MAPLAYERS - 1) + y * MAPLAYERS + x * MAPLAYERS * map.height] ) + { + spellTimer->z = -23; + } + } + } + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_TRAP_SABOTAGED; + serverSpawnMiscParticlesAtLocation(spellTimer->x, spellTimer->y, spellTimer->z, PARTICLE_EFFECT_SABOTAGE_TRAP, 0); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6658)); + } + } + } + else if ( spell->ID == SPELL_SABOTAGE && (target->behavior == &actMonster || target->behavior == &actChest) ) + { + found = true; + bool effect = false; + int damage = getSpellDamageFromID(SPELL_SABOTAGE, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + if ( target->behavior == &actChest ) + { + if ( effect = applyGenericMagicDamage(caster, target, *caster, spell->ID, damage, true) ) + { + spawnExplosion(target->x, target->y, 0.0); + } + } + else if ( Stat* targetStats = target->getStats() ) + { + if ( targetStats->type == CRYSTALGOLEM + || targetStats->type == AUTOMATON + || targetStats->type == MINIMIMIC + || targetStats->type == MIMIC + || monsterIsImmobileTurret(target, targetStats) + ) + { + Sint32 oldHP = targetStats->HP; + if ( effect = applyGenericMagicDamage(caster, target, *caster, spell->ID, damage, true, false, usingSpellbook ? SPELLBOOK_SABOTAGE : 0) ) + { + spawnExplosion(target->x, target->y, 0.0); + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6958), Language::get(6959), MSG_COMBAT); + } + } + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + } + } + } + + if ( spell->ID == SPELL_HARVEST_TRAP ) + { + bool anyDrop = false; + while ( scrapMetal > 0 || scrapMagic > 0 ) + { + Item* item = nullptr; + int qty = 3 + local_rng.rand() % 5; + if ( scrapMetal > 0 ) + { + qty = std::min(scrapMetal, qty); + scrapMetal -= qty; + item = newItem(TOOL_METAL_SCRAP, DECREPIT, 0, qty, 0, true, nullptr); + } + else if ( scrapMagic > 0 ) + { + qty = std::min(scrapMagic, qty); + scrapMagic -= qty; + item = newItem(TOOL_MAGIC_SCRAP, DECREPIT, 0, qty, 0, true, nullptr); + } + if ( item ) + { + if ( Entity* dropped = dropItemMonster(item, target, nullptr, item->count) ) + { + anyDrop = true; + dropped->yaw = local_rng.rand() % 360 * PI / 180; + dropped->vel_x = (0.5 + .005 * (local_rng.rand() % 11)) * cos(dropped->yaw); + dropped->vel_y = (0.5 + .005 * (local_rng.rand() % 11)) * sin(dropped->yaw); + if ( target->behavior == &actMagicTrap + || target->behavior == &actArrowTrap ) + { + real_t x = static_cast(target->x / 16); + real_t y = static_cast(target->y / 16); + if ( castSpellProps->wallDir == 1 ) + { + x = x * 16.0 + 16.001 + 2.0; + y = y * 16.0 + 8.0; + } + else if ( castSpellProps->wallDir == 3 ) + { + x = x * 16.0 - 0.001 - 2.0; + y = y * 16.0 + 8.0; + } + else if ( castSpellProps->wallDir == 2 ) + { + x = x * 16.0 + 8.0; + y = y * 16.0 + 16.001 + 2.0; + } + else if ( castSpellProps->wallDir == 4 ) + { + x = x * 16.0 + 8.0; + y = y * 16.0 - 0.001 - 2.0; + } + dropped->x = x; + dropped->y = y; + } + dropped->vel_z = (-10 - local_rng.rand() % 20) * .01; + dropped->z = target->z; + dropped->z = std::min(5.5, dropped->z); + } + } + } + + if ( anyDrop ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6662)); + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_DEFACE ) + { + if ( caster ) + { + bool found = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + struct DefaceEvent_t + { + Monster type = NOTHING; + Item* item = nullptr; + int weight = 1; + DefaceEvent_t(Monster _type, Item* _item, int _weight) { + item = _item; + type = _type; + weight = _weight; + } + ~DefaceEvent_t() + { + if ( item ) + { + free(item); + } + } + }; + std::vector itemPool; + auto& rng = target->entity_rng ? *target->entity_rng : local_rng; + if ( target->behavior == &actSink ) + { + found = true; + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, + makeColorRGB(0, 255, 0), + Language::get(6688), Language::get(4354)); + + itemPool.reserve(8); + itemPool.emplace_back( + NOTHING, + newItem(TOOL_METAL_SCRAP, DECREPIT, 0, 5 + rng.rand() % 11, 0, false, nullptr), + 8); + + itemPool.emplace_back( + NOTHING, + newItem(TOOL_MAGIC_SCRAP, DECREPIT, 0, 5 + rng.rand() % 11, 0, false, nullptr), + 8); + + itemPool.emplace_back( + NOTHING, + newItem(POTION_SICKNESS, DECREPIT, 0, 2 + rng.rand() % 4, rng.rand(), false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(GREASE_BALL, WORN, 0, 4 + rng.rand() % 5, 0, false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(SLOP_BALL, WORN, 0, 4 + rng.rand() % 5, 0, false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(GEM_OBSIDIAN, DECREPIT, 0, 1, 0, false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(TOOL_MIRROR, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 4); + + itemPool.emplace_back( + SLIME, + nullptr, + 10); + + playSoundEntity(target, 67, 128); + } + else if ( target->behavior == &actMailbox ) + { + found = true; + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, + makeColorRGB(0, 255, 0), + Language::get(6688), Language::get(6986)); + + playSoundEntity(target, 67, 128); + } + else if ( target->behavior == &actCauldron ) + { + found = true; + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, + makeColorRGB(0, 255, 0), + Language::get(6688), Language::get(6974)); + + itemPool.reserve(8); + itemPool.emplace_back( + NOTHING, + newItem(GREASE_BALL, WORN, 0, 4 + rng.rand() % 5, rng.rand(), false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(SLOP_BALL, WORN, 0, 4 + rng.rand() % 5, rng.rand(), false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(FOOD_RATION, DECREPIT, -1, 2 + rng.rand() % 5, 0, false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(TOOL_FRYING_PAN, DECREPIT, -1 + rng.rand() % 3, 2 + rng.rand() % 3, rng.rand(), false, nullptr), + 1); + + itemPool.emplace_back( + NOTHING, + newItem(GEM_LUCK, DECREPIT, 0, 1, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(TOOL_TORCH, WORN, 0, 2 + rng.rand() % 3, rng.rand(), false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(WOODEN_SHIELD, WORN, 0, 1, rng.rand(), false, nullptr), + 10); + + ItemType potionType = static_cast(POTION_WATER + rng.rand() % 15); + int potionAppearance = 0; + for ( size_t p = 0; p < potionStandardAppearanceMap.size(); ++p ) + { + if ( potionStandardAppearanceMap[p].first == potionType ) + { + potionAppearance = potionStandardAppearanceMap[p].second; + } + } + + itemPool.emplace_back( + NOTHING, + newItem(static_cast(POTION_WATER + rng.rand() % 15), SERVICABLE, + -1 + rng.rand() % 3, 1 + rng.rand() % 4, potionAppearance, false, nullptr), + 10); + + playSoundEntity(target, 67, 128); + } + else if ( target->behavior == &actWorkbench ) + { + found = true; + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, + makeColorRGB(0, 255, 0), + Language::get(6688), Language::get(6981)); + + itemPool.reserve(8); + itemPool.emplace_back( + NOTHING, + newItem(TOOL_METAL_SCRAP, DECREPIT, 0, 10 + rng.rand() % 31, 0, false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(TOOL_MAGIC_SCRAP, DECREPIT, 0, 10 + rng.rand() % 21, 0, false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(TOOL_LOCKPICK, EXCELLENT, 0, 1 + rng.rand() % 3, 0, false, nullptr), + 8); + + itemPool.emplace_back( + NOTHING, + newItem(TOOL_SKELETONKEY, DECREPIT, 0, 1, 0, false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(TOOL_DECOY, SERVICABLE, 0, 1 + rng.rand() % 2, 0, false, nullptr), + 4); + + itemPool.emplace_back( + NOTHING, + newItem(QUIVER_KNOCKBACK, SERVICABLE, 0, 6 + rng.rand() % 7, 0, false, nullptr), + 4); + + itemPool.emplace_back( + NOTHING, + newItem(SCROLL_BLANK, SERVICABLE, 0, 1, 0, false, nullptr), + 4); + + itemPool.emplace_back( + NOTHING, + newItem(SCROLL_REPAIR, SERVICABLE, 0, 1, 0, false, nullptr), + 1); + + playSoundEntity(target, 67, 128); + } + else if ( target->behavior == &actHeadstone ) + { + found = true; + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, + makeColorRGB(0, 255, 0), + Language::get(6688), Language::get(4357)); + + itemPool.reserve(17); + itemPool.emplace_back( + NOTHING, + newItem(GEM_ROCK, DECREPIT, -1, 2 + rng.rand() % 4, rng.rand(), false, nullptr), + 10); + + itemPool.emplace_back( + NOTHING, + newItem(GEM_LUCK, DECREPIT, -1, 2 + rng.rand() % 4, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(FOOD_MEAT, DECREPIT, -1, 1 + rng.rand() % 2, rng.rand(), false, nullptr), + 10); + + { + itemPool.emplace_back( + NOTHING, + newItem(TUNIC, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(SUEDE_BOOTS, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(SUEDE_GLOVES, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(SHAWL, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(ROBE_CULTIST, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(ROBE_HEALER, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(ROBE_MONK, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 2); + + itemPool.emplace_back( + NOTHING, + newItem(ROBE_WIZARD, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 2); + } + + itemPool.emplace_back( + NOTHING, + newItem(RING_ADORNMENT, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 4); + + itemPool.emplace_back( + NOTHING, + newItem(MASK_SPOOKY, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 4); + + itemPool.emplace_back( + NOTHING, + newItem(SCROLL_REMOVECURSE, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 4); + + itemPool.emplace_back( + NOTHING, + newItem(SCROLL_TELEPORTATION, DECREPIT, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr), + 4); + + itemPool.emplace_back( + SHADOW, + nullptr, + 10); + + itemPool.emplace_back( + GHOUL, + nullptr, + 10); + + playSoundEntity(target, 67, 128); + } + + if ( found ) + { + target->flags[PASSABLE] = true; + if ( itemPool.size() ) + { + std::vector weights(itemPool.size()); + int index = -1; + for ( auto& entry : itemPool ) + { + ++index; + weights[index] = entry.weight; + } + int pick = rng.discrete(weights.data(), weights.size()); + + if ( itemPool[pick].item ) + { + if ( Entity* dropped = dropItemMonster(itemPool[pick].item, target, nullptr, itemPool[pick].item->count) ) + { + dropped->yaw = local_rng.rand() % 360 * PI / 180; + dropped->vel_x = (0.5 + .005 * (local_rng.rand() % 11)) * cos(dropped->yaw); + dropped->vel_y = (0.5 + .005 * (local_rng.rand() % 11)) * sin(dropped->yaw); + dropped->vel_z = (-10 - local_rng.rand() % 20) * .02; + dropped->z = target->z; + dropped->z = std::min(5.5, dropped->z); + + itemPool[pick].item = nullptr; + } + } + else if ( itemPool[pick].type != NOTHING ) + { + Monster toSpawn = itemPool[pick].type; + int numSpawns = 1; + if ( toSpawn == GHOUL ) + { + if ( rng.rand() % 2 == 0 ) + { + toSpawn = BAT_SMALL; + numSpawns = 2 + rng.rand() % 3; + } + } + + for ( int i = 0; i < numSpawns; ++i ) + { + if ( Entity* monster = summonMonster(toSpawn, target->x, target->y) ) + { + monster->seedEntityRNG(rng.rand()); + monster->monsterAcquireAttackTarget(*caster, MONSTER_STATE_PATH, true); + if ( toSpawn == SHADOW ) + { + if ( Stat* monsterStats = monster->getStats() ) + { + if ( caster->behavior == &actPlayer ) + { + monsterStats->setAttribute("deface_spawn", std::to_string(caster->skill[2])); + } + } + } + if ( toSpawn == SLIME ) + { + slimeSetType(monster, monster->getStats(), true, &rng); + } + } + } + + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, + makeColorRGB(255, 0, 0), + Language::get(6689), getMonsterLocalizedName(toSpawn).c_str()); + } + } + + magicOnSpellCastEvent(caster, caster, target, spell->ID, spellEventFlags | spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + spawnMagicEffectParticles(target->x, target->y, target->z, 171); + createParticleRock(target, 78); + playSoundEntity(target, 167, 128); + + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent::GBREAK_DEFACE, target); + serverUpdatePlayerGameplayStats(caster->skill[2], STATISTICS_WRECKING_CREW, 1); + } + + if ( multiplayer == SERVER ) + { + serverSpawnMiscParticles(target, PARTICLE_EFFECT_ABILITY_ROCK, 78); + } + list_RemoveNode(target->mynode); + } + itemPool.clear(); + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_KINETIC_PUSH ) + { + if ( caster && (caster->behavior == &actPlayer || caster->behavior == &actMonster) ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + found = true; + if ( target->behavior == &actBoulder && caster->behavior == &actPlayer ) + { + target->skill[15] = player + 1; + } + else if ( Stat* targetStats = target->getStats() ) + { + if ( targetStats->type == EARTH_ELEMENTAL ) + { + if ( target->setEffect(EFF_KNOCKBACK, true, 40, false) ) + { + target->setEffect(EFF_DASH, true, 40, false); + target->setEffect(EFF_STUNNED, true, 40, false); + target->attack(MONSTER_POSE_EARTH_ELEMENTAL_ROLL, 0, nullptr); + effect = true; + real_t pushbackMultiplier = 2.0; + + real_t tangent = atan2(target->y - caster->y, target->x - caster->x); + target->vel_x = cos(tangent) * pushbackMultiplier; + target->vel_y = sin(tangent) * pushbackMultiplier; + target->monsterKnockbackVelocity = 0.005; + target->monsterKnockbackTangentDir = tangent; + target->monsterKnockbackUID = caster->getUID(); + } + } + else if ( target->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + effect = true; + real_t pushbackMultiplier = 0.6; + if ( caster ) + { + real_t dist = entityDist(caster, target); + if ( dist < TOUCHRANGE ) + { + pushbackMultiplier += 0.5; + } + } + if ( target->behavior == &actMonster ) + { + real_t tangent = atan2(target->y - caster->y, target->x - caster->x); + target->vel_x = cos(tangent) * pushbackMultiplier; + target->vel_y = sin(tangent) * pushbackMultiplier; + target->monsterKnockbackVelocity = 0.01; + target->monsterKnockbackTangentDir = tangent; + target->monsterKnockbackUID = caster->getUID(); + } + else if ( target->behavior == &actPlayer ) + { + if ( !players[target->skill[2]]->isLocalPlayer() ) + { + target->monsterKnockbackVelocity = pushbackMultiplier; + target->monsterKnockbackTangentDir = caster->yaw; + serverUpdateEntityFSkill(target, 11); + serverUpdateEntityFSkill(target, 9); + } + else + { + target->monsterKnockbackVelocity = pushbackMultiplier; + target->monsterKnockbackTangentDir = caster->yaw; + } + } + } + + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + } + } + spawnMagicEffectParticles(target->x, target->y, target->z, 171); + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_TELEKINESIS ) + { + if ( caster ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( target->behavior == &actBoulder && caster->behavior == &actPlayer ) + { + found = true; + target->skill[14] = player + 1; + + magicOnSpellCastEvent(caster, caster, target, spell->ID, spellEventFlags | spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + else if ( target->behavior == &actMonster && target->getMonsterTypeFromSprite() == EARTH_ELEMENTAL ) + { + found = true; + if ( target->setEffect(EFF_KNOCKBACK, true, 40, false) ) + { + target->setEffect(EFF_DASH, true, 40, false); + target->setEffect(EFF_STUNNED, true, 40, false); + target->attack(MONSTER_POSE_EARTH_ELEMENTAL_ROLL, 0, nullptr); + effect = true; + real_t pushbackMultiplier = 2.0; + /*if ( caster ) + { + real_t dist = entityDist(caster, target); + if ( dist < TOUCHRANGE ) + { + pushbackMultiplier += 0.5; + } + }*/ + real_t tangent = atan2(target->y - caster->y, target->x - caster->x) + PI; + target->vel_x = cos(tangent) * pushbackMultiplier; + target->vel_y = sin(tangent) * pushbackMultiplier; + target->monsterKnockbackVelocity = 0.005; + target->monsterKnockbackTangentDir = tangent; + target->monsterKnockbackUID = caster->getUID(); + + magicOnSpellCastEvent(caster, caster, target, spell->ID, spellEventFlags | spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + } + else if ( caster->behavior == &actMonster + && (target->behavior == &actMonster || target->behavior == &actPlayer) ) + { + found = true; + if ( target->setEffect(EFF_KNOCKBACK, true, 30, false) ) + { + real_t pushbackMultiplier = 1.2; + real_t tangent = atan2(target->y - caster->y, target->x - caster->x) + PI; + + if ( target->behavior == &actMonster ) + { + target->vel_x = cos(tangent) * pushbackMultiplier; + target->vel_y = sin(tangent) * pushbackMultiplier; + target->monsterKnockbackVelocity = 0.01; + target->monsterKnockbackTangentDir = tangent; + target->monsterKnockbackUID = caster->getUID(); + } + else if ( target->behavior == &actPlayer ) + { + if ( !players[target->skill[2]]->isLocalPlayer() ) + { + target->monsterKnockbackVelocity = pushbackMultiplier; + target->monsterKnockbackTangentDir = tangent; + serverUpdateEntityFSkill(target, 11); + serverUpdateEntityFSkill(target, 9); + } + else + { + target->monsterKnockbackVelocity = pushbackMultiplier; + target->monsterKnockbackTangentDir = tangent; + } + } + + messagePlayer(target->isEntityPlayer(), MESSAGE_HINT, Language::get(6756)); + } + } + else if ( caster->behavior == &actPlayer ) + { + found = true; + if ( players[player]->isLocalPlayer() ) + { + players[player]->magic.telekinesisTarget = castSpellProps->targetUID; + } + else + { + client_selected[player] = target; + inrange[player] = true; + } + + if ( Entity* telekinesisTarget = uidToEntity(players[player]->magic.telekinesisTarget) ) + { + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spellEventFlags | spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + } + spawnMagicEffectParticles(target->x, target->y, target->z, 171); + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_DISARM || spell->ID == SPELL_STRIP ) + { + if ( caster && caster->behavior == &actPlayer ) + { + bool found = false; + bool effect = false; + if ( castSpellProps && castSpellProps->targetUID != 0 ) + { + if ( Entity* target = uidToEntity(castSpellProps->targetUID) ) + { + if ( Stat* targetStats = target->getStats() ) + { + bool doEffect = true; + + std::vector slots; + slots.push_back(&targetStats->weapon); + if ( spell->ID == SPELL_STRIP ) + { + slots.push_back(&targetStats->shield); + slots.push_back(&targetStats->helmet); + slots.push_back(&targetStats->breastplate); + slots.push_back(&targetStats->gloves); + slots.push_back(&targetStats->shoes); + slots.push_back(&targetStats->cloak); + slots.push_back(&targetStats->amulet); + slots.push_back(&targetStats->ring); + slots.push_back(&targetStats->mask); + } + + for ( auto slot : slots ) + { + if ( !(*slot) ) + { + continue; + } + if ( target->behavior == &actMonster + && ((itemCategory(*slot) == SPELLBOOK) || !(*slot)->isDroppable) ) + { + doEffect = false; + } + else if ( targetStats->type == LICH || targetStats->type == LICH_FIRE || targetStats->type == LICH_ICE || targetStats->type == DEVIL + || targetStats->type == SHADOW ) + { + doEffect = false; + } + else if ( target->behavior == &actMonster + && (target->monsterAllySummonRank != 0 + || (targetStats->type == INCUBUS && !strncmp(targetStats->name, "inner demon", strlen("inner demon")))) + ) + { + doEffect = false; + } + + if ( doEffect ) + { + if ( Entity* dropped = dropItemMonster(*slot, target, targetStats, (*slot)->count) ) + { + effect = true; + + dropped->itemDelayMonsterPickingUp = element->duration; + double tangent = atan2(target->y - caster->y, target->x - caster->x) + PI; + dropped->yaw = tangent + PI; + dropped->vel_x = (1.5 + .025 * (local_rng.rand() % 11)) * cos(tangent); + dropped->vel_y = (1.5 + .025 * (local_rng.rand() % 11)) * sin(tangent); + dropped->vel_z = (-10 - local_rng.rand() % 20) * .01; + dropped->flags[USERFLAG1] = false; + dropped->itemOriginalOwner = target->getUID(); + } + else + { + if ( !(*slot) ) + { + effect = true; + } + } + } + } + found = true; + + + spawnMagicEffectParticles(target->x, target->y, target->z, 171); + if ( !effect ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(255, 0, 0), + *targetStats, Language::get(2905), Language::get(2906), MSG_COMBAT); + } + else + { + applyGenericMagicDamage(caster, target, *caster, spell->ID, 0, true, false); // alert the target + + if ( spell->ID == SPELL_DISARM ) + { + magicOnEntityHit(caster, caster, target, targetStats, 0, 0, 0, spell ? spell->ID : SPELL_NONE, usingSpellbook ? SPELLBOOK_DISARM : 0); + } + else + { + magicOnEntityHit(caster, caster, target, targetStats, 0, 0, 0, spell ? spell->ID : SPELL_NONE); + } + //magicOnSpellCastEvent(caster, caster, target, spell->ID, spellEventFlags | spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + + playSoundEntity(caster, 167, 128); + if ( spell->ID == SPELL_DISARM ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6644), Language::get(6645), MSG_COMBAT); + } + else if ( spell->ID == SPELL_STRIP ) + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), + *targetStats, Language::get(6646), Language::get(6647), MSG_COMBAT); + } + } + } + } + } + if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_DETECT_ENEMY ) + { + if ( caster ) + { + auto compFunc = [](std::pair, Entity*>& lhs, std::pair, Entity*>& rhs) + { + int lhs_dist = 64 * ((int)(lhs.first.second) / 64); + int rhs_dist = 64 * ((int)(rhs.first.second) / 64); + if ( lhs_dist == rhs_dist ) + { + return lhs.first.first < rhs.first.first; + } + return lhs_dist > rhs_dist; + }; + std::vector, Entity*>> enemies; + bool found = false; + for ( node_t* node = map.creatures->first; node; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, 10000, caster, false, TARGET_ENEMY) ) + { + if ( entity->behavior == actMonster ) + { + if ( Stat* stats = entity->getStats() ) + { + enemies.push_back(std::make_pair(std::make_pair(stats->HP, -entityDist(caster, entity)), entity)); + } + } + } + } + + std::sort(enemies.begin(), enemies.end(), compFunc); + int maxElements = std::min((int)enemies.size(), 5); + enemies.resize(maxElements); + while ( enemies.size() ) + { + int pick = local_rng.rand() % enemies.size(); + auto entity = enemies[pick].second; + if ( Stat* stats = entity->getStats() ) + { + if ( stats->getEffectActive(EFF_DETECT_ENEMY) ) + { + enemies.erase(enemies.begin() + pick); + continue; + } + + int duration = element->duration; + if ( entity->setEffect(EFF_DETECT_ENEMY, + (Uint8)(caster->behavior != &actPlayer ? MAXPLAYERS + 1 : caster->skill[2] + 1), duration, true, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6495)); + + createParticleSpellPinpointTarget(entity, caster->getUID(), 1775, duration, spell->ID); + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_PINPOINT, 1775, caster->getUID(), duration, spell->ID); + + if ( !strcmp(stats->name, "") ) + { + updateEnemyBar(caster, entity, getMonsterLocalizedName(stats->type).c_str(), stats->HP, stats->MAXHP, + false, DMG_DETECT_MONSTER); + } + else + { + updateEnemyBar(caster, entity, stats->name, stats->HP, stats->MAXHP, + false, DMG_DETECT_MONSTER); + } + found = true; + + playSoundEntity(entity, 167, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 174); + + magicOnSpellCastEvent(caster, caster, nullptr, SPELL_DETECT_ENEMY, spell_t::SPELL_LEVEL_EVENT_EFFECT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + break; + } + } + } + + if ( !found ) + { + if ( maxElements > 0 ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6946)); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6947)); + } + } + if ( caster ) + { + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + } + playSoundEntity(caster, 167, 128); + } + } + else if ( spell->ID == SPELL_PINPOINT || spell->ID == SPELL_PENANCE ) + { + if ( caster && caster->behavior == &actPlayer ) + { + auto compFunc = [](std::pair, Entity*>& lhs, std::pair, Entity*>& rhs) + { + return lhs.first.second > rhs.first.second; + }; + std::vector, Entity*>> enemies; + bool found = false; + for ( node_t* node = map.creatures->first; node; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, 10000, caster, false, TARGET_ENEMY) ) + { + if ( Stat* stats = entity->getStats() ) + { + if ( spell->ID == SPELL_PINPOINT ) + { + enemies.push_back(std::make_pair(std::make_pair(stats->HP, -entityDist(caster, entity)), entity)); + } + else if ( spell->ID == SPELL_PENANCE ) + { + Uint8 effectStrength = stats->getEffectActive(EFF_PENANCE); + if ( effectStrength == 0 ) + { + enemies.push_back(std::make_pair(std::make_pair(stats->HP, -entityDist(caster, entity)), entity)); + } + } + } + } + } + + std::sort(enemies.begin(), enemies.end(), compFunc); + int maxElements = std::min((int)enemies.size(), 5); + enemies.resize(maxElements); + while ( enemies.size() ) + { + int pick = local_rng.rand() % enemies.size(); + auto entity = enemies[pick].second; + if ( spell->ID == SPELL_PINPOINT ) + { + if ( Stat* stats = entity->getStats() ) + { + if ( stats->getEffectActive(EFF_PINPOINT) ) + { + enemies.erase(enemies.begin() + pick); + continue; + } + } + + int duration = element->duration; + if ( entity->setEffect(EFF_PINPOINT, (Uint8)(caster->behavior != &actPlayer ? MAXPLAYERS + 1 : caster->skill[2] + 1), duration, true, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6480)); + if ( Entity* fx = createParticleSpellPinpointTarget(entity, caster->getUID(), 1767, duration, spell->ID) ) + { + if ( spellBookBonusPercent > 0 ) + { + fx->actmagicSpellbookBonus = spellBookBonusPercent; + } + fx->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + } + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_PINPOINT, 1767, caster->getUID(), duration, spell->ID); + + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_PINPOINT, 20.0, 1.0, nullptr); + } + + playSoundEntity(entity, 167, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 174); + found = true; + break; + } + } + else + { + int duration = element->duration; + if ( entity->setEffect(EFF_PENANCE, (Uint8)(caster->behavior != &actPlayer ? MAXPLAYERS + 1 : caster->skill[2] + 1), duration, true, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6487)); + createParticleSpellPinpointTarget(entity, caster->getUID(), 1773, duration, spell->ID); + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_PINPOINT, 1773, caster->getUID(), duration, spell->ID); + + entity->monsterReleaseAttackTarget(); + for ( node_t* node = map.creatures->first; node != nullptr; node = node->next ) + { + Entity* entity2 = (Entity*)node->element; + if ( !entity2 ) { continue; } + if ( entity2->behavior == &actMonster && entity2 != entity ) + { + if ( entity2->monsterAllyGetPlayerLeader() && ((Uint32)entity2->monsterTarget == entity->getUID()) ) + { + entity2->monsterReleaseAttackTarget(); // player allies stop attacking this target + } + } + } + + playSoundEntity(entity, 167, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 174); + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_PENANCE, 20.0, 1.0, nullptr); + } + found = true; + break; + } + } + enemies.erase(enemies.begin() + pick); + } + + if ( !found ) + { + if ( maxElements > 0 ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6948)); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6949)); + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + playSoundEntity(caster, 163, 128); + } + else + { + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + playSoundEntity(caster, 167, 128); + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_detectFood.element_internal_name) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + spell_detectFoodEffectOnMap(i); + } + } + playSoundEntity(caster, 167, 128); + } + else if ( !strcmp(element->element_internal_name, spellElement_salvageItem.element_internal_name) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + int searchx = static_cast(caster->x + 32 * cos(caster->yaw)) >> 4; + int searchy = static_cast(caster->y + 32 * sin(caster->yaw)) >> 4; + std::vector itemsOnGround = TileEntityList.getEntitiesWithinRadius(searchx, searchy, 2); + int totalMetal = 0; + int totalMagic = 0; + int numItems = 0; + std::set> effectCoordinates; + for ( auto it = itemsOnGround.begin(); it != itemsOnGround.end(); ++it ) + { + list_t* currentList = *it; + node_t* itemNode; + node_t* nextItemNode = nullptr; + for ( itemNode = currentList->first; itemNode != nullptr; itemNode = nextItemNode ) + { + nextItemNode = itemNode->next; + Entity* itemEntity = (Entity*)itemNode->element; + if ( itemEntity && !itemEntity->flags[INVISIBLE] && itemEntity->behavior == &actItem && entityDist(itemEntity, caster) < TOUCHRANGE ) + { + Item* toSalvage = newItemFromEntity(itemEntity); + if ( toSalvage && GenericGUI[i].isItemSalvageable(toSalvage, i) ) + { + int metal = 0; + int magic = 0; + GenericGUIMenu::tinkeringGetItemValue(toSalvage, &metal, &magic); + totalMetal += metal; + totalMagic += magic; + ++numItems; + effectCoordinates.insert(std::make_pair(static_cast(itemEntity->x) >> 4, static_cast(itemEntity->y) >> 4)); + + // delete item on ground. + itemEntity->removeLightField(); + list_RemoveNode(itemEntity->mynode); + } + free(toSalvage); + } + } + } + if ( totalMetal == 0 && totalMagic == 0 ) + { + messagePlayer(i, MESSAGE_COMBAT, Language::get(3713)); + playSoundEntity(caster, 163, 128); + } + else + { + if ( totalMetal >= 10 ) + { + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, numItems, allowedSkillup); + } + else + { + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, numItems, allowedSkillup); + } + messagePlayerColor(i, MESSAGE_INVENTORY, makeColorRGB(0, 255, 0), Language::get(3712), numItems); + playSoundEntity(caster, 167, 128); + } + + int pickedUpMetal = 0; + while ( totalMetal > 0 ) + { + int metal = std::min(totalMetal, SCRAP_MAX_STACK_QTY - 1); + totalMetal -= metal; + Item* crafted = newItem(TOOL_METAL_SCRAP, DECREPIT, 0, metal, 0, true, nullptr); + if ( crafted ) + { + Item* pickedUp = itemPickup(player, crafted); + pickedUpMetal += metal; + if ( i == 0 || players[i]->isLocalPlayer() ) // server/singleplayer + { + free(crafted); // if player != clientnum, then crafted == pickedUp + } + if ( i != 0 && !players[i]->isLocalPlayer() ) + { + free(pickedUp); + } + } + } + if ( pickedUpMetal > 0 ) + { + messagePlayer(player, MESSAGE_INVENTORY, Language::get(3665), pickedUpMetal, items[TOOL_METAL_SCRAP].getIdentifiedName()); + } + int pickedUpMagic = 0; + while ( totalMagic > 0 ) + { + int magic = std::min(totalMagic, SCRAP_MAX_STACK_QTY - 1); + totalMagic -= magic; + Item* crafted = newItem(TOOL_MAGIC_SCRAP, DECREPIT, 0, magic, 0, true, nullptr); + if ( crafted ) + { + Item* pickedUp = itemPickup(player, crafted); + pickedUpMagic += magic; + if ( i == 0 || players[i]->isLocalPlayer() ) // server/singleplayer + { + free(crafted); // if player != clientnum, then crafted == pickedUp + } + if ( i != 0 && !players[i]->isLocalPlayer() ) + { + free(pickedUp); + } + } + } + if ( pickedUpMagic > 0 ) + { + messagePlayer(player, MESSAGE_INVENTORY, Language::get(3665), pickedUpMagic, items[TOOL_MAGIC_SCRAP].getIdentifiedName()); + } + if ( !effectCoordinates.empty() ) + { + for ( auto it = effectCoordinates.begin(); it != effectCoordinates.end(); ++it ) + { + std::pair coords = *it; + spawnMagicEffectParticles(coords.first * 16 + 8, coords.second * 16 + 8, 7, 171); + } + } + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + break; + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_trollsBlood.element_internal_name) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + int amount = element->duration; + amount += ((spellBookBonusPercent * 2 / 100.f) * amount); // 100-200% + + if ( overdrewIntoHP ) + { + amount /= 4; + messagePlayerColor(player, MESSAGE_COMBAT, makeColorRGB(255, 255, 255), Language::get(3400)); + } + + caster->setEffect(EFF_TROLLS_BLOOD, true, amount, true); + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerColor(i, MESSAGE_HINT, color, Language::get(3490)); + for ( node = map.creatures->first; node; node = node->next ) + { + Entity* entity = (Entity*)(node->element); + if ( !entity || entity == caster ) + { + continue; + } + if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) + { + continue; + } + + if ( entityDist(entity, caster) <= HEAL_RADIUS && entity->checkFriend(caster) ) + { + entity->setEffect(EFF_TROLLS_BLOOD, true, amount, true); + playSoundEntity(entity, 168, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); + if ( entity->behavior == &actPlayer ) + { + messagePlayerColor(entity->skill[2], MESSAGE_HINT, color, Language::get(3490)); + } + } + } + + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_EFFECT | spellEventFlags, 1, allowedSkillup); + break; + } + } + if ( caster->behavior == &actMonster ) + { + caster->setEffect(EFF_TROLLS_BLOOD, true, element->duration, true); + } + + playSoundEntity(caster, 168, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 169); + } + else if ( spell->ID == SPELL_HEAL_MINOR || spell->ID == SPELL_HEAL_OTHER ) + { + if ( caster ) + { + if ( spell->ID == SPELL_HEAL_MINOR || (spell->ID == SPELL_HEAL_OTHER && castSpellProps) ) + { + int amount = element->duration; + if ( caster->behavior == &actPlayer ) + { + if ( overdrewIntoHP ) + { + amount /= 4; + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_COMBAT, makeColorRGB(255, 255, 255), Language::get(3400)); + } + else + { + amount *= getSpellDamageSecondaryFromID(spell->ID, caster, caster->getStats(), caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0) / 100.0; + } + } + else + { + amount *= getSpellDamageSecondaryFromID(spell->ID, caster, caster->getStats(), caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0) / 100.0; + } + + Entity* target = spell->ID == SPELL_HEAL_MINOR ? caster : uidToEntity(castSpellProps->targetUID); + if ( target && target->getStats() ) + { + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(target, 1); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity2 = (Entity*)node->element ) + { + if ( entity2->behavior == &actRadiusMagic + && entity2->actRadiusMagicID == spell->ID + && entity2->actRadiusMagicFollowUID == target->getUID() ) + { + entity2->skill[0] = -1; // dispel the previous effect + } + } + } + } + + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + target->x, target->y, 16, amount, target) ) + { + fx->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + if ( spellBookBonusPercent > 0 ) + { + fx->actmagicSpellbookBonus = spellBookBonusPercent; + if ( caster->behavior == &actPlayer ) + { + if ( overdrewIntoHP ) + { + fx->actmagicSpellbookBonus = 0; + } + } + } + if ( target == caster ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6930)); + } + else + { + messagePlayerMonsterEvent(caster->isEntityPlayer(), makeColorRGB(0, 255, 0), *target->getStats(), + Language::get(6928), Language::get(6929), MSG_COMBAT); + if ( target->isEntityPlayer() >= 0 ) + { + messagePlayerColor(target->isEntityPlayer(), MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6932)); + } + } + + playSoundEntity(caster, 168, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 169); + + if ( caster->getStats() ) + { + caster->setEffect(EFF_HEALING_WORD, true, std::max(amount + 5, caster->getStats()->EFFECTS_TIMERS[EFF_HEALING_WORD]), false); + } + } + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_flutter.element_internal_name) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + //Duration for flutter. + int duration = element->duration; + + if ( caster->getStats() && !caster->getStats()->getEffectActive(EFF_FLUTTER) ) + { + achievementObserver.playerAchievements[i].flutterShyCoordinates = std::make_pair(caster->x, caster->y); + } + + if ( caster->setEffect(EFF_FLUTTER, true, duration, true) ) + { + messagePlayerColor(i, MESSAGE_STATUS, uint32ColorGreen, Language::get(3767)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + + //magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_EFFECT | spellEventFlags, 1, allowedSkillup); + } + break; + } + } + } + else if ( spell->ID == SPELL_BLOOD_WARD ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_BLOOD_WARD, true, duration, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6502)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2207); + } + } + } + else if ( spell->ID == SPELL_TRUE_BLOOD ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_TRUE_BLOOD, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6506)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_DIVINE_ZEAL ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_DIVINE_ZEAL, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6508)); + playSoundEntity(caster, 178, 128); + createParticleFociLight(caster, spell->ID, true); + + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + caster->x, caster->y, 16, 2 * TICKS_PER_SECOND, caster) ) + { + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_FLAME_CLOAK].element_internal_name) ) + { + if ( caster ) + { + if ( caster->behavior == &actMonster ) + { + caster->setEffect(EFF_FLAME_CLOAK, (Uint8)100, 1500, true); + playSoundEntity(caster, 164, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2207); + } + else if ( caster->behavior == &actPlayer && caster->getStats() ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + + int duration = element->duration; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + channeled_spell->channel_effectStrength = 25; + int effectStrength = getSpellDamageFromID(SPELL_FLAME_CLOAK, caster, nullptr, caster); + effectStrength = std::min(effectStrength, getSpellDamageSecondaryFromID(SPELL_FLAME_CLOAK, caster, nullptr, caster)); + channeled_spell->channel_effectStrength = std::min(100, effectStrength); + if ( caster->setEffect(EFF_FLAME_CLOAK, (Uint8)channeled_spell->channel_effectStrength, duration, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6730)); + playSoundEntity(caster, 164, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2207); + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_GUARD_BODY].element_internal_name) ) + { + if ( caster ) + { + if ( caster->behavior == &actMonster ) + { + caster->setEffect(EFF_GUARD_BODY, (Uint8)15, 1500, true); + playSoundEntity(caster, 166, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2204); + } + else if ( caster->behavior == &actPlayer && caster->getStats() ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + + int duration = element->duration; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + channeled_spell->channel_effectStrength = 1; + int effectStrength = 3;// std::max(3, getSpellDamageFromID(SPELL_GUARD_BODY, caster, nullptr, caster)); + effectStrength = std::min(effectStrength, getSpellEffectDurationSecondaryFromID(SPELL_GUARD_BODY, caster, nullptr, caster)); + channeled_spell->channel_effectStrength = std::min(100, effectStrength); + if ( caster->setEffect(EFF_GUARD_BODY, (Uint8)channeled_spell->channel_effectStrength, duration, true, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6473)); + playSoundEntity(caster, 167, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); + + for ( node_t* node = caster->getStats()->magic_effects.first; node; node = node->next ) + { + if ( spell_t* spell = (spell_t*)node->element ) + { + if ( spell->ID == SPELL_GUARD_SPIRIT || spell->ID == SPELL_DIVINE_GUARD ) + { + spell->sustain = false; + } + } + } + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_GUARD_SPIRIT].element_internal_name) ) + { + if ( caster ) + { + if ( caster->behavior == &actMonster ) + { + caster->setEffect(EFF_GUARD_SPIRIT, (Uint8)15, 1500, true); + playSoundEntity(caster, 166, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2204); + } + else if ( caster->behavior == &actPlayer && caster->getStats() ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + + int duration = element->duration; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + channeled_spell->channel_effectStrength = 1; + int effectStrength = 1;// std::max(3, getSpellDamageFromID(SPELL_GUARD_SPIRIT, caster, nullptr, caster)); + effectStrength = std::min(effectStrength, getSpellEffectDurationSecondaryFromID(SPELL_GUARD_SPIRIT, caster, nullptr, caster)); + channeled_spell->channel_effectStrength = std::min(100, effectStrength); + if ( caster->setEffect(EFF_GUARD_SPIRIT, (Uint8)channeled_spell->channel_effectStrength, duration, true, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6474)); + playSoundEntity(caster, 167, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); + + for ( node_t* node = caster->getStats()->magic_effects.first; node; node = node->next ) + { + if ( spell_t* spell = (spell_t*)node->element ) + { + if ( spell->ID == SPELL_DIVINE_GUARD || spell->ID == SPELL_GUARD_BODY ) + { + spell->sustain = false; + } + } + } + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_DIVINE_GUARD].element_internal_name) ) + { + if ( caster ) + { + if ( caster->behavior == &actMonster ) + { + caster->setEffect(EFF_DIVINE_GUARD, (Uint8)15, 1500, true); + playSoundEntity(caster, 166, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2204); + } + else if ( caster->behavior == &actPlayer && caster->getStats() ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + + int duration = element->duration; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + channeled_spell->channel_effectStrength = 1; + int effectStrength = 3;//std::max(3, getSpellDamageFromID(SPELL_DIVINE_GUARD, caster, nullptr, caster)); + effectStrength = std::min(effectStrength, getSpellEffectDurationSecondaryFromID(SPELL_DIVINE_GUARD, caster, nullptr, caster)); + channeled_spell->channel_effectStrength = std::min(100, effectStrength); + if ( caster->setEffect(EFF_DIVINE_GUARD, (Uint8)channeled_spell->channel_effectStrength, duration, true, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), + MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6475)); + playSoundEntity(caster, 167, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); + + for ( node_t* node = caster->getStats()->magic_effects.first; node; node = node->next ) + { + if ( spell_t* spell = (spell_t*)node->element ) + { + if ( spell->ID == SPELL_GUARD_SPIRIT || spell->ID == SPELL_GUARD_BODY ) + { + spell->sustain = false; + } + } + } + } + } + } + } + else if ( spell->ID == SPELL_OVERCHARGE ) + { + if ( caster ) + { + int charges = getSpellDamageFromID(SPELL_OVERCHARGE, caster, caster->getStats(), caster, spellBookBonusPercent); + charges = std::min(charges, getSpellEffectDurationSecondaryFromID(SPELL_OVERCHARGE, caster, caster->getStats(), caster, spellBookBonusPercent)); + + //if ( caster->setEffect(EFF_OVERCHARGE, (Uint8)charges, element->duration, false, true, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6520)); + playSoundEntity(caster, 167, 128); + + createParticleDropRising(caster, 791, 1.0); + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_RISING_DROP, 791); + + if ( caster->behavior == &actPlayer ) + { + if ( players[caster->skill[2]]->isLocalPlayer() ) + { + cast_animation[caster->skill[2]].overcharge_init = charges; + } + else if ( caster->skill[2] > 0 && multiplayer == SERVER && !players[caster->skill[2]]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "OVRC"); + net_packet->data[4] = charges; + net_packet->address.host = net_clients[caster->skill[2] - 1].host; + net_packet->address.port = net_clients[caster->skill[2] - 1].port; + net_packet->len = 5; + sendPacketSafe(net_sock, -1, net_packet, caster->skill[2] - 1); + } + } + } + } + } + else if ( spell->ID == SPELL_ENVENOM_WEAPON ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_ENVENOM_WEAPON, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6521)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_THORNS ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_THORNS, true, duration, true) ) + { + caster->setEffect(EFF_BLADEVINES, false, 0, true); + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6795)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_BLADEVINES ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_BLADEVINES, true, duration, true) ) + { + caster->setEffect(EFF_THORNS, false, 0, true); + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6796)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_ABUNDANCE ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_ABUNDANCE, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6650)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_GREATER_ABUNDANCE ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_GREATER_ABUNDANCE, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6651)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_PRESERVE ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_PRESERVE, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6655)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_MIST_FORM ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_MIST_FORM, true, duration, true) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6663)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_LIGHTEN_LOAD ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + channeled_spell->channel_effectStrength = 50; + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_LIGHTEN_LOAD, (Uint8)50, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6681)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_ATTRACT_ITEMS ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_ATTRACT_ITEMS, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6683)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_RETURN_ITEMS ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + } + if ( caster->setEffect(EFF_RETURN_ITEM, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6685)); + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( spell->ID == SPELL_HOLOGRAM ) + { + if ( caster ) + { + if ( castSpellProps ) + { + if ( Entity* monster = spellEffectHologram(*caster, *element, castSpellProps->target_x, castSpellProps->target_y) ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6667)); + monster->monsterSpecialState = caster->getUID(); + serverUpdateEntitySkill(monster, 33); + monster->setEffect(EFF_MIST_FORM, true, element->duration, false); + } + else + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6578)); + } + } + } + } + else if ( spell->ID == SPELL_SPORES ) + { + if ( caster ) + { + int duration = element->duration; + if ( caster->behavior == &actPlayer ) + { + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + } + else + { + duration = element->duration; + floorMagicCreateSpores(caster, caster->x, caster->y, caster, 0, SPELL_SPORES); + } + if ( caster->setEffect(EFF_SPORES, true, duration, false) ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, uint32ColorGreen, Language::get(6643)); + playSoundEntity(caster, 717 + local_rng.rand() % 3, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 170); + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_dash.element_internal_name) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + playSoundEntity(caster, 180, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); + + int strength = getSpellDamageFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + if ( strength >= 2 ) + { + Uint8 effectStrength = 2 + (0 * (MAXPLAYERS + 1)) + i; + caster->setEffect(EFF_DASH, effectStrength, 60, false); + } + else + { + caster->setEffect(EFF_DASH, true, 60, false); + } + + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, + spell_t::SPELL_LEVEL_EVENT_EFFECT + | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE + | spellEventFlags, 1, allowedSkillup); + if ( i > 0 && multiplayer == SERVER && !players[i]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "DASH"); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 4; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + else + { + real_t vel = sqrt(pow(caster->vel_y, 2) + pow(caster->vel_x, 2)); + real_t maxVelocity = 0.25; + int percentModifier = getSpellDamageSecondaryFromID(spell->ID, caster, nullptr, caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + maxVelocity += (2.25 - 0.25) * (std::min(100, percentModifier) / 100.0); + maxVelocity = std::min(2.25, maxVelocity); + caster->monsterKnockbackVelocity = std::min(maxVelocity, std::max(1.0, vel)); + caster->monsterKnockbackTangentDir = atan2(caster->vel_y, caster->vel_x); + if ( vel < 0.01 ) + { + caster->monsterKnockbackTangentDir = caster->yaw + PI; + } + } + break; + } + } + if ( caster->behavior == &actMonster ) + { + playSoundEntity(caster, 180, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 982); + caster->setEffect(EFF_DASH, true, 30, false); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_speed.element_internal_name) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + //Duration for speed. + int duration = element->duration; + duration += ((spellBookBonusPercent * 2 / 100.f) * duration); // 100-200% + + if ( stats[i]->getEffectActive(EFF_SLOW) ) + { + caster->setEffect(EFF_SLOW, false, 0, true); + } + caster->setEffect(EFF_FAST, Uint8(1 | (1 << (i + 1))), duration, false, true, true); + messagePlayerColor(i, MESSAGE_STATUS, uint32ColorGreen, Language::get(768)); + for ( node = map.creatures->first; node; node = node->next ) + { + Entity* entity = (Entity*)(node->element); + if ( !entity || entity == caster ) + { + continue; + } + if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) + { + continue; + } + + if ( entityDist(entity, caster) <= HEAL_RADIUS && entity->checkFriend(caster) ) + { + entity->setEffect(EFF_FAST, Uint8(1 | (1 << (i + 1))), duration, false, true, true); + playSoundEntity(entity, 178, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 174); + if ( entity->behavior == &actPlayer ) + { + messagePlayerColor(entity->skill[2], MESSAGE_STATUS, uint32ColorGreen, Language::get(768)); + } + } + } + + //magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_EFFECT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE | spellEventFlags, 1, allowedSkillup); + break; + } + } + if ( caster->behavior == &actMonster ) + { + if ( caster->getStats()->getEffectActive(EFF_SLOW) ) + { + caster->setEffect(EFF_SLOW, false, 0, true); + } + caster->setEffect(EFF_FAST, true, element->duration, true); + } + + playSoundEntity(caster, 178, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + } + else if (!strcmp(element->element_internal_name, spellElement_heal.element_internal_name) + || (spell->ID == SPELL_EXTRAHEALING) + /*|| (spell->ID == SPELL_HEAL_OTHER && castSpellProps)*/ ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + int amount = element->getDamage(); //Amount to heal. + + // spellbook 100-150%, 50 INT = 200%. + real_t bonus = ((spellBookBonusPercent * 1 / 100.f) + getBonusFromCasterOfSpellElement(caster, nullptr, element, spell ? spell->ID : SPELL_NONE, spell->skillID)); + if ( overdrewIntoHP ) + { + amount /= 2; + } + else + { + amount += amount * bonus; + } + if ( caster && caster->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateCodex(caster->skill[2], Compendium_t::CPDM_CLASS_PWR_MAX_CASTED, "pwr", + (Sint32)(bonus * 100.0)); + } + + int totalHeal = 0; + playSoundEntity(caster, 168, 128); + if ( spell->ID == SPELL_HEAL_OTHER ) + { + if ( Entity* entity = uidToEntity(castSpellProps->targetUID) ) + { + int oldHP = entity->getHP(); + spell_changeHealth(entity, amount, overdrewIntoHP); + int heal = std::max(entity->getHP() - oldHP, 0); + totalHeal += heal; + if ( heal > 0 ) + { + spawnDamageGib(entity, -heal, DamageGib::DMG_HEAL, DamageGibDisplayType::DMG_GIB_NUMBER, true); + } + playSoundEntity(entity, 168, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); + } + } + else + { + int oldHP = players[i]->entity->getHP(); + /*if ( overdrewIntoHP ) + { + amount /= 2; + }*/ + if ( oldHP > 0 ) + { + spell_changeHealth(players[i]->entity, amount, overdrewIntoHP); + } + totalHeal += std::max(players[i]->entity->getHP() - oldHP, 0); + if ( totalHeal > 0 ) + { + spawnDamageGib(players[i]->entity, -totalHeal, DamageGib::DMG_HEAL, DamageGibDisplayType::DMG_GIB_NUMBER, true); + } + for ( node = map.creatures->first; node; node = node->next ) + { + Entity* entity = (Entity*)(node->element); + if ( !entity || entity == caster ) + { + continue; + } + if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) + { + continue; + } + + if ( entityDist(entity, caster) <= HEAL_RADIUS && entity->checkFriend(caster) ) + { + oldHP = entity->getHP(); + spell_changeHealth(entity, amount); + int heal = std::max(entity->getHP() - oldHP, 0); + totalHeal += heal; + if ( heal > 0 ) + { + spawnDamageGib(entity, -heal, DamageGib::DMG_HEAL, DamageGibDisplayType::DMG_GIB_NUMBER, true); + } + playSoundEntity(entity, 168, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); + } + } + } + + if ( totalHeal > 0 ) + { + serverUpdatePlayerGameplayStats(i, STATISTICS_HEAL_BOT, totalHeal); + if ( spell && spell->ID > SPELL_NONE ) + { + //magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DMG | spellEventFlags, totalHeal, allowedSkillup); + if ( caster && caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(spell->ID, totalHeal + 50.0, 1.0, nullptr); + } + + if ( !using_magicstaff && !trap ) + { + if ( usingSpellbook ) + { + auto find = ItemTooltips.spellItems.find(spell->ID); + if ( find != ItemTooltips.spellItems.end() ) + { + if ( find->second.spellbookId >= 0 && find->second.spellbookId < NUMITEMS && items[find->second.spellbookId].category == SPELLBOOK ) + { + Compendium_t::Events_t::eventUpdate(i, Compendium_t::CPDM_SPELL_HEAL, (ItemType)find->second.spellbookId, totalHeal); + } + } + } + else + { + Compendium_t::Events_t::eventUpdate(i, Compendium_t::CPDM_SPELL_HEAL, SPELL_ITEM, totalHeal, false, spell->ID); + } + } + } + } + break; + } + } + + if ( caster->behavior == &actMonster ) + { + spell_changeHealth(caster, element->getDamage()); + } + + //playSoundEntity(caster, 168, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 169); + } + else if ( !strcmp(element->element_internal_name, spellElement_shapeshift.element_internal_name) + && caster && caster->behavior == &actPlayer ) + { + Monster type = NOTHING; + switch ( spell->ID ) + { + case SPELL_RAT_FORM: + type = RAT; + break; + case SPELL_TROLL_FORM: + type = TROLL; + break; + case SPELL_SPIDER_FORM: + type = SPIDER; + break; + case SPELL_IMP_FORM: + type = CREATURE_IMP; + break; + case SPELL_REVERT_FORM: + break; + default: + break; + } + int duration = element->duration; + if ( type != NOTHING && caster->setEffect(EFF_SHAPESHIFT, true, duration, true) ) + { + spawnExplosion(caster->x, caster->y, caster->z); + playSoundEntity(caster, 400, 92); + createParticleDropRising(caster, 593, 1.f); + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_RISING_DROP, 593); + + caster->effectShapeshift = type; + serverUpdateEntitySkill(caster, 53); + + for ( node = map.creatures->first; node && stat; node = node->next ) + { + Entity* entity = (Entity*)(node->element); + if ( !entity || entity == caster ) + { + continue; + } + if ( entity->behavior != &actMonster ) + { + continue; + } + if ( entity->monsterTarget == caster->getUID() && entity->checkEnemy(caster) ) + { + Monster oldType = stat->type; + stat->type = type; + if ( !entity->checkEnemy(caster) ) // we're now friendly. + { + entity->monsterReleaseAttackTarget(); + } + stat->type = oldType; + } + } + + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerColor(caster->skill[2], MESSAGE_STATUS, color, Language::get(3419), getMonsterLocalizedName((Monster)caster->effectShapeshift).c_str()); + } + else + { + if ( spell->ID == SPELL_REVERT_FORM ) + { + if ( stats[caster->skill[2]]->getEffectActive(EFF_SHAPESHIFT) ) + { + int previousShapeshift = caster->effectShapeshift; + caster->setEffect(EFF_SHAPESHIFT, false, 0, true); + caster->effectShapeshift = 0; + serverUpdateEntitySkill(caster, 53); + if ( previousShapeshift == CREATURE_IMP && !isLevitating(stats[caster->skill[2]]) ) + { + stats[caster->skill[2]]->setEffectActive(EFF_LEVITATING, 1); + stats[caster->skill[2]]->EFFECTS_TIMERS[EFF_LEVITATING] = 5; + } + + if ( stats[caster->skill[2]]->getEffectActive(EFF_POLYMORPH) ) + { + messagePlayer(caster->skill[2], MESSAGE_STATUS, Language::get(4302)); // return to your 'abnormal' form + } + else + { + messagePlayer(caster->skill[2], MESSAGE_STATUS, Language::get(3417)); + } + playSoundEntity(caster, 400, 92); + createParticleDropRising(caster, 593, 1.f); + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_RISING_DROP, 593); + + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, 1, allowedSkillup); + } + else if ( stats[caster->skill[2]]->getEffectActive(EFF_POLYMORPH) ) + { + caster->setEffect(EFF_POLYMORPH, false, 0, true); + caster->effectPolymorph = 0; + serverUpdateEntitySkill(caster, 50); + + messagePlayer(player, MESSAGE_STATUS, Language::get(3185)); + playSoundEntity(caster, 400, 92); + createParticleDropRising(caster, 593, 1.f); + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_RISING_DROP, 593); + + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, 1, allowedSkillup); + } + else + { + messagePlayer(caster->skill[2], MESSAGE_HINT, Language::get(3715)); + playSoundEntity(caster, 163, 128); + } + } + else + { + messagePlayer(caster->skill[2], MESSAGE_HINT, Language::get(3420)); + } + } + } + else if (!strcmp(element->element_internal_name, spellElement_cure_ailment.element_internal_name)) //TODO: Generalize it for NPCs too? + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + int c = 0; + int numEffectsCured = 0; + for (c = 0; c < NUMEFFECTS; ++c) //This does a whole lot more than just cure ailments. + { + if ( stats[i] && stats[i]->statusEffectRemovedByCureAilment(c, players[i]->entity) ) + { + if ( stats[i]->getEffectActive(c) ) + { + stats[i]->clearEffect(c); + if ( stats[i]->EFFECTS_TIMERS[c] > 0 ) + { + stats[i]->EFFECTS_TIMERS[c] = 1; + } + ++numEffectsCured; + } + } + } + if ( stats[i]->getEffectActive(EFF_WITHDRAWAL) ) + { + ++numEffectsCured; + players[i]->entity->setEffect(EFF_WITHDRAWAL, false, EFFECT_WITHDRAWAL_BASE_TIME, true); + serverUpdatePlayerGameplayStats(i, STATISTICS_FUNCTIONAL, 1); + } + if ( players[i]->entity->flags[BURNING] ) + { + ++numEffectsCured; + players[i]->entity->flags[BURNING] = false; + serverUpdateEntityFlag(players[i]->entity, BURNING); + } + + bool regenEffect = spellBookBonusPercent >= 10; + if ( regenEffect ) + { + int bonus = 10 * ((spellBookBonusPercent * 4) / 100.f); // 25% = 10 seconds, 50% = 20 seconds. + caster->setEffect(EFF_HP_REGEN, true, std::max(stats[i]->EFFECTS_TIMERS[EFF_HP_REGEN], bonus * TICKS_PER_SECOND), true); + } + + if ( numEffectsCured > 0 || regenEffect ) + { + serverUpdateEffects(player); + } + playSoundEntity(players[i]->entity, 168, 128); + + + int numAlliesEffectsCured = 0; + for ( node = map.creatures->first; node; node = node->next ) + { + Entity* entity = (Entity*)(node->element); + if ( !entity || entity == caster ) + { + continue; + } + if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) + { + continue; + } + int entityEffectsCured = 0; + Stat* target_stat = entity->getStats(); + if ( target_stat ) + { + if (entityDist(entity, caster) <= HEAL_RADIUS && entity->checkFriend(caster)) + { + for (c = 0; c < NUMEFFECTS; ++c) //This does a whole lot more than just cure ailments. + { + if ( target_stat->statusEffectRemovedByCureAilment(c, entity) ) + { + if ( target_stat->getEffectActive(c) ) + { + target_stat->clearEffect(c); + if ( target_stat->EFFECTS_TIMERS[c] > 0 ) + { + target_stat->EFFECTS_TIMERS[c] = 1; + } + ++numAlliesEffectsCured; + ++entityEffectsCured; + } + } + } + if ( target_stat->getEffectActive(EFF_WITHDRAWAL) ) + { + ++numAlliesEffectsCured; + ++entityEffectsCured; + entity->setEffect(EFF_WITHDRAWAL, false, EFFECT_WITHDRAWAL_BASE_TIME, true); + serverUpdatePlayerGameplayStats(i, STATISTICS_FUNCTIONAL, 1); + } + if ( regenEffect ) + { + int bonus = 10 * ((spellBookBonusPercent * 4) / 100.f); // 25% = 10 seconds, 50% = 20 seconds. + entity->setEffect(EFF_HP_REGEN, true, std::max(target_stat->EFFECTS_TIMERS[EFF_HP_REGEN], bonus * TICKS_PER_SECOND), true); + } + if ( entity->flags[BURNING] ) + { + ++numAlliesEffectsCured; + ++entityEffectsCured; + entity->flags[BURNING] = false; + serverUpdateEntityFlag(entity, BURNING); + } + if ( entity->behavior == &actPlayer && (entityEffectsCured > 0 || regenEffect) ) + { + serverUpdateEffects(entity->skill[2]); + messagePlayerColor(entity->skill[2], MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(411)); + } + + playSoundEntity(entity, 168, 128); + spawnMagicEffectParticles(entity->x, entity->y, entity->z, 169); + } + } + } + + if ( numEffectsCured > 0 || numAlliesEffectsCured > 0 ) + { + magicOnSpellCastEvent(caster, caster, nullptr, spell->ID, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spellEventFlags, numEffectsCured + numAlliesEffectsCured, allowedSkillup); + } + + if ( regenEffect || numEffectsCured > 0 ) + { + messagePlayerColor(i, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(411)); // your body feels cleansed + } + if ( numAlliesEffectsCured > 0 ) + { + messagePlayerColor(i, MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(4313)); // your allies feel cleansed + } + if ( !regenEffect && numEffectsCured == 0 && numAlliesEffectsCured == 0 ) + { + messagePlayer(i, MESSAGE_STATUS, Language::get(3715)); // had no effect. + } + break; + } + } + + playSoundEntity(caster, 168, 128 ); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 169); + } + else if ( !strcmp(element->element_internal_name, spellElement_summon.element_internal_name) ) + { + playSoundEntity(caster, 251, 128); + playSoundEntity(caster, 252, 128); + if ( caster->behavior == &actPlayer && stats[caster->skill[2]] ) + { + // kill old summons. + for ( node = stats[caster->skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + { + Entity* follower = nullptr; + if ( (Uint32*)(node)->element ) + { + follower = uidToEntity(*((Uint32*)(node)->element)); + } + if ( follower && follower->monsterAllySummonRank != 0 ) + { + Stat* followerStats = follower->getStats(); + if ( followerStats ) + { + follower->setMP(followerStats->MAXMP * (followerStats->HP / static_cast(followerStats->MAXHP))); + follower->setHP(0); + } + } + } + + real_t startx = caster->x; + real_t starty = caster->y; + real_t startz = -4; + real_t pitch = caster->pitch; + if ( pitch < 0 ) + { + pitch = 0; + } + // draw line from the players height and direction until we hit the ground. + real_t previousx = startx; + real_t previousy = starty; + int index = 0; + for ( ; startz < 0.f; startz += abs(0.05 * tan(pitch)) ) + { + startx += 0.1 * cos(caster->yaw); + starty += 0.1 * sin(caster->yaw); + index = (static_cast(starty + 16 * sin(caster->yaw)) >> 4) * MAPLAYERS + + (static_cast(startx + 16 * cos(caster->yaw)) >> 4) * MAPLAYERS * map.height; + if ( map.tiles[index] && !map.tiles[OBSTACLELAYER + index] ) + { + // store the last known good coordinate + previousx = startx; + previousy = starty; + } + if ( map.tiles[OBSTACLELAYER + index] ) + { + break; + } + } + + Entity* timer = createParticleTimer(caster, 55, 0); + timer->x = static_cast(previousx / 16) * 16 + 8; + timer->y = static_cast(previousy / 16) * 16 + 8; + timer->sizex = 4; + timer->sizey = 4; + timer->particleTimerCountdownSprite = 791; + timer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SPELL_SUMMON; + timer->particleTimerPreDelay = 40; + timer->particleTimerEndAction = PARTICLE_EFFECT_SPELL_SUMMON; + timer->z = 0; + Entity* sapParticle = createParticleSapCenter(caster, caster, SPELL_SUMMON, 599, 599); + sapParticle->parent = 0; + sapParticle->yaw = caster->yaw; + sapParticle->skill[7] = caster->getUID(); + sapParticle->skill[8] = timer->x; + sapParticle->skill[9] = timer->y; + serverSpawnMiscParticlesAtLocation(previousx / 16, previousy / 16, 0, PARTICLE_EFFECT_SPELL_SUMMON, 791); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_reflectMagic.element_internal_name) ) + { + if ( caster->behavior == &actMonster ) + { + caster->setEffect(EFF_MAGICREFLECT, true, 600, true); + playSoundEntity(caster, 166, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + } + else + { + //TODO: Refactor into a function that adds magic_effects. Invisibility also makes use of this. + //Also refactor the duration determining code. + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + + int duration = element->duration; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + caster->setEffect(EFF_MAGICREFLECT, true, duration, true); + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + serverUpdateEffects(i); + } + } + + playSoundEntity(caster, 166, 128 ); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_amplifyMagic.element_internal_name) ) + { + if ( caster->behavior == &actMonster ) + { + caster->setEffect(EFF_MAGICAMPLIFY, true, 600, true); + playSoundEntity(caster, 166, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + } + else + { + //TODO: Refactor into a function that adds magic_effects. Invisibility also makes use of this. + //Also refactor the duration determining code. + node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); + spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. + channeled_spell = (spell_t*)(spellnode->element); + channeled_spell->magic_effects_node = spellnode; + spellnode->size = sizeof(spell_t); + ((spell_t*)spellnode->element)->caster = caster->getUID(); + spellnode->deconstructor = &spellDeconstructor; + + int duration = element->duration; + channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. + caster->setEffect(EFF_MAGICAMPLIFY, true, duration, true); + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i] && caster && (caster == players[i]->entity) ) + { + serverUpdateEffects(i); + messagePlayer(i, MESSAGE_PROGRESSION, Language::get(3442)); + } + } + + playSoundEntity(caster, 166, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + } + } + else if ( !strcmp(element->element_internal_name, spellElement_vampiricAura.element_internal_name) ) + { + if ( caster->behavior == &actMonster ) + { + createParticleDropRising(caster, 600, 0.7); + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_VAMPIRIC_AURA, 600); + caster->getStats()->setEffectActive(EFF_VAMPIRICAURA, 1); + caster->getStats()->EFFECTS_TIMERS[EFF_VAMPIRICAURA] = 600; + } + else if ( caster->behavior == &actPlayer ) + { + channeled_spell = spellEffectVampiricAura(caster, spell); + } + //Also refactor the duration determining code. + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_FORGE_METAL_SCRAP].element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_FORGE_MAGIC_SCRAP].element_internal_name) ) + { + if ( caster ) + { + Item* item = nullptr; + if ( spell->ID == SPELL_FORGE_METAL_SCRAP ) + { + item = newItem(TOOL_METAL_SCRAP, DECREPIT, 0, 50, 0, true, nullptr); + } + else if ( spell->ID == SPELL_FORGE_MAGIC_SCRAP ) + { + item = newItem(TOOL_MAGIC_SCRAP, DECREPIT, 0, 50, 0, true, nullptr); + } + if ( item ) + { + std::string itemName = item->getName(); + int qty = item->count; + Entity* dropped = dropItemMonster(item, caster, nullptr, item->count); + if ( dropped ) + { + dropped->yaw = caster->yaw; + dropped->vel_x = (1.5 + .025 * (local_rng.rand() % 11)) * cos(caster->yaw); + dropped->vel_y = (1.5 + .025 * (local_rng.rand() % 11)) * sin(caster->yaw); + dropped->flags[USERFLAG1] = false; + + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_INVENTORY, makeColorRGB(0, 255, 0), Language::get(6697), qty, itemName.c_str()); + playSoundEntity(caster, 167, 128); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + } + else + { + free(item); + } + } + } + } + else if ( !strcmp(element->element_internal_name, spellElement_slime_spray.element_internal_name) + || !strcmp(element->element_internal_name, spellElementMap[SPELL_ELEMENT_PROPULSION_MAGIC_SPRAY].element_internal_name) ) + { + int particle = -1; + switch ( spell->ID ) + { + case SPELL_SLIME_ACID: + particle = 180; + break; + case SPELL_SLIME_WATER: + particle = 181; + break; + case SPELL_SLIME_FIRE: + particle = 182; + break; + case SPELL_SLIME_TAR: + particle = 183; + break; + case SPELL_SLIME_METAL: + particle = 184; + break; + case SPELL_GREASE_SPRAY: + particle = 245; + break; + case SPELL_BREATHE_FIRE: + particle = 233; + break; + default: + break; + } + + if ( spell->ID == SPELL_BREATHE_FIRE ) + { + Entity* spellTimer = createParticleTimer(caster, 50, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_FOCI_SPRAY; + spellTimer->particleTimerCountdownSprite = particle; + spellTimer->particleTimerVariable2 = SPELL_BREATHE_FIRE; + spellTimer->particleTimerVariable3 = getSpellDamageSecondaryFromID(SPELL_BREATHE_FIRE, caster, caster->getStats(), caster, usingSpellbook ? spellBookBonusPercent / 100.0 : 0.0); + spellTimer->particleTimerVariable3 = std::max(1, std::min(10, spellTimer->particleTimerVariable3)); + spellTimer->particleTimerEffectLifetime = spellTimer->particleTimerVariable3 * 25; + result = spellTimer; + } + else if ( particle >= 0 ) + { + Entity* spellTimer = createParticleTimer(caster, 30, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_MAGIC_SPRAY; + spellTimer->particleTimerCountdownSprite = particle; + spellTimer->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + if ( spellBookBonusPercent > 0 ) + { + spellTimer->actmagicSpellbookBonus = spellBookBonusPercent; + } + result = spellTimer; + + if ( !(caster && caster->behavior == &actMonster && caster->getStats() && caster->getStats()->type == SLIME) ) + { + // spawn these if not a slime doing its special attack, client spawns own particles + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_SLIME_SPRAY, particle); + } + } + } + else if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_ELEMENT_PROPULSION_FOCI_SPRAY].element_internal_name) + && (spell->ID == SPELL_FOCI_ARCS || spell->ID == SPELL_FOCI_FIRE || spell->ID == SPELL_FOCI_SNOW + || spell->ID == SPELL_FOCI_NEEDLES || spell->ID == SPELL_FOCI_SANDBLAST || spell->ID == SPELL_BREATHE_FIRE) ) + { + static ConsoleVariable cvar_foci_sprite("/foci_sprite", 13); + int particle = -1; + if ( svFlags & SV_FLAG_CHEATS ) + { + particle = *cvar_foci_sprite; + } + switch ( spell->ID ) + { + case SPELL_FOCI_FIRE: + case SPELL_BREATHE_FIRE: + particle = 233; + break; + case SPELL_FOCI_SNOW: + particle = 256; + break; + case SPELL_FOCI_NEEDLES: + particle = 2154; + break; + case SPELL_FOCI_ARCS: + particle = 2153; + break; + case SPELL_FOCI_SANDBLAST: + particle = 2156; + break; + default: + break; + } + if ( particle >= 0 && caster ) + { + real_t velocityBonus = 0.0; + { + real_t velocityDir = atan2(caster->vel_y, caster->vel_x); + real_t casterDir = fmod(caster->yaw, 2 * PI); + real_t yawDiff = velocityDir - casterDir; + while ( yawDiff > PI ) + { + yawDiff -= 2 * PI; + } + while ( yawDiff <= -PI ) + { + yawDiff += 2 * PI; + } + if ( abs(yawDiff) <= PI ) + { + real_t vel = sqrt(pow(caster->vel_x, 2) + pow(caster->vel_y, 2)); + velocityBonus = std::max(0.0, cos(yawDiff) * vel); + if ( spell->ID == SPELL_BREATHE_FIRE ) + { + velocityBonus += 2; + } + } + } + if ( Entity* gib = spawnFociGib(caster->x, caster->y, 1.0, caster->yaw, velocityBonus, caster->getUID(), particle, local_rng.rand()) ) + { + node_t* node = list_AddNodeFirst(&gib->children); + node->element = copySpell(spell); + ((spell_t*)node->element)->caster = caster->getUID(); + node->deconstructor = &spellDeconstructor; + node->size = sizeof(spell_t); + + if ( usingFoci && stat->shield && itemTypeIsFoci(stat->shield->type) ) + { + spellBookBonusPercent = getSpellbookBonusPercent(caster, stat, stat->shield); + if ( spellBookBonusPercent > 0 ) + { + gib->actmagicSpellbookBonus += spellBookBonusPercent; + } + } + result = gib; + } + + /*Entity* fx = createParticleAestheticOrbit(parent, 16, 50, PARTICLE_EFFECT_FOCI_ORBIT); + fx->scalex = 0.5; + fx->scaley = 0.5; + fx->scalez = 0.5; + fx->flags[SPRITE] = true; + fx->x = parent->x + 8.0 * cos(parent->yaw); + fx->y = parent->y + 8.0 * sin(parent->yaw); + fx->yaw = parent->yaw; + fx->fskill[0] = (PI / 4) + (local_rng.rand() % 2) * 3 * PI / 2; + fx->z = 0.0;*/ + //fx->actmagicOrbitDist = 4; + } + } + + if ( spell->ID == SPELL_FOCI_DARK_LIFE + || spell->ID == SPELL_FOCI_DARK_RIFT + || spell->ID == SPELL_FOCI_DARK_SILENCE + || spell->ID == SPELL_FOCI_DARK_SUPPRESS + || spell->ID == SPELL_FOCI_DARK_VENGEANCE ) + { + if ( caster ) + { + if ( castSpellProps ) + { + bool effect = true; + int charge = castSpellProps->optionalData & (0x7F); + bool newCast = charge == 1 && !(castSpellProps->optionalData & (1 << 7)); + bool finishCast = (castSpellProps->optionalData & (1 << 7)); + node_t* nextnode = nullptr; + int radius = getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_RADIUS, spell->ID, caster, nullptr, caster); + if ( finishCast ) + { + real_t x = caster->x; + real_t y = caster->y; + real_t dist = lineTrace(caster, caster->x, caster->y, caster->yaw, 64.0, 0, false); + x += dist * cos(caster->yaw); + y += dist * sin(caster->yaw); + + const int maxDuration = getSpellEffectDurationSecondaryFromID(spell->ID, caster, nullptr, caster); + if ( innerElement ) + { + int duration = std::min(maxDuration, innerElement->duration * charge); + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + x, y, radius, duration, nullptr) ) + { + fx->actRadiusMagicEffectPower = duration; + playSoundEntity(fx, 166, 128); + } + } + + //spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + createParticleFociDark(caster, spell->ID, true); + playSoundEntity(caster, 166, 128); + } + /*for ( node_t* node = map.entities->first; node; node = nextnode ) + { + nextnode = node->next; + if ( Entity* entity = (Entity*)(node->element) ) + { + if ( entity->behavior == &actRadiusMagic && entity->actRadiusMagicID == spell->ID + && entity->parent == caster->getUID() ) + { + if ( newCast || true ) + { + entity->removeLightField(); + list_RemoveNode(entity->mynode); + continue; + } + else + { + effect = false; + int duration = entity->skill[0]; + duration += element->duration; + duration = std::max(element->duration * charge, duration); + entity->skill[0] = duration; + entity->actRadiusMagicAutoPulseTick = getSpellPropertyFromID(spell_t::SPELLPROP_FOCI_REFIRE_TICKS, spell->ID, + caster, nullptr, caster); + if ( castSpellProps->optionalData & (1 << 7) ) + { + } + else + { + entity->actRadiusMagicDoPulseTick = ticks + 1; + } + break; + } + } + } + }*/ + + /*if ( effect && (castSpellProps->optionalData & (1 << 7)) ) + { + real_t x = caster->x; + real_t y = caster->y; + real_t dist = lineTrace(caster, caster->x, caster->y, caster->yaw, 64.0, 0, false); + x += dist * cos(caster->yaw); + y += dist * sin(caster->yaw); + + int duration = element->duration * charge; + if ( Entity* fx = createRadiusMagic(spell->ID, caster, + x, y, radius, duration, nullptr) ) + { + fx->actRadiusMagicAutoPulseTick = getSpellPropertyFromID(spell_t::SPELLPROP_FOCI_REFIRE_TICKS, spell->ID, + caster, nullptr, caster); + if ( castSpellProps->optionalData & (1 << 7) ) + { + } + } + + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 171, 128); + }*/ + } + } + } + else if ( spell->ID == SPELL_FOCI_LIGHT_SANCTUARY + || spell->ID == SPELL_FOCI_LIGHT_JUSTICE + || spell->ID == SPELL_FOCI_LIGHT_PURITY + || spell->ID == SPELL_FOCI_LIGHT_PEACE + || spell->ID == SPELL_FOCI_LIGHT_PROVIDENCE ) + { + if ( caster ) + { + if ( castSpellProps ) + { + /*real_t x = caster->x; + real_t y = caster->y; + real_t dist = lineTrace(caster, caster->x, caster->y, caster->yaw, 64.0, 0, false); + x += dist * cos(caster->yaw); + y += dist * sin(caster->yaw);*/ + + if ( castSpellProps->optionalData > 0 ) + { + int effectID = -1; + int particle = 0; + int langEntry = 0; + switch ( spell->ID ) + { + case SPELL_FOCI_LIGHT_SANCTUARY: + effectID = EFF_FOCI_LIGHT_SANCTUARY; + particle = 2188; + langEntry = 6830; + break; + case SPELL_FOCI_LIGHT_JUSTICE: + effectID = EFF_FOCI_LIGHT_JUSTICE; + particle = 2184; + langEntry = 6827; + break; + case SPELL_FOCI_LIGHT_PURITY: + effectID = EFF_FOCI_LIGHT_PURITY; + particle = 2187; + langEntry = 6829; + break; + case SPELL_FOCI_LIGHT_PEACE: + effectID = EFF_FOCI_LIGHT_PEACE; + langEntry = 6826; + break; + case SPELL_FOCI_LIGHT_PROVIDENCE: + effectID = EFF_FOCI_LIGHT_PROVIDENCE; + langEntry = 6828; + break; + default: + break; + } + particle = 169; + + int radius = getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_RADIUS, spell->ID, caster, nullptr, caster); + + if ( effectID > 0 ) + { + int charge = castSpellProps->optionalData & (0x7F); + + Uint8 effectStrength = std::min(4, 1 + (charge / 4)); + Sint32 CHR = statGetCHR(caster->getStats(), caster); + int tier = 1; + if ( CHR >= 3 && CHR <= 7 ) + { + tier = 1; + } + else if ( CHR >= 8 && CHR <= 14 ) + { + tier = 2; + } + else if ( CHR >= 15 && CHR <= 29 ) + { + tier = 3; + } + else if ( CHR >= 30 ) + { + tier = 4; + } + effectStrength = std::min((Uint8)tier, effectStrength); + + if ( castSpellProps->optionalData & (1 << 7) ) + { + // finishing casting, apply to allies + for ( node_t* node = map.creatures->first; node; node = node->next ) + { + if ( Entity* entity = getSpellTarget(node, radius + 4.0, caster, false, TARGET_FRIEND) ) + { + if ( Stat* entityStats = entity->getStats() ) + { + if ( innerElement ) + { + int duration = entityStats->getEffectActive(effectID) ? entityStats->EFFECTS_TIMERS[effectID] : 0; + duration += innerElement->duration * charge; + bool prevEffect = entityStats->getEffectActive(effectID) > 0; + if ( entity->setEffect(effectID, effectStrength, duration, false) ) + { + if ( !prevEffect ) + { + playSoundEntity(entity, 168, 128); + } + //spawnMagicEffectParticles(entity->x, entity->y, entity->z, particle); + createParticleFociLight(caster, spell->ID, true); + messagePlayerColor(entity->isEntityPlayer(), MESSAGE_STATUS, makeColorRGB(0, 255, 0), + Language::get(langEntry)); + } + } + } + } + } + createRadiusMagic(spell->ID, caster, + caster->x, caster->y, radius, TICKS_PER_SECOND, nullptr); + playSoundEntity(caster, 168, 128); + } + + { + // charging up the spell, set status effect on caster + if ( Stat* casterStats = caster->getStats() ) + { + if ( innerElement ) + { + int duration = casterStats->getEffectActive(effectID) ? casterStats->EFFECTS_TIMERS[effectID] : 0; + duration += innerElement->duration; + + duration = std::max(innerElement->duration * charge, duration); + + static ConsoleVariable cvar_foci_light_debug("/foci_light_debug", false); + if ( *cvar_foci_light_debug ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_DEBUG, "Duration: %.2f", duration / (real_t)TICKS_PER_SECOND); + } + + bool prevEffect = casterStats->getEffectActive(effectID) > 0; + if ( caster->setEffect(effectID, effectStrength, duration, false) ) + { + /*if ( !prevEffect ) + { + playSoundEntity(caster, 171, 128); + }*/ + if ( !prevEffect ) + { + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, makeColorRGB(0, 255, 0), + Language::get(langEntry)); + } + //spawnMagicEffectParticles(caster->x, caster->y, caster->z, particle); + createParticleFociLight(caster, spell->ID, true); + } + } + } + } + } + } + } } } - else if ( !strcmp(element->element_internal_name, spellElement_reflectMagic.element_internal_name) ) + else if ( spell->ID == SPELL_IGNITE ) { - if ( caster->behavior == &actMonster ) - { - caster->setEffect(EFF_MAGICREFLECT, true, 600, true); - playSoundEntity(caster, 166, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); - } - else + if ( caster ) { - //TODO: Refactor into a function that adds magic_effects. Invisibility also makes use of this. - //Also refactor the duration determining code. - int duration = element->duration; - //duration += (((element->mana + extramagic_to_use) - element->base_mana) / static_cast(element->overload_multiplier)) * element->duration; - node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); - spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. - channeled_spell = (spell_t*)(spellnode->element); - channeled_spell->magic_effects_node = spellnode; - spellnode->size = sizeof(spell_t); - ((spell_t*)spellnode->element)->caster = caster->getUID(); - spellnode->deconstructor = &spellDeconstructor; - //if ( newbie ) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if ( chance >= spellcasting / 10 ) - // { - // duration -= local_rng.rand() % (1000 / (spellcasting + 1)); - // } - // if ( duration < 180 ) - // { - // duration = 180; //Range checking. - // } - //} - duration /= getCostOfSpell((spell_t*)spellnode->element); - channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. - caster->setEffect(EFF_MAGICREFLECT, true, duration, true); - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( Entity* spellTimer = createParticleIgnite(caster) ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_IGNITE, 0); + } + + if ( Stat* casterStats = caster->getStats() ) + { + if ( casterStats->getEffectActive(EFF_FLAME_CLOAK) ) { - serverUpdateEffects(i); + if ( caster->flags[BURNABLE] && caster->SetEntityOnFire(nullptr) ) + { + casterStats->burningInflictedBy = 0; + } } } - playSoundEntity(caster, 166, 128 ); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 2207); + playSoundEntity(caster, 164, 128); } } - else if ( !strcmp(element->element_internal_name, spellElement_amplifyMagic.element_internal_name) ) + else if ( spell->ID == SPELL_SHATTER_OBJECTS ) { - if ( caster->behavior == &actMonster ) - { - caster->setEffect(EFF_MAGICAMPLIFY, true, 600, true); - playSoundEntity(caster, 166, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); - } - else + if ( caster ) { - //TODO: Refactor into a function that adds magic_effects. Invisibility also makes use of this. - //Also refactor the duration determining code. - int duration = element->duration; - //duration += (((element->mana + extramagic_to_use) - element->base_mana) / static_cast(element->overload_multiplier)) * element->duration; - node_t* spellnode = list_AddNodeLast(&caster->getStats()->magic_effects); - spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. - channeled_spell = (spell_t*)(spellnode->element); - channeled_spell->magic_effects_node = spellnode; - spellnode->size = sizeof(spell_t); - ((spell_t*)spellnode->element)->caster = caster->getUID(); - spellnode->deconstructor = &spellDeconstructor; - //if ( newbie ) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if ( chance >= spellcasting / 10 ) - // { - // duration -= local_rng.rand() % (1000 / (spellcasting + 1)); - // } - // if ( duration < 180 ) - // { - // duration = 180; //Range checking. - // } - //} - duration /= getCostOfSpell((spell_t*)spellnode->element); - channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. - caster->setEffect(EFF_MAGICAMPLIFY, true, duration, true); - for ( int i = 0; i < MAXPLAYERS; ++i ) + if ( Entity* spellTimer = createParticleShatterObjects(caster) ) { - if ( players[i] && caster && (caster == players[i]->entity) ) + if ( spellBookBonusPercent > 0 ) { - serverUpdateEffects(i); - messagePlayer(i, MESSAGE_PROGRESSION, Language::get(3442)); + spellTimer->actmagicSpellbookBonus = spellBookBonusPercent; } + spellTimer->actmagicFromSpellbook = usingSpellbook ? 1 : 0; + serverSpawnMiscParticles(caster, PARTICLE_EFFECT_SHATTER_OBJECTS, 0); } - playSoundEntity(caster, 166, 128); - spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 171); + playSoundEntity(caster, 167, 128); } } - else if ( !strcmp(element->element_internal_name, spellElement_vampiricAura.element_internal_name) ) + else if ( spell->ID == SPELL_PROJECT_SPIRIT ) { - if ( caster->behavior == &actMonster ) + if ( caster && caster->behavior == &actDeathGhost ) { - createParticleDropRising(caster, 600, 0.7); - serverSpawnMiscParticles(caster, PARTICLE_EFFECT_VAMPIRIC_AURA, 600); - caster->getStats()->EFFECTS[EFF_VAMPIRICAURA] = true; - caster->getStats()->EFFECTS_TIMERS[EFF_VAMPIRICAURA] = 600; + if ( duckAreaQuck(caster) ) + { + if ( caster->skill[2] >= 0 && caster->skill[2] < MAXPLAYERS ) + { + if ( players[caster->skill[2]]->entity ) + { + magicOnSpellCastEvent(players[caster->skill[2]]->entity, players[caster->skill[2]]->entity, nullptr, spell->ID, + spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + } + } } - else if ( caster->behavior == &actPlayer ) + else if ( caster && caster->behavior == &actPlayer ) { - channeled_spell = spellEffectVampiricAura(caster, spell, extramagic_to_use); + node_t* nextnode = nullptr; + for ( auto node = map.entities->first; node; node = nextnode ) + { + nextnode = node->next; + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->behavior == &actDeathGhost && entity->skill[2] == caster->skill[2] ) + { + entity->removeLightField(); + list_RemoveNode(entity->mynode); + } + } + } + if ( Stat* casterStats = caster->getStats() ) + { + if ( !casterStats->getEffectActive(EFF_PROJECT_SPIRIT) ) + { + if ( caster->setEffect(EFF_PROJECT_SPIRIT, true, element->duration, false) ) + { + messagePlayer(caster->skill[2], MESSAGE_STATUS, Language::get(6874)); + if ( players[caster->skill[2]]->isLocalPlayer() ) + { + players[caster->skill[2]]->ghost.initTeleportLocations(caster->x / 16, caster->y / 16); + players[caster->skill[2]]->ghost.spawnGhost(); + players[caster->skill[2]]->entity->skill[3] = 2; + } + else + { + strcpy((char*)net_packet->data, "PROJ"); + net_packet->address.host = net_clients[caster->skill[2] - 1].host; + net_packet->address.port = net_clients[caster->skill[2] - 1].port; + net_packet->len = 4; + sendPacketSafe(net_sock, -1, net_packet, caster->skill[2] - 1); + } + } + } + } } - //Also refactor the duration determining code. } - else if ( !strcmp(element->element_internal_name, spellElement_slime_spray.element_internal_name) ) + else if ( spell->ID == SPELL_SIGIL ) { - int particle = -1; - switch ( spell->ID ) + if ( caster ) { - case SPELL_SLIME_ACID: - particle = 180; - break; - case SPELL_SLIME_WATER: - particle = 181; - break; - case SPELL_SLIME_FIRE: - particle = 182; - break; - case SPELL_SLIME_TAR: - particle = 183; - break; - case SPELL_SLIME_METAL: - particle = 184; - break; - default: - break; + bool found = false; + bool effect = false; + if ( castSpellProps ) + { + if ( Entity* fx = createRadiusMagic(SPELL_SIGIL, caster, + castSpellProps->target_x, castSpellProps->target_y, 32, + getSpellEffectDurationFromID(SPELL_SIGIL, caster, nullptr, caster), nullptr) ) + { + playSoundEntity(fx, 167, 128); + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6952)); + } + } + /*if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + }*/ + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + playSoundEntity(caster, 167, 64); } - if ( particle >= 0 ) + } + else if ( spell->ID == SPELL_SANCTUARY ) + { + if ( caster ) { - Entity* spellTimer = createParticleTimer(caster, 30, -1); - spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_MAGIC_SPRAY; - spellTimer->particleTimerCountdownSprite = particle; - result = spellTimer; - if ( !(caster && caster->behavior == &actMonster && caster->getStats() && caster->getStats()->type == SLIME) ) + bool found = false; + bool effect = false; + if ( castSpellProps ) { - // spawn these if not a slime doing its special attack, client spawns own particles - serverSpawnMiscParticles(caster, PARTICLE_EFFECT_SLIME_SPRAY, particle); + if ( Entity* fx = createRadiusMagic(SPELL_SANCTUARY, caster, + castSpellProps->target_x, castSpellProps->target_y, 32, + getSpellEffectDurationFromID(SPELL_SANCTUARY, caster, nullptr, caster), nullptr) ) + { + playSoundEntity(fx, 167, 128); + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6953)); + } } + /*if ( !found ) + { + messagePlayer(caster->isEntityPlayer(), MESSAGE_HINT, Language::get(6498)); + }*/ + spawnMagicEffectParticles(caster->x, caster->y, caster->z, 174); + playSoundEntity(caster, 167, 64); } } - + // intentional separate from else/if chain. // disables propulsion if found a marked target. if ( !strcmp(spell->spell_internal_name, spell_telePull.spell_internal_name) ) @@ -2097,7 +8088,12 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool if ( entityToTeleport ) { propulsion = 0; - spellEffectTeleportPull(nullptr, *element, caster, entityToTeleport, 0); + if ( spellEffectTeleportPull(nullptr, *element, caster, entityToTeleport, 0) ) + { + magicOnEntityHit(caster, caster, entityToTeleport, entityToTeleport->getStats(), 0, 0, 0, spell ? spell->ID : SPELL_NONE); + magicOnSpellCastEvent(caster, caster, entityToTeleport, + SPELL_SHADOW_TAG, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + } } } } @@ -2116,7 +8112,7 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool missileEntity->flags[UPDATENEEDED] = true; missileEntity->flags[PASSABLE] = true; missileEntity->behavior = &actMagicMissile; - double missile_speed = 4 * (element->mana / static_cast(element->overload_multiplier)); //TODO: Factor in base mana cost? + double missile_speed = 4; missileEntity->vel_x = cos(missileEntity->yaw) * (missile_speed); missileEntity->vel_y = sin(missileEntity->yaw) * (missile_speed); @@ -2134,8 +8130,117 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool missileEntity->actmagicSpellbookBonus = spellBookBonusPercent; } } + + if ( spell->ID == SPELL_METEOR && castSpellProps ) + { + missile_speed = 3.0; + real_t yaw = missileEntity->yaw; + int delayMove = TICKS_PER_SECOND; + if ( innerElement && !strcmp(innerElement->element_internal_name, "spell_element_flames") ) + { + int spread = 5; + missile_speed = 3.0 + ((spread - (local_rng.rand() % (spread * 2 + 1))) / 5.0); + yaw += ((spread - (local_rng.rand() % (spread * 2 + 1))) / 5.0) * PI / 64; + missileEntity->actmagicNoHitMessage = 1; + delayMove = std::max(0, 10 * (castSpellProps->elementIndex - 1)); + } + real_t spellDistance = sqrt(pow(castSpellProps->caster_x - castSpellProps->target_x, 2) + + pow(castSpellProps->caster_y - castSpellProps->target_y, 2)); + spellDistance += 4.0; // add a little distance + missile_speed *= spellDistance / 64.0; + missileEntity->vel_x = cos(yaw) * (missile_speed); + missileEntity->vel_y = sin(yaw) * (missile_speed); + + missileEntity->focalz = 0.5; + + real_t startZ = -16.0; + missileEntity->vel_z = -(startZ - 7.5) / (spellDistance / 3.0); + missileEntity->actmagicIsVertical = MAGIC_ISVERTICAL_XYZ; + missileEntity->z = startZ; + missileEntity->pitch = atan2(missileEntity->vel_z, missile_speed); + + if ( delayMove > 0 ) + { + missileEntity->flags[INVISIBLE] = true; + missileEntity->flags[UPDATENEEDED] = false; + missileEntity->actmagicDelayMove = delayMove; + missileEntity->actmagicVelXStore = missileEntity->vel_x; + missileEntity->actmagicVelYStore = missileEntity->vel_y; + missileEntity->actmagicVelZStore = missileEntity->vel_z; + missileEntity->vel_x = 0.0; + missileEntity->vel_y = 0.0; + missileEntity->vel_z = 0.0; + } + } + else if ( spell->ID == SPELL_METEOR_SHOWER && castSpellProps ) + { + missile_speed = 3.0; + real_t yaw = missileEntity->yaw; + if ( innerElement && !strcmp(innerElement->element_internal_name, "spell_element_flames") ) + { + int spread = 5; + missile_speed = 3.0 + ((spread - (local_rng.rand() % (spread * 2 + 1))) / 5.0); + yaw += ((spread - (local_rng.rand() % (spread * 2 + 1))) / 5.0) * PI / 64; + missileEntity->actmagicNoHitMessage = 1; + } + else + { + int spread = 5; + missile_speed = 3.0 + ((spread - (local_rng.rand() % (spread * 2 + 1))) / 5.0); + yaw += ((spread - (local_rng.rand() % (spread * 2 + 1))) / 5.0) * PI / 64; + } + + int delayMove = 0; + if ( castSpellProps->elementIndex == 0 ) + { + delayMove += (TICKS_PER_SECOND) * ((castSpellProps->elementIndex + 6) / 2); + } + else + { + delayMove += (TICKS_PER_SECOND) * ((castSpellProps->elementIndex - 1) / 2); + if ( (castSpellProps->elementIndex - 1) % 2 == 1 ) + { + delayMove += 30; + } + } + real_t spellDistance = sqrt(pow(castSpellProps->caster_x - castSpellProps->target_x, 2) + + pow(castSpellProps->caster_y - castSpellProps->target_y, 2)); + spellDistance += 4.0; // add a little distance + missile_speed *= spellDistance / 64.0; + + real_t speedScale = 1.0; + if ( castSpellProps->elementIndex == 0 ) + { + speedScale = 0.5; + } + + missileEntity->vel_x = cos(yaw) * (missile_speed) * speedScale; + missileEntity->vel_y = sin(yaw) * (missile_speed) * speedScale; + + missileEntity->focalz = 0.5; + + real_t startZ = -16.0; + missileEntity->vel_z = speedScale * (-(startZ - 7.5) / (spellDistance / 3.0)); + missileEntity->actmagicIsVertical = MAGIC_ISVERTICAL_XYZ; + missileEntity->z = startZ; + missileEntity->pitch = atan2(missileEntity->vel_z, missile_speed); + + if ( delayMove > 0 ) + { + missileEntity->flags[INVISIBLE] = true; + missileEntity->flags[UPDATENEEDED] = false; + missileEntity->actmagicDelayMove = delayMove; + missileEntity->actmagicVelXStore = missileEntity->vel_x; + missileEntity->actmagicVelYStore = missileEntity->vel_y; + missileEntity->actmagicVelZStore = missileEntity->vel_z; + missileEntity->vel_x = 0.0; + missileEntity->vel_y = 0.0; + missileEntity->vel_z = 0.0; + } + } + Stat* casterStats = caster->getStats(); - if ( !trap && !using_magicstaff && casterStats && casterStats->EFFECTS[EFF_MAGICAMPLIFY] ) + if ( !trap && !using_magicstaff && casterStats && casterStats->getEffectActive(EFF_MAGICAMPLIFY) ) { if ( spell->ID == SPELL_FIREBALL || spell->ID == SPELL_COLD || spell->ID == SPELL_LIGHTNING || spell->ID == SPELL_MAGICMISSILE ) { @@ -2148,6 +8253,18 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool missileEntity->pitch = -PI / 32; } } + + if ( spell->ID == SPELL_SPORE_BOMB || spell->ID == SPELL_MYCELIUM_BOMB ) + { + missile_speed *= 0.5; + missileEntity->vel_x = cos(missileEntity->yaw) * (missile_speed); + missileEntity->vel_y = sin(missileEntity->yaw) * (missile_speed); + missileEntity->actmagicProjectileArc = 1; + missileEntity->actmagicIsVertical = MAGIC_ISVERTICAL_XYZ; + missileEntity->vel_z = -0.3; + missileEntity->pitch = -PI / 32; + } + node = list_AddNodeFirst(&missileEntity->children); node->element = copySpell(spell); ((spell_t*)node->element)->caster = caster->getUID(); @@ -2157,7 +8274,7 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool int volume = 128; // IMPORTANT - TRAP IS USED FOR STORM POTIONS AND ORBIT PARTICLES, QUIET SOUND HERE. - if ( trap && caster && (caster->behavior == &actPlayer || caster->behavior == &actMonster) ) + if ( trap && caster && (caster->behavior == &actPlayer || caster->behavior == &actMonster || caster->behavior == &actBoulder) ) { volume = 8; } @@ -2189,6 +8306,132 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool traveltime = 10; missileEntity->skill[5] = traveltime; } + if ( caster && caster->getStats() ) + { + Stat* casterStats = caster->getStats(); + if ( casterStats->type == FLAME_ELEMENTAL ) + { + traveltime = 15; + missileEntity->skill[5] = traveltime; + if ( node_t* elementNode = ((spell_t*)node->element)->elements.first ) + { + if ( auto element = (spellElement_t*)elementNode->element ) + { + if ( elementNode = element->elements.first ) + { + element = (spellElement_t*)elementNode->element; + if ( element ) + { + element->setDamage(element->getDamage() / 2); + } + } + } + } + } + if ( casterStats->type == MOTH_SMALL && casterStats->getAttribute("special_npc") == "fire sprite" ) + { + if ( node_t* elementNode = ((spell_t*)node->element)->elements.first ) + { + if ( auto element = (spellElement_t*)elementNode->element ) + { + if ( elementNode = element->elements.first ) + { + if ( element = (spellElement_t*)elementNode->element ) + { + element->setDamage(std::max(1, casterStats->INT)); + } + if ( Entity* leader = caster->monsterAllyGetPlayerLeader() ) + { + element->duration = getSpellEffectDurationFromID(SPELL_FIRE_SPRITE, leader, nullptr, nullptr); + } + } + } + } + } + + if ( casterStats->type == SHOPKEEPER && spell->ID == SPELL_BLEED ) + { + if ( node_t* elementNode = ((spell_t*)node->element)->elements.first ) + { + if ( auto element = (spellElement_t*)elementNode->element ) + { + if ( elementNode = element->elements.first ) + { + if ( element = (spellElement_t*)elementNode->element ) + { + element->setDamage(element->getDamage() * 2); + } + } + } + } + } + + if ( spell->ID == SPELL_BLOOD_WAVES ) + { + if ( node_t* elementNode = ((spell_t*)node->element)->elements.first ) + { + if ( auto element = (spellElement_t*)elementNode->element ) + { + if ( elementNode = element->elements.first ) + { + element = (spellElement_t*)elementNode->element; + if ( element ) + { + int ratioINT = getSpellDamageSecondaryFromID(SPELL_BLOOD_WAVES, caster, casterStats, missileEntity); + element->setDamage(element->getDamage() + std::max(1, ratioINT * statGetINT(casterStats, caster))); + } + } + } + } + + playSoundEntity(caster, 28, 128); + real_t hpLoss = getSpellEffectDurationSecondaryFromID(SPELL_BLOOD_WAVES, caster, casterStats, missileEntity); + if ( caster->behavior == &actPlayer ) + { + int playerhit = caster->skill[2]; + if ( playerhit > 0 && multiplayer == SERVER && !players[playerhit]->isLocalPlayer() ) + { + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 20; // turns into .1 + net_packet->data[5] = 20; + net_packet->address.host = net_clients[playerhit - 1].host; + net_packet->address.port = net_clients[playerhit - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, playerhit - 1); + } + else if ( playerhit == 0 || (splitscreen && playerhit > 0) ) + { + cameravars[playerhit].shakex += 0.2; + cameravars[playerhit].shakey += 20; + } + } + + int hpLossLimit = casterStats->MAXHP * getSpellEffectDurationSecondaryFromID(SPELL_BLOOD_WAVES, caster, casterStats, missileEntity) / 100.0; + int healthLoss = (std::max(1.0, casterStats->MAXHP * hpLoss / 100.0)); + if ( hpLossLimit > 0 ) + { + int reduceHealthLoss = (casterStats->HP - healthLoss - casterStats->MAXHP / 10); + if ( reduceHealthLoss < 0 ) + { + healthLoss += reduceHealthLoss; + } + } + if ( healthLoss > 0 ) + { + caster->modHP(-healthLoss); + if ( casterStats->sex == MALE ) + { + caster->setObituary(Language::get(1528)); + casterStats->killer = KilledBy::FAILED_INVOCATION; + } + else + { + caster->setObituary(Language::get(1529)); + casterStats->killer = KilledBy::FAILED_INVOCATION; + } + } + } + } int sound = spellGetCastSound(spell); if ( volume > 0 && sound > 0 ) @@ -2206,6 +8449,25 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool missileEntity->z = ceilingModel->z; } } + + if ( missileEntity && (!strcmp(spell->spell_internal_name, spell_telePull.spell_internal_name) + || !strcmp(spell->spell_internal_name, spell_shadowTag.spell_internal_name)) ) + { + missileEntity->actmagicAllowFriendlyFireHit = 1; + } + + if ( caster->behavior == &actMonster && missileEntity && !trap ) + { + if ( Stat* casterStats = caster->getStats() ) + { + int accuracy = casterStats->monsterRangedAccuracy.getAccuracy(caster->monsterTarget); + if ( accuracy > 0 ) + { + casterStats->monsterRangedAccuracy.modifyProjectile(*caster, *missileEntity); + } + casterStats->monsterRangedAccuracy.incrementAccuracy(); + } + } } else if ( propulsion == PROPULSION_MISSILE_TRIO ) { @@ -2248,7 +8510,7 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool missileEntity->behavior = &actMagicMissile; missileEntity->sprite = sprite; - double missile_speed = baseSpeed * (element->mana / static_cast(element->overload_multiplier)); //TODO: Factor in base mana cost? + double missile_speed = baseSpeed; missileEntity->vel_x = cos(missileEntity->yaw) * (missile_speed); missileEntity->vel_y = sin(missileEntity->yaw) * (missile_speed); @@ -2289,7 +8551,7 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool entity1->behavior = &actMagicMissile; entity1->sprite = sprite; - missile_speed = baseSideSpeed * (element->mana / static_cast(element->overload_multiplier)); //TODO: Factor in base mana cost? + missile_speed = baseSideSpeed; entity1->vel_x = cos(entity1->yaw) * (missile_speed); entity1->vel_y = sin(entity1->yaw) * (missile_speed); @@ -2326,7 +8588,7 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool entity2->behavior = &actMagicMissile; entity2->sprite = sprite; - missile_speed = baseSideSpeed * (element->mana / static_cast(element->overload_multiplier)); //TODO: Factor in base mana cost? + missile_speed = baseSideSpeed; entity2->vel_x = cos(entity2->yaw) * (missile_speed); entity2->vel_y = sin(entity2->yaw) * (missile_speed); @@ -2349,427 +8611,547 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool ((spell_t*)node->element)->caster = caster->getUID(); node->deconstructor = &spellDeconstructor; node->size = sizeof(spell_t); - } - extramagic_to_use = 0; - if (extramagic > 0) - { - //Extra magic. Pump it in here? - chance = local_rng.rand() % 5; - if (chance == 1) + if ( spell->ID == SPELL_ACID_SPRAY ) { - //Use some of that extra magic in this element. - int amount = local_rng.rand() % extramagic; - extramagic -= amount; - extramagic_to_use += amount; //TODO: Make the elements here use this? Looks like they won't, currently. Oh well. + if ( missileEntity ) { missileEntity->actmagicUpdateOLDHPOnHit = 1; } + if ( entity1 ) { entity1->actmagicUpdateOLDHPOnHit = 1; } + if ( entity2 ) { entity2->actmagicUpdateOLDHPOnHit = 1; } + } + + if ( caster->behavior == &actMonster && missileEntity && !trap ) + { + if ( Stat* casterStats = caster->getStats() ) + { + int accuracy = casterStats->monsterRangedAccuracy.getAccuracy(caster->monsterTarget); + if ( accuracy > 0 ) + { + casterStats->monsterRangedAccuracy.modifyProjectile(*caster, *missileEntity); + } + casterStats->monsterRangedAccuracy.incrementAccuracy(); + } } } + //TODO: Add the status/conditional elements/modifiers (probably best as elements) too. Like onCollision or something. - //element = (spellElement_t *)element->elements->first->element; - node = element->elements.first; - if ( node ) + if ( innerElement ) { - element = (spellElement_t*)node->element; - if (!strcmp(element->element_internal_name, spellElement_force.element_internal_name)) + if (!strcmp(innerElement->element_internal_name, spellElement_force.element_internal_name)) { //Give the spell force properties. if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 173; } - - // !-- DONT MODIFY element->damage since this affects every subsequent spellcast, removing this aspect as unnecessary 05/04/22 - //if (newbie) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if (chance >= spellcasting / 10) - // { - // element->damage -= local_rng.rand() % (100 / (spellcasting + 1)); - // } - // if (element->damage < 10) - // { - // element->damage = 10; //Range checking. - // } - //} } - else if (!strcmp(element->element_internal_name, spellElement_fire.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_fire.element_internal_name)) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 168; - //missileEntity->skill[4] = missileEntity->x; //Store what x it started shooting out from the player at. - //missileEntity->skill[5] = missileEntity->y; //Store what y it started shooting out from the player at. - //missileEntity->skill[12] = (100 * stat->PROFICIENCIES[PRO_SPELLCASTING]) + (100 * stat->PROFICIENCIES[PRO_MAGIC]) + (100 * (local_rng.rand()%10)) + (10 * (local_rng.rand()%10)) + (local_rng.rand()%10); //How long this thing lives. - - //playSoundEntity( entity, 59, 128 ); } - // !-- DONT MODIFY element->damage since this affects every subsequent spellcast, removing this aspect as unnecessary 05/04/22 - //if (newbie) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if (chance >= spellcasting / 10) - // { - // element->damage -= local_rng.rand() % (100 / (spellcasting + 1)); - // } - // if (element->damage < 10) - // { - // element->damage = 10; //Range checking. - // } - //} } - else if ( !strcmp(element->element_internal_name, spellElement_lightning.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_lightning.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 170; } - // !-- DONT MODIFY element->damage since this affects every subsequent spellcast, removing this aspect as unnecessary 05/04/22 - //if ( newbie ) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // chance = local_rng.rand() % 10; - // if ( chance >= spellcasting / 10 ) - // { - // element->damage -= local_rng.rand() % (100 / (spellcasting + 1)); - // } - // if ( element->damage < 10 ) - // { - // element->damage = 10; //Range checking. - // } - //} } - else if ( !strcmp(element->element_internal_name, spellElement_stoneblood.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_stoneblood.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 170; } } - else if ( !strcmp(element->element_internal_name, spellElement_ghostBolt.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_ghostBolt.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 1244; } } - else if ( !strcmp(element->element_internal_name, spellElement_bleed.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_bleed.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 643; } } - else if (!strcmp(element->element_internal_name, spellElement_confuse.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_confuse.element_internal_name)) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 173; } } - else if (!strcmp(element->element_internal_name, spellElement_cold.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_cold.element_internal_name)) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 172; } } - else if (!strcmp(element->element_internal_name, spellElement_dig.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_dig.element_internal_name)) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 171; } } - else if (!strcmp(element->element_internal_name, spellElement_locking.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_locking.element_internal_name) + || spell->ID == SPELL_SPLINTER_GEAR ) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 171; } } - else if (!strcmp(element->element_internal_name, spellElement_opening.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_opening.element_internal_name)) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 171; } } - else if (!strcmp(element->element_internal_name, spellElement_slow.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_slow.element_internal_name)) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 171; } } - else if ( !strcmp(element->element_internal_name, spellElement_poison.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_poison.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 597; } } - else if (!strcmp(element->element_internal_name, spellElement_sleep.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_sleep.element_internal_name)) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 172; } } - else if (!strcmp(element->element_internal_name, spellElement_magicmissile.element_internal_name)) + else if (!strcmp(innerElement->element_internal_name, spellElement_magicmissile.element_internal_name)) { if (propulsion == PROPULSION_MISSILE) { missileEntity->sprite = 173; } } - else if ( !strcmp(element->element_internal_name, spellElement_dominate.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_dominate.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { - missileEntity->sprite = 168; + missileEntity->sprite = 2189; } } - else if ( !strcmp(element->element_internal_name, spellElement_acidSpray.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_acidSpray.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 171; } } - else if ( !strcmp(element->element_internal_name, spellElement_stealWeapon.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_stealWeapon.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 175; } } - else if ( !strcmp(element->element_internal_name, spellElement_drainSoul.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_drainSoul.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 598; } } - else if ( !strcmp(element->element_internal_name, spellElement_charmMonster.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_charmMonster.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 173; } } - else if ( !strcmp(element->element_internal_name, spellElement_telePull.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_telePull.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 175; } } - else if ( !strcmp(element->element_internal_name, spellElement_shadowTag.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_shadowTag.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 175; } } - else if ( !strcmp(element->element_internal_name, spellElement_demonIllusion.element_internal_name) ) + else if ( !strcmp(innerElement->element_internal_name, spellElement_demonIllusion.element_internal_name) ) { if ( propulsion == PROPULSION_MISSILE ) { missileEntity->sprite = 171; } } - } - } - - //Random chance to level up spellcasting skill. - if ( !trap ) - { - if ( player >= 0 ) - { - if ( !using_magicstaff ) + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_MERCURY_BOLT].element_internal_name) ) { - if ( !usingSpellbook ) + if ( propulsion == PROPULSION_MISSILE ) { - Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_SPELL_CASTS_RUN, "memorized", 1); - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELL_CASTS, SPELL_ITEM, 1, false, spell->ID); + missileEntity->sprite = 1799; } - else if ( usingSpellbook ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_LEAD_BOLT].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( items[spellbookType].category == SPELLBOOK ) - { - Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_SPELLBOOK_CASTS_RUN, "spellbook casting", 1); - Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELLBOOK_CASTS, spellbookType, 1); - - if ( items[spellbookType].hasAttribute("SPELLBOOK_CAST_BONUS") ) - { - Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_PWR_MAX_SPELLBOOK, "pwr", spellBookBonusPercent); - } - } + missileEntity->sprite = 1798; } } - } - if ( using_magicstaff ) - { - if ( stat ) + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_SHADE_BOLT].element_internal_name) ) { - // spellcasting increase chances. - if ( stat->getProficiency(PRO_SPELLCASTING) < 60 ) + if ( propulsion == PROPULSION_MISSILE ) { - if ( local_rng.rand() % 6 == 0 ) //16.67% - { - caster->increaseSkill(PRO_SPELLCASTING); - } + missileEntity->sprite = 1801; } - else if ( stat->getProficiency(PRO_SPELLCASTING) < 80 ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_SPHERE_SILENCE].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( local_rng.rand() % 9 == 0 ) //11.11% - { - caster->increaseSkill(PRO_SPELLCASTING); - } + missileEntity->sprite = 1818; } - else // greater than 80 + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_WONDERLIGHT].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( local_rng.rand() % 12 == 0 ) //8.33% - { - caster->increaseSkill(PRO_SPELLCASTING); - } + missileEntity->sprite = 1802; } - - // magic increase chances. - if ( stat->getProficiency(PRO_SPELLCASTING) < 60 ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_SPORE_BOMB].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( local_rng.rand() % 7 == 0 ) //14.2% - { - caster->increaseSkill(PRO_MAGIC); - } + missileEntity->sprite = 1816; } - else if ( stat->getProficiency(PRO_SPELLCASTING) < 80 ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_MYCELIUM_BOMB].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( local_rng.rand() % 10 == 0 ) //10.00% - { - caster->increaseSkill(PRO_MAGIC); - } + missileEntity->sprite = 1886; + } + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_NUMBING_BOLT].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) + { + missileEntity->sprite = 2356; } - else // greater than 80 + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_DEFY_FLESH].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( local_rng.rand() % 13 == 0 ) //7.69% - { - caster->increaseSkill(PRO_MAGIC); - } + missileEntity->sprite = 2361; } } - } - else - { - if ( stat ) + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_PSYCHIC_SPEAR].element_internal_name) ) { - int spellCastChance = 5; // 20% - int magicChance = 6; // 16.67% - int castDifficulty = stat->getProficiency(PRO_SPELLCASTING) / 20 - spell->difficulty / 20; - if ( castDifficulty <= -1 ) + if ( propulsion == PROPULSION_MISSILE ) { - // spell was harder. - spellCastChance = 3; // 33% - magicChance = 3; // 33% + missileEntity->sprite = 2357; } - else if ( castDifficulty == 0 ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_INCOHERENCE].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - // spell was same level - spellCastChance = 3; // 33% - magicChance = 4; // 25% + missileEntity->sprite = 2355; } - else if ( castDifficulty == 1 ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElement_weakness.element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - // spell was easy. - spellCastChance = 4; // 25% - magicChance = 5; // 20% + missileEntity->sprite = 2367; } - else if ( castDifficulty > 1 ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_BLOOD_WAVES].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - // piece of cake! - spellCastChance = 6; // 16.67% - magicChance = 7; // 14.2% + missileEntity->flags[INVISIBLE] = true; + missileEntity->sprite = 2364; + + missileEntity->skill[5] *= 2; // double lifetime as half speed + missileEntity->scalex = 0.5; + missileEntity->scaley = missileEntity->scalex; + missileEntity->scalez = missileEntity->scalex; + missileEntity->vel_x *= 0.5; + missileEntity->vel_y *= 0.5; } - if ( usingSpellbook && !playerCastingFromKnownSpellbook ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_HOLY_BEAM].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - spellCastChance *= 2; - magicChance *= 2; + //missileEntity->flags[INVISIBLE] = true; + missileEntity->sprite = 2407; + + //missileEntity->skill[5] *= 5; // double lifetime as half speed + missileEntity->actmagicAllowFriendlyFireHit = 1; + missileEntity->scalex = 1.0; + missileEntity->scaley = missileEntity->scalex; + missileEntity->scalez = missileEntity->scalex; + missileEntity->vel_x *= 1.0; + missileEntity->vel_y *= 1.0; } - //messagePlayer(0, "Difficulty: %d, chance 1 in %d, 1 in %d", castDifficulty, spellCastChance, magicChance); - if ( (!strcmp(element->element_internal_name, spellElement_light.element_internal_name) || spell->ID == SPELL_REVERT_FORM) ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_SPIN].element_internal_name) + || !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_DIZZY].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( stat->getProficiency(PRO_SPELLCASTING) >= SKILL_LEVEL_SKILLED ) - { - spellCastChance = 0; - } - if ( stat->getProficiency(PRO_MAGIC) >= SKILL_LEVEL_SKILLED ) - { - magicChance = 0; - } - - // light provides no levelling past 40 in both spellcasting and magic. - if ( (magicChance == 0 && spellCastChance == 0) && local_rng.rand() % 20 == 0 ) - { - for ( int i = 0; i < MAXPLAYERS; ++i ) - { - if ( players[i] && caster && (caster == players[i]->entity) ) - { - messagePlayer(i, MESSAGE_HINT, Language::get(2591)); - } - } - } + missileEntity->sprite = 1856; } - - bool sustainedChance = players[caster->skill[2]]->mechanics.sustainedSpellLevelChance(); - if ( spellCastChance > 0 && (local_rng.rand() % spellCastChance == 0) ) + } + else if ( !strcmp(innerElement->element_internal_name, "spell_element_flames") ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( sustainedSpell && caster->behavior == &actPlayer ) - { - if ( sustainedChance ) - { - players[caster->skill[2]]->mechanics.sustainedSpellMPUsed = 0; - - caster->increaseSkill(PRO_SPELLCASTING); - } - } - else - { - caster->increaseSkill(PRO_SPELLCASTING); - } + missileEntity->sprite = 233; + missileEntity->flags[SPRITE] = true; } - - bool magicIncreased = false; - if ( magicChance > 0 && (local_rng.rand() % magicChance == 0) ) + } + else if ( !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_METEOR].element_internal_name) + || !strcmp(innerElement->element_internal_name, spellElementMap[SPELL_METEOR_SHOWER].element_internal_name) ) + { + if ( propulsion == PROPULSION_MISSILE ) { - if ( sustainedSpell && caster->behavior == &actPlayer ) - { - if ( sustainedChance ) - { - players[caster->skill[2]]->mechanics.sustainedSpellMPUsed = 0; + missileEntity->sprite = 2209; + missileEntity->scalex = 0.75; + missileEntity->scaley = missileEntity->scalex; + missileEntity->scalez = missileEntity->scalex; + } + } + else if ( !strcmp(innerElement->element_internal_name, "spell_element_scepter_blast") ) + { + if ( propulsion == PROPULSION_MISSILE ) + { + missileEntity->skill[5] *= 2; // double lifetime as half speed + missileEntity->sprite = 2191; + missileEntity->scalex = 0.5; + missileEntity->scaley = missileEntity->scalex; + missileEntity->scalez = missileEntity->scalex; + missileEntity->vel_x *= 0.5; + missileEntity->vel_y *= 0.5; + } + } + } + } - caster->increaseSkill(PRO_MAGIC); // otherwise you will basically never be able to learn all the spells in the game... - magicIncreased = true; - } - } - else - { - caster->increaseSkill(PRO_MAGIC); // otherwise you will basically never be able to learn all the spells in the game... - magicIncreased = true; - } + if ( !trap ) + { + if ( player >= 0 ) + { + if ( !using_magicstaff ) + { + if ( !usingSpellbook ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_SPELL_CASTS_RUN, "memorized", 1); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELL_CASTS, SPELL_ITEM, 1, false, spell->ID); } - if ( magicIncreased && usingSpellbook && caster->behavior == &actPlayer ) + else if ( usingSpellbook ) { - if ( stats[caster->skill[2]] && stats[caster->skill[2]]->playerRace == RACE_INSECTOID && stats[caster->skill[2]]->stat_appearance == 0 ) + if ( items[spellbookType].category == SPELLBOOK ) { - steamStatisticUpdateClient(caster->skill[2], STEAM_STAT_BOOKWORM, STEAM_STAT_INT, 1); + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_CLASS_SPELLBOOK_CASTS_RUN, "spellbook casting", 1); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_SPELLBOOK_CASTS, spellbookType, 1); + + if ( items[spellbookType].hasAttribute("SPELLBOOK_CAST_BONUS") ) + { + Compendium_t::Events_t::eventUpdateCodex(player, Compendium_t::CPDM_PWR_MAX_SPELLBOOK, "pwr", spellBookBonusPercent); + } } } } } } + //Random chance to level up spellcasting skill. + // if ( !trap ) + // { + // if ( usingFoci ) + // { + + // } + // else if ( using_magicstaff ) + // { + // if ( stat ) + // { + // // spellcasting increase chances. + // if ( stat->getProficiency(PRO_LEGACY_SPELLCASTING) < 60 ) + // { + // if ( local_rng.rand() % 6 == 0 ) //16.67% + // { + // caster->increaseSkill(PRO_LEGACY_SPELLCASTING); + // } + // } + // else if ( stat->getProficiency(PRO_LEGACY_SPELLCASTING) < 80 ) + // { + // if ( local_rng.rand() % 9 == 0 ) //11.11% + // { + // caster->increaseSkill(PRO_LEGACY_SPELLCASTING); + // } + // } + // else // greater than 80 + // { + // if ( local_rng.rand() % 12 == 0 ) //8.33% + // { + // caster->increaseSkill(PRO_LEGACY_SPELLCASTING); + // } + // } + + // // magic increase chances. + // if ( stat->getProficiency(PRO_LEGACY_SPELLCASTING) < 60 ) + // { + // if ( local_rng.rand() % 7 == 0 ) //14.2% + // { + // caster->increaseSkill(PRO_LEGACY_MAGIC); + // } + // } + // else if ( stat->getProficiency(PRO_LEGACY_SPELLCASTING) < 80 ) + // { + // if ( local_rng.rand() % 10 == 0 ) //10.00% + // { + // caster->increaseSkill(PRO_LEGACY_MAGIC); + // } + // } + // else // greater than 80 + // { + // if ( local_rng.rand() % 13 == 0 ) //7.69% + // { + // caster->increaseSkill(PRO_LEGACY_MAGIC); + // } + // } + // } + // } + // else + // { + // if ( stat ) + // { + // int spellCastChance = 5; // 20% + // int magicChance = 6; // 16.67% + // int castDifficulty = stat->getProficiency(PRO_LEGACY_SPELLCASTING) / 20 - spell->difficulty / 20; + // if ( castDifficulty <= -1 ) + // { + // // spell was harder. + // spellCastChance = 3; // 33% + // magicChance = 3; // 33% + // } + // else if ( castDifficulty == 0 ) + // { + // // spell was same level + // spellCastChance = 3; // 33% + // magicChance = 4; // 25% + // } + // else if ( castDifficulty == 1 ) + // { + // // spell was easy. + // spellCastChance = 4; // 25% + // magicChance = 5; // 20% + // } + // else if ( castDifficulty > 1 ) + // { + // // piece of cake! + // spellCastChance = 6; // 16.67% + // magicChance = 7; // 14.2% + // } + // if ( usingSpellbook && !playerCastingFromKnownSpellbook ) + // { + // spellCastChance *= 2; + // magicChance *= 2; + // } + // //messagePlayer(0, "Difficulty: %d, chance 1 in %d, 1 in %d", castDifficulty, spellCastChance, magicChance); + // if ( (!strcmp(element->element_internal_name, spellElement_light.element_internal_name) || spell->ID == SPELL_REVERT_FORM) ) + // { + // if ( stat->getProficiency(PRO_LEGACY_SPELLCASTING) >= SKILL_LEVEL_SKILLED ) + // { + // spellCastChance = 0; + // } + // if ( stat->getProficiency(PRO_LEGACY_MAGIC) >= SKILL_LEVEL_SKILLED ) + // { + // magicChance = 0; + // } + + // // light provides no levelling past 40 in both spellcasting and magic. + // if ( (magicChance == 0 && spellCastChance == 0) && local_rng.rand() % 20 == 0 ) + // { + // for ( int i = 0; i < MAXPLAYERS; ++i ) + // { + // if ( players[i] && caster && (caster == players[i]->entity) ) + // { + // messagePlayer(i, MESSAGE_HINT, Language::get(2591)); + // } + // } + // } + // } + + // bool sustainedChance = players[caster->skill[2]]->mechanics.sustainedSpellLevelChance(); + // if ( spellCastChance > 0 && (local_rng.rand() % spellCastChance == 0) ) + // { + // if ( sustainedSpell && caster->behavior == &actPlayer ) + // { + // if ( sustainedChance ) + // { + // players[caster->skill[2]]->mechanics.sustainedSpellMPUsed = 0; + + // caster->increaseSkill(PRO_LEGACY_SPELLCASTING); + // } + // } + // else + // { + // caster->increaseSkill(PRO_LEGACY_SPELLCASTING); + // } + // } + + // bool magicIncreased = false; + // if ( magicChance > 0 && (local_rng.rand() % magicChance == 0) ) + // { + // if ( sustainedSpell && caster->behavior == &actPlayer ) + // { + // if ( sustainedChance ) + // { + // players[caster->skill[2]]->mechanics.sustainedSpellMPUsed = 0; + + // caster->increaseSkill(PRO_LEGACY_MAGIC); // otherwise you will basically never be able to learn all the spells in the game... + // magicIncreased = true; + // } + // } + // else + // { + // caster->increaseSkill(PRO_LEGACY_MAGIC); // otherwise you will basically never be able to learn all the spells in the game... + // magicIncreased = true; + // } + // } + // if ( magicIncreased && usingSpellbook && caster->behavior == &actPlayer ) + // { + // if ( stats[caster->skill[2]] && stats[caster->skill[2]]->playerRace == RACE_INSECTOID && stats[caster->skill[2]]->stat_appearance == 0 ) + // { + // steamStatisticUpdateClient(caster->skill[2], STEAM_STAT_BOOKWORM, STEAM_STAT_INT, 1); + // } + // } + // } + // } + //} + if ( !trap && usingSpellbook && stat ) // degrade spellbooks on use. { int chance = 8; @@ -2861,7 +9243,7 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } } - if (spell_isChanneled(spell) && !using_magicstaff && !trap) //TODO: What about magic traps and channeled spells? + if (spell_isChanneled(spell) && !using_magicstaff && !trap && !usingFoci ) //TODO: What about magic traps and channeled spells? { if (!channeled_spell) { @@ -2888,6 +9270,12 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool net_packet->len = 9; sendPacketSafe(net_sock, -1, net_packet, target_client - 1); } + + if ( usingSpellbook ) + { + channeled_spell->spellbook = true; + } + //Add this spell to the list of channeled spells. node = list_AddNodeLast(&channeledSpells[target_client]); node->element = channeled_spell; @@ -2897,6 +9285,50 @@ Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool } } + if ( element ) + { + if ( list_Size(&element->elements) > 1 ) // recursively cast the remaining elements + { + int index = 0; + for ( node_t* node = element->elements.first; node; node = node->next, ++index ) + { + if ( index == 0 ) { continue; } + spell_t* subSpell = copySpell(spell, index); + bool _trap = true; + if ( castSpellProps ) + { + castSpellProps->elementIndex = index; + } + castSpell(caster_uid, subSpell, using_magicstaff, _trap, usingSpellbook, castSpellProps, usingFoci); + } + } + + if ( !trap && !usingFoci && !using_magicstaff ) + { + if ( caster && players[player]->entity == caster && stats[player]->type == SALAMANDER ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_SALAMANDER_HEART); + if ( effectStrength == 2 && stats[player]->EFFECTS_TIMERS[EFF_SALAMANDER_HEART] == -1 ) + { + caster->setEffect(EFF_SALAMANDER_HEART, (Uint8)2, 5 * TICKS_PER_SECOND, true, true, true); + } + else if ( spell->ID == SPELL_BREATHE_FIRE + && !strcmp(element->element_internal_name, spellElementMap[SPELL_ELEMENT_PROPULSION_MAGIC_SPRAY].element_internal_name) ) + { + if ( /*prevMP*/stats[player]->MP >= stats[player]->MAXMP * 0.75 ) + { + if ( effectStrength != 2 && effectStrength != 1 ) + { + caster->setEffect(EFF_SALAMANDER_HEART, (Uint8)2, -1, true, true, true); + castSpell(caster_uid, getSpellFromID(SPELL_IGNITE), true, false, false); + messagePlayerColor(caster->isEntityPlayer(), MESSAGE_STATUS, makeColorRGB(0, 255, 0), Language::get(6918)); + playSoundEntity(caster, 167, 128); + } + } + } + } + } + } return result; } @@ -2922,7 +9354,8 @@ int spellGetCastSound(spell_t* spell) { return 172; } - else if ( !strcmp(spell->spell_internal_name, spell_bleed.spell_internal_name) ) + else if ( !strcmp(spell->spell_internal_name, spell_bleed.spell_internal_name) + || spell->ID == SPELL_BLOOD_WAVES ) { return 171; } @@ -2942,6 +9375,10 @@ int spellGetCastSound(spell_t* spell) { return 0; } + else if ( spell->ID == SPELL_METEOR || spell->ID == SPELL_METEOR_SHOWER ) + { + return 814; + } else { return 169; @@ -2949,9 +9386,9 @@ int spellGetCastSound(spell_t* spell) return 0; } -bool spellIsNaturallyLearnedByRaceOrClass(Entity& caster, Stat& stat, int spellID) +bool spellIsNaturallyLearnedByRaceOrClass(Entity* caster, Stat& stat, int spellID, int player) { - if ( caster.behavior != &actPlayer ) + if ( caster && caster->behavior != &actPlayer ) { return false; } @@ -2977,9 +9414,30 @@ bool spellIsNaturallyLearnedByRaceOrClass(Entity& caster, Stat& stat, int spellI { return true; } + else if ( stat.playerRace == RACE_GREMLIN && stat.stat_appearance == 0 && (spellID == SPELL_DEFACE) ) + { + return true; + } + else if ( stat.playerRace == RACE_DRYAD && stat.stat_appearance == 0 && (spellID == SPELL_THORNS || spellID == SPELL_SHRUB) ) + { + return true; + } + else if ( stat.playerRace == RACE_MYCONID && stat.stat_appearance == 0 && (spellID == SPELL_SPORES || spellID == SPELL_MUSHROOM) ) + { + return true; + } + else if ( stat.playerRace == RACE_SALAMANDER && stat.stat_appearance == 0 && (spellID == SPELL_BREATHE_FIRE) ) + { + return true; + } + else if ( stat.playerRace == RACE_GNOME && stat.stat_appearance == 0 && (spellID == SPELL_FORGE_JEWEL) ) + { + return true; + } // class specific: - int playernum = caster.skill[2]; + int playernum = caster ? caster->skill[2] : player; + if ( playernum < 0 ) { return false; } if ( client_classes[playernum] == CLASS_PUNISHER && (spellID == SPELL_TELEPULL || spellID == SPELL_DEMON_ILLUSION) ) { return true; @@ -2992,7 +9450,64 @@ bool spellIsNaturallyLearnedByRaceOrClass(Entity& caster, Stat& stat, int spellI return true; } } - else if ( stat.EFFECTS[EFF_SHAPESHIFT] ) + else if ( client_classes[playernum] == CLASS_BARD ) + { + if ( spellID == SPELL_ALTER_INSTRUMENT ) + { + return true; + } + } + else if ( client_classes[playernum] == CLASS_SAPPER ) + { + if ( spellID == SPELL_BOOBY_TRAP ) + { + return true; + } + } + else if ( client_classes[playernum] == CLASS_ARCANIST ) + { + if ( spellID == SPELL_WINDGATE ) + { + return true; + } + } + else if ( client_classes[playernum] == CLASS_MESMER ) + { + if ( spellID == SPELL_COMMAND ) + { + return true; + } + } + else if ( client_classes[playernum] == CLASS_CONJURER ) + { + if ( spellID == SPELL_SUMMON ) + { + return true; + } + } + else if ( client_classes[playernum] == CLASS_SCION ) + { + if ( spellID == SPELL_BLESS_FOOD || spellID == SPELL_EARTH_ELEMENTAL || spellID == SPELL_TELEKINESIS ) + { + return true; + } + } + else if ( client_classes[playernum] == CLASS_HERMIT ) + { + if ( spellID == SPELL_MAGICIANS_ARMOR || spellID == SPELL_DEEP_SHADE || spellID == SPELL_PROJECT_SPIRIT ) + { + return true; + } + } + else if ( client_classes[playernum] == CLASS_PALADIN ) + { + if ( spellID == SPELL_DIVINE_ZEAL ) + { + return true; + } + } + + if ( stat.getEffectActive(EFF_SHAPESHIFT) ) { switch ( spellID ) { @@ -3033,3 +9548,50 @@ bool spellIsNaturallyLearnedByRaceOrClass(Entity& caster, Stat& stat, int spellI return false; } + +void createParticleFociLight(Entity* entity, int spellID, bool updateClients) +{ + if ( !entity ) { return; } + + int sprite = 1866; + if ( updateClients ) + { + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_FOCI_LIGHT, spellID); + } + + for ( int i = 0; i < 4; ++i ) + { + Entity* fx = createParticleAestheticOrbit(entity, sprite, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_FOCI_LIGHT); + fx->yaw = i * PI / 2 + 3 * PI / 4; + fx->fskill[2] = 7.75; + fx->fskill[3] = -16.25; + fx->fskill[6] = 0.15; // yaw + fx->fskill[7] = 0.1; + fx->z = 7.75; + fx->flags[INVISIBLE] = true; + fx->scaley = 1.0; + } +} + +void createParticleFociDark(Entity* entity, int spellID, bool updateClients) +{ + if ( !entity ) { return; } + + if ( updateClients ) + { + serverSpawnMiscParticles(entity, PARTICLE_EFFECT_FOCI_DARK, spellID); + } + + for ( int i = 0; i < 4; ++i ) + { + Entity* fx = createParticleAestheticOrbit(entity, 2374, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_FOCI_DARK); + fx->yaw = i * PI / 2 + 3 * PI / 4; + fx->fskill[2] = 7.75; + fx->fskill[3] = -16.25; + fx->fskill[6] = 0.15; // yaw + fx->fskill[7] = 0.1; + fx->z = 7.75; + fx->flags[INVISIBLE] = true; + fx->scaley = 1.0; + } +} \ No newline at end of file diff --git a/src/magic/magic.cpp b/src/magic/magic.cpp index 498ef66a7..e2a1b46e1 100644 --- a/src/magic/magic.cpp +++ b/src/magic/magic.cpp @@ -26,6 +26,7 @@ #include "../mod_tools.hpp" std::map> particleTimerEmitterHitEntities; +std::map particleTimerEffects; ParticleEmitterHit_t* getParticleEmitterHitProps(Uint32 emitterUid, Entity* hitentity) { if ( emitterUid == 0 || !hitentity ) { return nullptr; } @@ -51,12 +52,12 @@ void freeSpells() { for ( auto it = allGameSpells.begin(); it != allGameSpells.end(); ++it ) { - spell_t& spell = **it; + spell_t& spell = *it->second; list_FreeAll(&spell.elements); } } -void spell_magicMap(int player) +void spell_magicMap(int player, int radius, int x, int y) { if (players[player] == nullptr || players[player]->entity == nullptr) { @@ -67,15 +68,18 @@ void spell_magicMap(int player) { //Tell the client to map the magic. strcpy((char*)net_packet->data, "MMAP"); + SDLNet_Write16(radius, &net_packet->data[4]); + SDLNet_Write16(x, &net_packet->data[6]); + SDLNet_Write16(y, &net_packet->data[8]); net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 4; + net_packet->len = 10; sendPacketSafe(net_sock, -1, net_packet, player - 1); return; } messagePlayer(player, MESSAGE_HINT, Language::get(412)); - mapLevel(player); + mapLevel(player, radius, x, y, true); } void spell_detectFoodEffectOnMap(int player) @@ -123,17 +127,14 @@ bool spellEffectDominate(Entity& my, spellElement_t& element, Entity& caster, En } //Abort if invalid creature (boss, shopkeep, etc). - if ( hitstats->type == MINOTAUR - || hitstats->type == LICH - || hitstats->type == DEVIL - || hitstats->type == SHOPKEEPER - || hitstats->type == LICH_ICE - || hitstats->type == LICH_FIRE - || hitstats->type == SHADOW + if ( hit.entity->isBossMonster() || hitstats->type == MIMIC + || hitstats->type == MINIMIMIC || hitstats->type == BAT_SMALL - || (hitstats->type == VAMPIRE && MonsterData_t::nameMatchesSpecialNPCName(*hitstats, "bram kindly")) - || (hitstats->type == COCKATRICE && !strncmp(map.name, "Cockatrice Lair", 15)) + || hitstats->type == HOLOGRAM + || hit.entity->monsterIsTinkeringCreation() + || hit.entity->monsterAllySummonRank != 0 + || (hitstats->type == INCUBUS && !strncmp(hitstats->name, "inner demon", strlen("inner demon"))) ) { Uint32 color = makeColorRGB(255, 0, 0); @@ -141,9 +142,39 @@ bool spellEffectDominate(Entity& my, spellElement_t& element, Entity& caster, En { messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(2429)); } + playSoundEntity(hit.entity, 163, 128); return false; } + if ( caster.behavior == &actPlayer ) + { + int numDominated = 0; + for ( node_t* node = stats[caster.skill[2]]->FOLLOWERS.first; node != nullptr; node = node->next ) + { + Entity* follower = nullptr; + if ( (Uint32*)(node)->element ) + { + follower = uidToEntity(*((Uint32*)(node)->element)); + } + if ( follower ) + { + Stat* followerStats = follower->getStats(); + if ( followerStats && followerStats->getAttribute("DOMINATED_CREATURE") != "" ) + { + ++numDominated; + } + } + } + + int maxDominate = getSpellDamageFromID(SPELL_DOMINATE, &caster, nullptr, &caster, my.actmagicSpellbookBonus / 100.f); + if ( numDominated >= maxDominate ) + { + messagePlayerColor(caster.isEntityPlayer(), MESSAGE_COMBAT, makeColorRGB(255, 0, 0), Language::get(6962)); + playSoundEntity(hit.entity, 163, 128); + return false; + } + } + playSoundEntity(hit.entity, 174, 64); //TODO: Dominate spell sound effect. //Make the monster a follower. @@ -166,13 +197,19 @@ bool spellEffectDominate(Entity& my, spellElement_t& element, Entity& caster, En } hit.entity->monsterAllyIndex = parent->skill[2]; - hit.entity->setEffect(EFF_CONFUSED, false, 0, false); + hit.entity->setEffect(EFF_CONFUSED, false, 0, true); if ( multiplayer == SERVER ) { serverUpdateEntitySkill(hit.entity, 42); // update monsterAllyIndex for clients. } // change the color of the hit entity. + if ( hit.entity->getStats() ) + { + hit.entity->getStats()->setAttribute("DOMINATED_CREATURE", "1"); + hit.entity->getStats()->monsterIsCharmed = 0; + } + hit.entity->flags[USERFLAG2] = true; serverUpdateEntityFlag(hit.entity, USERFLAG2); if ( monsterChangesColorWhenAlly(hitstats) ) @@ -211,23 +248,19 @@ bool spellEffectDominate(Entity& my, spellElement_t& element, Entity& caster, En return true; } -void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int resistance) +void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int damage, int resistance) { playSoundEntity(&my, 173, 128); if ( hit.entity ) { - int damage = element.damage; - damage += damage * ((my.actmagicSpellbookBonus / 100.f) + getBonusFromCasterOfSpellElement(parent, nullptr, &element, SPELL_ACID_SPRAY)); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - if ( (hit.entity->behavior == &actMonster && !hit.entity->isInertMimic()) || hit.entity->behavior == &actPlayer ) { Entity* parent = uidToEntity(my.parent); if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(hit.entity) ) + if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) { return; } @@ -256,8 +289,16 @@ void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int re } } + if ( hasamulet && !hasgoggles ) + { + hit.entity->degradeAmuletProc(hitstats, AMULET_POISONRESISTANCE); + } + DamageGib dmgGib = DMG_DEFAULT; - real_t damageMultiplier = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_MAGIC); + real_t damageMultiplier = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_MAGIC, &resistance); + + Entity::modifyDamageMultipliersFromEffects(hit.entity, parent, damageMultiplier, DAMAGE_TABLE_MAGIC, &my, SPELL_ACID_SPRAY); + if ( damageMultiplier <= 0.75 ) { dmgGib = DMG_WEAKEST; @@ -282,7 +323,6 @@ void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int re int oldHP = hitstats->HP; Sint32 preResistanceDamage = damage; damage *= damageMultiplier; - damage /= (1 + (int)resistance); if ( !hasgoggles ) { hit.entity->modHP(-damage); @@ -296,16 +336,16 @@ void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int re // write the obituary if ( parent ) { - parent->killedByMonsterObituary(hit.entity); + parent->killedByMonsterObituary(hit.entity, true); } int previousDuration = hitstats->EFFECTS_TIMERS[EFF_POISONED]; - int duration = 6 * TICKS_PER_SECOND; - duration /= (1 + (int)resistance); + int duration = element.duration; + duration = convertResistancePointsToMagicValue(duration, resistance); bool recentlyHitBySameSpell = false; if ( !hasamulet && !hasgoggles ) { - hitstats->EFFECTS[EFF_POISONED] = true; + hitstats->setEffectActive(EFF_POISONED, 1); hitstats->EFFECTS_TIMERS[EFF_POISONED] = duration; // 6 seconds. if ( abs(duration - previousDuration) > 10 ) // message if not recently acidified { @@ -322,9 +362,6 @@ void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int re { playSoundEntity(hit.entity, 249, 64); } - /*hitstats->EFFECTS[EFF_SLOW] = true; - hitstats->EFFECTS_TIMERS[EFF_SLOW] = (element->duration * (((element->mana) / static_cast(element->base_mana)) * element->overload_multiplier)); - hitstats->EFFECTS_TIMERS[EFF_SLOW] /= (1 + (int)resistance);*/ if ( hit.entity->behavior == &actPlayer ) { serverUpdateEffects(hit.entity->skill[2]); @@ -412,7 +449,7 @@ void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int re } } } - else if ( hit.entity->behavior == &actDoor ) + else if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) { hit.entity->doorHandleDamageMagic(damage, my, parent); } @@ -428,22 +465,18 @@ void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int re } } -void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int resistance) +void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int damage, int resistance) { playSoundEntity(&my, 173, 128); if ( hit.entity ) { - int damage = element.damage; - damage += damage * ((my.actmagicSpellbookBonus / 100.f) + getBonusFromCasterOfSpellElement(parent, nullptr, &element, SPELL_POISON)); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - if ( (hit.entity->behavior == &actMonster && !hit.entity->isInertMimic()) || hit.entity->behavior == &actPlayer ) { Entity* parent = uidToEntity(my.parent); if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(hit.entity) ) + if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) { return; } @@ -462,8 +495,16 @@ void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int hasamulet = true; } + if ( hasamulet ) + { + hit.entity->degradeAmuletProc(hitstats, AMULET_POISONRESISTANCE); + } + DamageGib dmgGib = DMG_DEFAULT; - real_t damageMultiplier = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_MAGIC); + real_t damageMultiplier = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_MAGIC, &resistance); + + Entity::modifyDamageMultipliersFromEffects(hit.entity, parent, damageMultiplier, DAMAGE_TABLE_MAGIC, &my, SPELL_POISON); + if ( damageMultiplier <= 0.75 ) { dmgGib = DMG_WEAKEST; @@ -487,7 +528,6 @@ void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int Sint32 preResistanceDamage = damage; damage *= damageMultiplier; - damage /= (1 + (int)resistance); Sint32 oldHP = hitstats->HP; hit.entity->modHP(-damage); @@ -496,7 +536,7 @@ void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int // write the obituary if ( parent ) { - parent->killedByMonsterObituary(hit.entity); + parent->killedByMonsterObituary(hit.entity, true); } if ( !hasamulet ) @@ -507,7 +547,7 @@ void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int } else { - hit.entity->setEffect(EFF_POISONED, true, std::max(200, 350 - hit.entity->getCON() * 5), true); // 4-7 seconds. + hit.entity->setEffect(EFF_POISONED, true, std::max(200, element.duration - hit.entity->getCON() * 5), true); // 4-7 seconds. } hitstats->poisonKiller = my.parent; } @@ -556,7 +596,7 @@ void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(3428)); } } - else if ( hit.entity->behavior == &actDoor ) + else if ( hit.entity->behavior == &actDoor || hit.entity->behavior == &actIronDoor ) { hit.entity->doorHandleDamageMagic(damage, my, parent); } @@ -590,7 +630,7 @@ bool spellEffectFear(Entity* my, spellElement_t& element, Entity* forceParent, E if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(target) ) + if ( parent && parent->checkFriend(target) && parent->friendlyFireProtection(target) ) { return false; } @@ -604,13 +644,14 @@ bool spellEffectFear(Entity* my, spellElement_t& element, Entity* forceParent, E int duration = 400; // 8 seconds duration = std::max(150, duration - TICKS_PER_SECOND * (hitstats->CON / 5)); // 3-8 seconds, depending on CON. - duration /= (1 + resistance); + duration = convertResistancePointsToMagicValue(duration, resistance); if ( target->setEffect(EFF_FEAR, true, duration, true) ) { playSoundEntity(target, 687, 128); // fear.ogg Uint32 color = 0; if ( parent ) { + magicOnEntityHit(parent, parent, target, target->getStats(), 0, 0, 0, SPELL_FEAR); // update enemy bar for attacker /*if ( !strcmp(hitstats->name, "") ) { @@ -679,7 +720,7 @@ void spellEffectSprayWeb(Entity& my, spellElement_t& element, Entity* parent, in if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(hit.entity) ) + if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) { return; } @@ -692,14 +733,14 @@ void spellEffectSprayWeb(Entity& my, spellElement_t& element, Entity* parent, in } bool spawnParticles = true; - if ( hitstats->EFFECTS[EFF_WEBBED] ) + if ( hitstats->getEffectActive(EFF_WEBBED) ) { spawnParticles = false; } int previousDuration = hitstats->EFFECTS_TIMERS[EFF_WEBBED]; int duration = 400; - duration /= (1 + resistance); - if ( hit.entity->setEffect(EFF_WEBBED, true, 400, true) ) // 8 seconds. + duration = convertResistancePointsToMagicValue(duration, resistance); + if ( hit.entity->setEffect(EFF_WEBBED, true, duration, true) ) // 8 seconds. { magicOnEntityHit(parent, &my, hit.entity, hitstats, 0, 0, 0, SPELL_SPRAY_WEB); if ( abs(duration - previousDuration) > 10 ) @@ -777,7 +818,7 @@ void spellEffectStealWeapon(Entity& my, spellElement_t& element, Entity* parent, if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(hit.entity) ) + if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) { return; } @@ -789,13 +830,19 @@ void spellEffectStealWeapon(Entity& my, spellElement_t& element, Entity* parent, return; } - if ( hitstats->type == LICH || hitstats->type == LICH_FIRE || hitstats->type == LICH_ICE || hitstats->type == DEVIL ) + if ( hitstats->type == LICH + || hitstats->type == LICH_FIRE + || hitstats->type == LICH_ICE + || hitstats->type == DEVIL + || hitstats->type == SHADOW + || hitstats->type == SHOPKEEPER ) { return; } if ( hit.entity->behavior == &actMonster && (hit.entity->monsterAllySummonRank != 0 + /*|| hitstats->type == MONSTER_ADORCISED_WEAPON*/ || (hitstats->type == INCUBUS && !strncmp(hitstats->name, "inner demon", strlen("inner demon")))) ) { @@ -932,7 +979,7 @@ void spellEffectStealWeapon(Entity& my, spellElement_t& element, Entity* parent, return; } -void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, int resistance) +void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, int damage, int resistance) { if ( hit.entity ) { @@ -942,7 +989,7 @@ void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, i if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(hit.entity) ) + if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) { return; } @@ -955,7 +1002,7 @@ void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, i } DamageGib dmgGib = DMG_DEFAULT; - real_t damageMultiplier = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_MAGIC); + real_t damageMultiplier = Entity::getDamageTableMultiplier(hit.entity, *hitstats, DAMAGE_TABLE_MAGIC, &resistance); if ( damageMultiplier <= 0.75 ) { dmgGib = DMG_WEAKEST; @@ -977,13 +1024,8 @@ void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, i dmgGib = DMG_WEAKEST; } - int damage = element.damage; - damage += damage * ((my.actmagicSpellbookBonus / 100.f) + getBonusFromCasterOfSpellElement(parent, nullptr, &element, SPELL_DRAIN_SOUL)); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - Sint32 preResistanceDamage = damage; damage *= damageMultiplier; - damage /= (1 + (int)resistance); if ( parent ) { @@ -1017,7 +1059,7 @@ void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, i // write the obituary if ( parent ) { - parent->killedByMonsterObituary(hit.entity); + parent->killedByMonsterObituary(hit.entity, true); } // update enemy bar for attacker @@ -1109,86 +1151,17 @@ void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, i { if ( hit.entity->isDamageableCollider() && hit.entity->isColliderDamageableByMagic() ) { - int damage = element.damage; - damage += (my.actmagicSpellbookBonus * damage); - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - damage /= (1 + (int)resistance); - hit.entity->colliderHandleDamageMagic(damage, my, parent); return; } else if ( hit.entity->behavior == &actChest || hit.entity->isInertMimic() ) { - int damage = element.damage; - damage += (my.actmagicSpellbookBonus * damage); - damage /= (1 + (int)resistance); hit.entity->chestHandleDamageMagic(damage, my, parent); return; } else if ( hit.entity->behavior == &actFurniture ) { - int damage = element.damage; - damage += (my.actmagicSpellbookBonus * damage); - damage /= (1 + (int)resistance); - int oldHP = hit.entity->furnitureHealth; - hit.entity->furnitureHealth -= damage; - if ( parent ) - { - if ( parent->behavior == &actPlayer ) - { - bool destroyed = oldHP > 0 && hit.entity->furnitureHealth <= 0; - if ( destroyed ) - { - gameModeManager.currentSession.challengeRun.updateKillEvent(hit.entity); - } - switch ( hit.entity->furnitureType ) - { - case FURNITURE_CHAIR: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(388)); - } - updateEnemyBar(parent, hit.entity, Language::get(677), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_TABLE: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(389)); - } - updateEnemyBar(parent, hit.entity, Language::get(676), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2505)); - } - updateEnemyBar(parent, hit.entity, Language::get(2505), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_BUNKBED: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2506)); - } - updateEnemyBar(parent, hit.entity, Language::get(2506), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - case FURNITURE_PODIUM: - if ( destroyed ) - { - messagePlayer(parent->skill[2], MESSAGE_COMBAT, Language::get(2508), Language::get(2507)); - } - updateEnemyBar(parent, hit.entity, Language::get(2507), hit.entity->furnitureHealth, hit.entity->furnitureMaxHealth, - false, DamageGib::DMG_DEFAULT); - break; - default: - break; - } - } - } - playSoundEntity(hit.entity, 28, 128); + hit.entity->furnitureHandleDamageMagic(damage, my, parent); } } } @@ -1201,7 +1174,7 @@ void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, i return; } -spell_t* spellEffectVampiricAura(Entity* caster, spell_t* spell, int extramagic_to_use) +spell_t* spellEffectVampiricAura(Entity* caster, spell_t* spell) { if ( !caster ) { @@ -1224,18 +1197,6 @@ spell_t* spellEffectVampiricAura(Entity* caster, spell_t* spell, int extramagic_ return nullptr; } - bool newbie = false; - if ( caster->behavior == &actPlayer ) - { - newbie = isSpellcasterBeginner(caster->skill[2], caster); - } - else - { - newbie = isSpellcasterBeginner(-1, caster); - } - - int duration = element->duration; // duration in ticks. - //duration += (((element->mana + extramagic_to_use) - element->base_mana) / static_cast(element->overload_multiplier)) * element->duration; node_t* spellnode = list_AddNodeLast(&myStats->magic_effects); spellnode->element = copySpell(spell); //We need to save the spell since this is a channeled spell. spell_t* channeled_spell = (spell_t*)(spellnode->element); @@ -1243,22 +1204,8 @@ spell_t* spellEffectVampiricAura(Entity* caster, spell_t* spell, int extramagic_ spellnode->size = sizeof(spell_t); ((spell_t*)spellnode->element)->caster = caster->getUID(); spellnode->deconstructor = &spellDeconstructor; - //if ( newbie ) - //{ - // //This guy's a newbie. There's a chance they've screwed up and negatively impacted the efficiency of the spell. - // int chance = local_rng.rand() % 10; - // // spellcasting power is 0 to 100, based on spellcasting and intelligence. - // int spellcastingPower = std::min(std::max(0, myStats->PROFICIENCIES[PRO_SPELLCASTING] + statGetINT(myStats, caster)), 100); - // if ( chance >= spellcastingPower / 10 ) - // { - // duration -= local_rng.rand() % (1000 / (spellcastingPower + 1)); // reduce the duration by 0-20 seconds - // } - // if ( duration < 50 ) - // { - // duration = 50; //Range checking. - // } - //} - duration /= getCostOfSpell((spell_t*)spellnode->element); + + int duration = element->duration; // duration in ticks. channeled_spell->channel_duration = duration; //Tell the spell how long it's supposed to last so that it knows what to reset its timer to. caster->setEffect(EFF_VAMPIRICAURA, true, duration, true); for ( int i = 0; i < MAXPLAYERS; ++i ) @@ -1294,6 +1241,7 @@ int getCharmMonsterDifficulty(Entity& my, Stat& myStats) case SKELETON: case SCORPION: case SHOPKEEPER: + case REVENANT_SKULL: difficulty = 0; break; case GOBLIN: @@ -1303,6 +1251,7 @@ int getCharmMonsterDifficulty(Entity& my, Stat& myStats) case SCARAB: case AUTOMATON: case SUCCUBUS: + case GREMLIN: difficulty = 1; break; case CREATURE_IMP: @@ -1312,6 +1261,13 @@ int getCharmMonsterDifficulty(Entity& my, Stat& myStats) case INSECTOID: case GOATMAN: case BUGBEAR: + case MONSTER_ADORCISED_WEAPON: + case FLAME_ELEMENTAL: + case EARTH_ELEMENTAL: + case MOTH_SMALL: + case DRYAD: + case MYCONID: + case SALAMANDER: difficulty = 2; break; case CRYSTALGOLEM: @@ -1327,13 +1283,23 @@ int getCharmMonsterDifficulty(Entity& my, Stat& myStats) case MINOTAUR: case MIMIC: case BAT_SMALL: + case MINIMIMIC: + case HOLOGRAM: + case DUCK_SMALL: + case MONSTER_UNUSED_6: + case MONSTER_UNUSED_7: + case MONSTER_UNUSED_8: difficulty = 666; break; } + if ( my.monsterCanTradeWith(-1) ) + { + difficulty = 666; + } /************** CHANCE CALCULATION ***********/ - if ( myStats.EFFECTS[EFF_CONFUSED] || myStats.EFFECTS[EFF_DRUNK] || my.behavior == &actPlayer ) + if ( myStats.getEffectActive(EFF_CONFUSED) || myStats.getEffectActive(EFF_DRUNK) || my.behavior == &actPlayer ) { difficulty -= 1; // players and confused/drunk monsters have lower resistance. } @@ -1353,7 +1319,7 @@ void spellEffectCharmMonster(Entity& my, spellElement_t& element, Entity* parent if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) { // test for friendly fire - if ( parent && parent->checkFriend(hit.entity) ) + if ( parent && parent->checkFriend(hit.entity) && parent->friendlyFireProtection(hit.entity) ) { return; } @@ -1387,11 +1353,11 @@ void spellEffectCharmMonster(Entity& my, spellElement_t& element, Entity* parent { if ( magicstaff ) { - chance += ((parent->getCHR() + casterStats->getModifiedProficiency(PRO_LEADERSHIP)) / 20) * 10; + chance += ((parent->getCHR() + std::max(casterStats->getModifiedProficiency(PRO_MYSTICISM), casterStats->getModifiedProficiency(PRO_LEADERSHIP))) / 20) * 10; } else { - chance += ((parent->getCHR() + casterStats->getModifiedProficiency(PRO_LEADERSHIP)) / 20) * 5; + chance += ((parent->getCHR() + std::max(casterStats->getModifiedProficiency(PRO_MYSTICISM), casterStats->getModifiedProficiency(PRO_LEADERSHIP))) / 20) * 5; chance += (parent->getINT() * 2); } @@ -1445,13 +1411,11 @@ void spellEffectCharmMonster(Entity& my, spellElement_t& element, Entity* parent { chance += 10; } - chance /= (1 + resistance); + chance = convertResistancePointsToMagicValue(chance, resistance); /************** END CHANCE CALCULATION ***********/ // special cases: - if ( (hitstats->type == VAMPIRE && MonsterData_t::nameMatchesSpecialNPCName(*hitstats, "bram kindly")) - || (hitstats->type == COCKATRICE && !strncmp(map.name, "Cockatrice Lair", 15)) - ) + if ( hit.entity->isBossMonster() && hitstats->type != SHOPKEEPER ) { chance = 0; } @@ -1469,7 +1433,7 @@ void spellEffectCharmMonster(Entity& my, spellElement_t& element, Entity* parent { // caster hit themselves somehow... get pacified. int duration = element.duration; - duration /= (1 + resistance); + duration = convertResistancePointsToMagicValue(duration, resistance); if ( hit.entity->setEffect(EFF_PACIFY, true, duration, true) ) { playSoundEntity(hit.entity, 168, 128); // Healing.ogg @@ -1576,7 +1540,7 @@ void spellEffectCharmMonster(Entity& my, spellElement_t& element, Entity* parent } } - hit.entity->setEffect(EFF_CONFUSED, false, 0, false); + hit.entity->setEffect(EFF_CONFUSED, false, 0, true); magicOnEntityHit(parent, &my, hit.entity, hitstats, 0, 0, 0, SPELL_CHARM_MONSTER); // change the color of the hit entity. @@ -1629,7 +1593,7 @@ void spellEffectCharmMonster(Entity& my, spellElement_t& element, Entity* parent // had a chance, or currently in service of another monster, or a player, or spell no parent, failed to completely charm. // loses will to attack. int duration = element.duration; - duration /= (1 + resistance); + duration = convertResistancePointsToMagicValue(duration, resistance); if ( hitstats->type == SHOPKEEPER ) { duration = 100; @@ -1708,7 +1672,7 @@ void spellEffectCharmMonster(Entity& my, spellElement_t& element, Entity* parent return; } -Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell, int customDuration) +Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell, int customDuration, Monster customMonster) { int effectDuration = 0; effectDuration = TICKS_PER_SECOND * 60 * (4 + local_rng.rand() % 3); // 4-6 minutes @@ -1718,7 +1682,7 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell } if ( !target || !target->getStats() ) { - if ( parent && parent->behavior == &actPlayer ) + if ( parent && parent->behavior == &actPlayer && customMonster == NOTHING ) { messagePlayer(parent->skill[2], MESSAGE_HINT, Language::get(3191)); // had no effect } @@ -1727,20 +1691,33 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell Stat* targetStats = target->getStats(); - if ( targetStats->type == LICH || targetStats->type == SHOPKEEPER || targetStats->type == DEVIL - || targetStats->type == MINOTAUR || targetStats->type == LICH_FIRE || targetStats->type == LICH_ICE - || (target->behavior == &actMonster && target->monsterAllySummonRank != 0) - || targetStats->type == MIMIC || targetStats->type == BAT_SMALL - || (targetStats->type == INCUBUS && !strncmp(targetStats->name, "inner demon", strlen("inner demon"))) - || targetStats->type == SENTRYBOT || targetStats->type == SPELLBOT || targetStats->type == GYROBOT - || targetStats->type == DUMMYBOT - ) + if ( customMonster == NOTHING ) { - if ( parent && parent->behavior == &actPlayer ) + if ( targetStats->type == LICH || targetStats->type == SHOPKEEPER || targetStats->type == DEVIL + || targetStats->type == MINOTAUR || targetStats->type == LICH_FIRE || targetStats->type == LICH_ICE + || (target->behavior == &actMonster && target->monsterAllySummonRank != 0) + || target->monsterCanTradeWith(-1) + || (targetStats->type == SKELETON && targetStats->getAttribute("revenant_skeleton") != "" ) + || targetStats->type == MIMIC || targetStats->type == BAT_SMALL + || targetStats->type == MONSTER_ADORCISED_WEAPON + || targetStats->type == MOTH_SMALL + || targetStats->type == MINIMIMIC + || targetStats->type == REVENANT_SKULL + || targetStats->type == FLAME_ELEMENTAL + || targetStats->type == EARTH_ELEMENTAL + || (targetStats->type == VAMPIRE && (targetStats->getAttribute("special_npc") == "bram kindly")) + || (targetStats->type == INCUBUS && (targetStats->getAttribute("special_npc") == "johann")) + || (targetStats->type == INCUBUS && !strncmp(targetStats->name, "inner demon", strlen("inner demon"))) + || targetStats->type == SENTRYBOT || targetStats->type == SPELLBOT || targetStats->type == GYROBOT + || targetStats->type == DUMMYBOT + ) { - messagePlayer(parent->skill[2], MESSAGE_HINT, Language::get(3191)); // had no effect + if ( parent && parent->behavior == &actPlayer ) + { + messagePlayer(parent->skill[2], MESSAGE_HINT, Language::get(3191)); // had no effect + } + return nullptr; } - return nullptr; } if ( target->behavior == &actMonster ) @@ -1748,7 +1725,11 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell auto& rng = target->entity_rng ? *target->entity_rng : local_rng; Monster monsterSummonType; - if ( targetStats->type == SHADOW ) + if ( customMonster != NOTHING ) + { + monsterSummonType = customMonster; + } + else if ( targetStats->type == SHADOW ) { monsterSummonType = CREATURE_IMP; // shadows turn to imps } @@ -1759,7 +1740,21 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell LICH, SHOPKEEPER, DEVIL, MIMIC, CRAB, BAT_SMALL, MINOTAUR, LICH_FIRE, LICH_ICE, NOTHING, HUMAN, SENTRYBOT, SPELLBOT, GYROBOT, - DUMMYBOT + DUMMYBOT, REVENANT_SKULL, + MINIMIMIC, + MONSTER_ADORCISED_WEAPON, + FLAME_ELEMENTAL, + HOLOGRAM, + EARTH_ELEMENTAL, + DUCK_SMALL, + MYCONID, + DRYAD, + GREMLIN, + SALAMANDER, + MONSTER_UNUSED_6, + MONSTER_UNUSED_7, + MONSTER_UNUSED_8, + MOTH_SMALL }; typesToSkip.insert(targetStats->type); if ( target->monsterAllyGetPlayerLeader() ) @@ -1831,7 +1826,7 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell bool fellInLava = false; bool fellInWater = false; - if ( targetStats->EFFECTS[EFF_LEVITATING] + if ( targetStats->getEffectActive(EFF_LEVITATING) && (monsterSummonType != CREATURE_IMP && monsterSummonType != COCKATRICE && monsterSummonType != SHADOW) ) { // check if there's a floor... @@ -1878,7 +1873,16 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell if ( tryReposition ) { summonedEntity = summonMonster(monsterSummonType, target->x, target->y); - if ( !summonedEntity && (fellToDeath || fellInLava) ) + if ( !summonedEntity && customMonster != NOTHING ) + { + /*monsterSummonType = REVENANT_SKULL; + summonCanEquipItems = false; + summonedEntity = summonMonster(monsterSummonType, target->x, target->y, true); + fellToDeath = false; + fellInLava = false; + fellInWater = false;*/ + } + else if ( !summonedEntity && (fellToDeath || fellInLava) ) { summonedEntity = summonMonster(monsterSummonType, target->x, target->y, true); // force try, kill monster later. } @@ -1890,7 +1894,7 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell if ( !summonedEntity ) { - if ( parent && parent->behavior == &actPlayer ) + if ( parent && parent->behavior == &actPlayer && customMonster == NOTHING ) { if ( fellInWater ) { @@ -1970,9 +1974,11 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell summonedStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; summonedStats->leader_uid = targetStats->leader_uid; summonedStats->monsterIsCharmed = targetStats->monsterIsCharmed; + summonedStats->setAttribute("DOMINATED_CREATURE", targetStats->getAttribute("DOMINATED_CREATURE")); + Entity* leader = nullptr; if ( summonedStats->leader_uid != 0 && summonedStats->type != SHADOW ) { - Entity* leader = uidToEntity(summonedStats->leader_uid); + leader = uidToEntity(summonedStats->leader_uid); if ( leader ) { // lose old ally @@ -2019,42 +2025,54 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell } } } + } + } - if ( forceFollower(*leader, *summonedEntity) ) + if ( customMonster != NOTHING ) + { + if ( parent ) + { + if ( Stat* parentStats = parent->getStats() ) { - summonedEntity->monsterAllyIndex = -1; - if ( leader->behavior == &actPlayer ) - { - summonedEntity->monsterAllyIndex = leader->skill[2]; - if ( multiplayer == SERVER ) - { - serverUpdateEntitySkill(summonedEntity, 42); // update monsterAllyIndex for clients. - } - } - // change the color of the hit entity. - summonedEntity->flags[USERFLAG2] = true; - serverUpdateEntityFlag(summonedEntity, USERFLAG2); - if ( monsterChangesColorWhenAlly(summonedStats) ) + summonedStats->leader_uid = parent->getUID(); + leader = parent; + } + } + } + + if ( leader && forceFollower(*leader, *summonedEntity) ) + { + summonedEntity->monsterAllyIndex = -1; + if ( leader->behavior == &actPlayer ) + { + summonedEntity->monsterAllyIndex = leader->skill[2]; + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(summonedEntity, 42); // update monsterAllyIndex for clients. + } + } + // change the color of the hit entity. + summonedEntity->flags[USERFLAG2] = true; + serverUpdateEntityFlag(summonedEntity, USERFLAG2); + if ( monsterChangesColorWhenAlly(summonedStats) ) + { + int bodypart = 0; + for ( node_t* node = summonedEntity->children.first; node != nullptr; node = node->next ) + { + if ( bodypart >= LIMB_HUMANOID_TORSO ) { - int bodypart = 0; - for ( node_t* node = summonedEntity->children.first; node != nullptr; node = node->next ) + Entity* tmp = (Entity*)node->element; + if ( tmp ) { - if ( bodypart >= LIMB_HUMANOID_TORSO ) - { - Entity* tmp = (Entity*)node->element; - if ( tmp ) - { - tmp->flags[USERFLAG2] = true; - //serverUpdateEntityFlag(tmp, USERFLAG2); - } - } - ++bodypart; + tmp->flags[USERFLAG2] = true; + //serverUpdateEntityFlag(tmp, USERFLAG2); } } + ++bodypart; } } } - if ( targetStats->type == HUMAN ) + if ( targetStats->type == HUMAN && customMonster == NOTHING ) { strcpy(summonedStats->name, targetStats->name); } @@ -2065,8 +2083,24 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell Item** slot = itemSlot(targetStats, targetStats->weapon); if ( slot ) { - summonedStats->weapon = newItem((*slot)->type, (*slot)->status, (*slot)->beatitude, - (*slot)->count, (*slot)->appearance, (*slot)->identified, nullptr); + if ( targetStats->weapon && itemCategory(targetStats->weapon) == SPELLBOOK ) + { + // spellbooks are not dropped + if ( targetStats->weapon->node ) + { + list_RemoveNode(targetStats->weapon->node); + } + else + { + free(targetStats->weapon); + } + targetStats->weapon = nullptr; + } + else + { + summonedStats->weapon = newItem((*slot)->type, (*slot)->status, (*slot)->beatitude, + (*slot)->count, (*slot)->appearance, (*slot)->identified, nullptr); + } } // shield @@ -2184,10 +2218,27 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell } else if ( hitMonsterCanTransferEquipment && !summonCanEquipItems ) { - Entity* dropped = dropItemMonster(targetStats->weapon, target, targetStats); - if ( dropped ) + Entity* dropped = nullptr; + if ( targetStats->weapon && itemCategory(targetStats->weapon) == SPELLBOOK ) { - dropped->flags[USERFLAG1] = true; + // spellbooks are not dropped + if ( targetStats->weapon->node ) + { + list_RemoveNode(targetStats->weapon->node); + } + else + { + free(targetStats->weapon); + } + targetStats->weapon = nullptr; + } + else + { + dropped = dropItemMonster(targetStats->weapon, target, targetStats); + if ( dropped ) + { + dropped->flags[USERFLAG1] = true; + } } dropped = dropItemMonster(targetStats->shield, target, targetStats); if ( dropped ) @@ -2236,9 +2287,14 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell { nextnode = node->next; Item* item = (Item*)node->element; - if ( item && item->appearance != MONSTER_ITEM_UNDROPPABLE_APPEARANCE && itemSlot(targetStats, item) == nullptr ) + if ( item && item->appearance != MONSTER_ITEM_UNDROPPABLE_APPEARANCE + && item->isDroppable + && itemSlot(targetStats, item) == nullptr ) { - Item* copiedItem = newItem(item->type, item->status, item->beatitude, item->count, item->appearance, item->identified, &summonedStats->inventory); + if ( targetStats->type != SHOPKEEPER || (targetStats->type == SHOPKEEPER && local_rng.rand() % 2) ) + { + Item* copiedItem = newItem(item->type, item->status, item->beatitude, item->count, item->appearance, item->identified, &summonedStats->inventory); + } if ( item->node ) { list_RemoveNode(item->node); @@ -2255,7 +2311,11 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell Uint32 color = makeColorRGB(0, 255, 0); bool namedMonsterAsGeneric = monsterNameIsGeneric(*targetStats); // the %s polymorph into a %s! - if ( !strcmp((*targetStats).name, "") || namedMonsterAsGeneric ) + if ( customMonster != NOTHING ) + { + + } + else if ( !strcmp((*targetStats).name, "") || namedMonsterAsGeneric ) { messagePlayerColor(parent->skill[2], MESSAGE_COMBAT, color, Language::get(3187), getMonsterLocalizedName((*targetStats).type).c_str(), getMonsterLocalizedName(summonedStats->type).c_str()); } @@ -2265,10 +2325,13 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell } } - playSoundEntity(target, 400, 92); - spawnExplosion(target->x, target->y, target->z); - createParticleDropRising(target, 593, 1.f); - serverSpawnMiscParticles(target, PARTICLE_EFFECT_RISING_DROP, 593); + if ( customMonster == NOTHING ) + { + playSoundEntity(target, 400, 92); + spawnExplosion(target->x, target->y, target->z); + createParticleDropRising(target, 593, 1.f); + serverSpawnMiscParticles(target, PARTICLE_EFFECT_RISING_DROP, 593); + } if ( fellToDeath ) { @@ -2319,7 +2382,24 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell if ( targetStats->playerRace == RACE_HUMAN || (targetStats->playerRace != RACE_HUMAN && targetStats->stat_appearance != 0) ) { - int roll = (RACE_HUMAN + 1) + local_rng.rand() % 8; + std::vector chances = + { + RACE_SKELETON, + RACE_VAMPIRE, + RACE_SUCCUBUS, + RACE_GOATMAN, + RACE_AUTOMATON, + RACE_INCUBUS, + RACE_GOBLIN, + RACE_INSECTOID, + RACE_GNOME, + RACE_GREMLIN, + RACE_DRYAD, + RACE_MYCONID, + RACE_SALAMANDER + }; + int roll = chances[local_rng.rand() % chances.size()]; + if ( target->effectPolymorph == 0 ) { target->effectPolymorph = target->getMonsterFromPlayerRace(roll); @@ -2328,7 +2408,7 @@ Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell { while ( target->effectPolymorph == target->getMonsterFromPlayerRace(roll) ) { - roll = (RACE_HUMAN + 1) + local_rng.rand() % 8; // re roll to not polymorph into the same thing + roll = chances[local_rng.rand() % chances.size()]; // re roll to not polymorph into the same thing } target->effectPolymorph = target->getMonsterFromPlayerRace(roll); } @@ -2393,8 +2473,6 @@ bool spellEffectTeleportPull(Entity* my, spellElement_t& element, Entity* parent if ( target ) { playSoundEntity(target, 173, 128); - //int damage = element.damage; - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; if ( (target->behavior == &actMonster && !target->isInertMimic()) || target->behavior == &actPlayer /*|| target->behavior == &actDoor || target->behavior == &actChest*/ ) @@ -2402,14 +2480,14 @@ bool spellEffectTeleportPull(Entity* my, spellElement_t& element, Entity* parent Stat* hitstats = target->getStats(); if ( hitstats ) { - if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) - { - // test for friendly fire - if ( parent && parent->checkFriend(target) ) - { - return false; - } - } + //if ( !(svFlags & SV_FLAG_FRIENDLYFIRE) ) + //{ + // // test for friendly fire + // if ( parent && parent->checkFriend(target) && parent->friendlyFireProtection(target) ) + // { + // return false; + // } + //} } //playSoundEntity(target, 249, 64); @@ -2544,7 +2622,7 @@ bool spellEffectTeleportPull(Entity* my, spellElement_t& element, Entity* parent locationTimer->x = tx * 16.0 + 8; locationTimer->y = ty * 16.0 + 8; locationTimer->z = 0; - locationTimer->particleTimerCountdownAction = PARTICLE_EFFECT_TELEPORT_PULL_TARGET_LOCATION; + locationTimer->particleTimerCountdownAction = PARTICLE_TIMER_TELEPORT_PULL_TARGET_LOCATION; locationTimer->particleTimerCountdownSprite = 593; locationTimer->particleTimerTarget = static_cast(target->getUID()); // get the target to teleport around. locationTimer->particleTimerEndAction = PARTICLE_EFFECT_TELEPORT_PULL; // teleport behavior of timer. @@ -2610,9 +2688,6 @@ void spellEffectShadowTag(Entity& my, spellElement_t& element, Entity* parent, i { if ( hit.entity ) { - //int damage = element.damage; - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - if ( (hit.entity->behavior == &actMonster && !hit.entity->isInertMimic()) || hit.entity->behavior == &actPlayer ) { playSoundEntity(&my, 174, 128); @@ -2646,7 +2721,7 @@ void spellEffectShadowTag(Entity& my, spellElement_t& element, Entity* parent, i } else { - hit.entity->setEffect(EFF_SHADOW_TAGGED, true, 10 * TICKS_PER_SECOND, true); + hit.entity->setEffect(EFF_SHADOW_TAGGED, true, element.duration, true); } magicOnEntityHit(parent, &my, hit.entity, hitstats, 0, 0, 0, SPELL_SHADOW_TAG); parent->creatureShadowTaggedThisUid = hit.entity->getUID(); @@ -2701,9 +2776,6 @@ bool spellEffectDemonIllusion(Entity& my, spellElement_t& element, Entity* paren { if ( target ) { - //int damage = element.damage; - //damage += ((element->mana - element->base_mana) / static_cast(element->overload_multiplier)) * element->damage; - if ( (target->behavior == &actMonster && !target->isInertMimic()) || target->behavior == &actPlayer ) { Stat* hitstats = target->getStats(); @@ -2716,6 +2788,13 @@ bool spellEffectDemonIllusion(Entity& my, spellElement_t& element, Entity* paren || hitstats->type == AUTOMATON || hitstats->type == DEVIL || hitstats->type == DEMON || hitstats->type == CREATURE_IMP || hitstats->type == SHADOW || hitstats->type == MIMIC + || hitstats->type == MINIMIMIC + || hitstats->type == MONSTER_ADORCISED_WEAPON + || hitstats->type == MOTH_SMALL + || hitstats->type == REVENANT_SKULL + || hitstats->type == FLAME_ELEMENTAL + || hitstats->type == EARTH_ELEMENTAL + || (hitstats->type == SKELETON && hitstats->getAttribute("revenant_skeleton") != "") || (hitstats->type == INCUBUS && !strncmp(hitstats->name, "inner demon", strlen("inner demon"))) ) { if ( parent && parent->behavior == &actPlayer ) @@ -2895,3 +2974,1587 @@ bool spellEffectDemonIllusion(Entity& my, spellElement_t& element, Entity* paren } return false; } + +Entity* spellEffectHologram(Entity& caster, spellElement_t& element, real_t x, real_t y) +{ + Entity* monster = nullptr; + { + // try find a summon location around the entity. + int tx = static_cast(std::floor(x)) >> 4; + int ty = static_cast(std::floor(y)) >> 4; + int dist = 1; + std::vector> goodspots; + for ( int iy = std::max(1, ty - dist); iy < std::min(ty + dist, static_cast(map.height)); ++iy ) + { + for ( int ix = std::max(1, tx - dist); ix < std::min(tx + dist, static_cast(map.width)); ++ix ) + { + if ( !checkObstacle((ix << 4) + 8, (iy << 4) + 8, &caster, NULL, true, true, false) ) + { + goodspots.push_back(std::make_pair(ix, iy)); + } + } + } + if ( goodspots.size() == 0 ) + { + return nullptr; + } + else + { + if ( !checkObstacle((tx << 4) + 8, (ty << 4) + 8, &caster, NULL, true, true, false, false) ) + { + monster = summonMonster(HOLOGRAM, tx * 16.0 + 8, ty * 16.0 + 8, true); + } + while ( !monster && goodspots.size() ) + { + int pick = local_rng.rand() % goodspots.size(); + std::pair tmpPair = goodspots[pick]; + tx = tmpPair.first; + ty = tmpPair.second; + goodspots.erase(goodspots.begin() + pick); + monster = summonMonster(HOLOGRAM, tx * 16.0 + 8, ty * 16.0 + 8, true); + } + + if ( monster ) + { + playSoundEntity(monster, 166, 128); + //playSoundEntity(&my, 178, 128); + createParticleErupt(monster, 983); + serverSpawnMiscParticles(monster, PARTICLE_EFFECT_ERUPT, 983); + + if ( Stat* monsterStats = monster->getStats() ) + { + monsterStats->monsterNoDropItems = 1; + monsterStats->leader_uid = caster.getUID(); + monster->parent = caster.getUID(); + monster->setEffect(EFF_STUNNED, true, -1, false); + } + } + } + } + return monster; +} + +Entity* spellEffectAdorcise(Entity& caster, spellElement_t& element, real_t x, real_t y, Item* itemToAdorcise) +{ + Entity* monster = nullptr; + { + // try find a summon location around the entity. + int tx = static_cast(std::floor(x)) >> 4; + int ty = static_cast(std::floor(y)) >> 4; + int dist = 1; + std::vector> goodspots; + for ( int iy = std::max(1, ty - dist); iy < std::min(ty + dist, static_cast(map.height)); ++iy ) + { + for ( int ix = std::max(1, tx - dist); ix < std::min(tx + dist, static_cast(map.width)); ++ix ) + { + if ( !checkObstacle((ix << 4) + 8, (iy << 4) + 8, &caster, NULL, true, true, false) ) + { + goodspots.push_back(std::make_pair(ix, iy)); + } + } + } + if ( goodspots.size() == 0 ) + { + return nullptr; + } + else + { + if ( !checkObstacle((tx << 4) + 8, (ty << 4) + 8, &caster, NULL, true, true, false, false) ) + { + monster = summonMonster(MONSTER_ADORCISED_WEAPON, tx * 16.0 + 8, ty * 16.0 + 8, true); + } + while ( !monster && goodspots.size() ) + { + int pick = local_rng.rand() % goodspots.size(); + std::pair tmpPair = goodspots[pick]; + tx = tmpPair.first; + ty = tmpPair.second; + goodspots.erase(goodspots.begin() + pick); + monster = summonMonster(MONSTER_ADORCISED_WEAPON, tx * 16.0 + 8, ty * 16.0 + 8, true); + } + + if ( monster ) + { + playSoundEntity(monster, 171, 128); + //playSoundEntity(&my, 178, 128); + createParticleErupt(monster, 983); + serverSpawnMiscParticles(monster, PARTICLE_EFFECT_ERUPT, 983); + + + Stat* monsterStats = monster->getStats(); + if ( monsterStats ) + { + if ( &element == &spellElementMap[SPELL_SPIRIT_WEAPON] ) + { + int duration = getSpellEffectDurationFromID(SPELL_SPIRIT_WEAPON, &caster, nullptr, &caster); + monsterStats->setAttribute("spirit_weapon", std::to_string(duration)); + monsterStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; + monster->setEffect(EFF_ROOTED, true, -1, false); + + ItemType type = IRON_SWORD; + switch ( local_rng.rand() % 4 ) + { + case 1: + type = IRON_SPEAR; + break; + case 2: + type = IRON_MACE; + break; + case 3: + type = IRON_AXE; + break; + case 0: + default: + type = IRON_SWORD; + break; + } + + monsterStats->STR = getSpellDamageFromID(SPELL_SPIRIT_WEAPON, &caster, nullptr, &caster); + monsterStats->MAXHP = getSpellDamageSecondaryFromID(SPELL_SPIRIT_WEAPON, &caster, nullptr, &caster); + monsterStats->HP = monsterStats->MAXHP; + monsterStats->OLDHP = monsterStats->MAXHP; + + if ( monsterStats->weapon = newItem(type, EXCELLENT, 0, 1, local_rng.rand(), true, nullptr) ) + { + monsterStats->weapon->isDroppable = false; + } + monsterStats->monsterNoDropItems = 1; + monsterStats->leader_uid = caster.getUID(); + monster->parent = caster.getUID(); + + monster->monsterHitTime = HITRATE * 1.5 - 10; + const real_t lookDist = 40.0; + real_t dist = lookDist; + Entity* newTarget = nullptr; + for ( node_t* node = map.creatures->first; node != nullptr; node = node->next ) + { + Entity* target = (Entity*)node->element; + if ( target->behavior == &actMonster && monster->checkEnemy(target) ) + { + real_t oldDist = dist; + dist = sqrt(pow(monster->x - target->x, 2) + pow(monster->y - target->y, 2)); + if ( dist < lookDist && dist <= oldDist ) + { + double tangent = atan2(target->y - monster->y, target->x - monster->x); + lineTrace(monster, monster->x, monster->y, tangent, lookDist, 0, false); + if ( hit.entity == target ) + { + newTarget = target; + } + } + } + } + + if ( newTarget ) + { + monster->lookAtEntity(*newTarget); + if ( entityDist(monster, newTarget) < TOUCHRANGE ) + { + monster->monsterAcquireAttackTarget(*newTarget, MONSTER_STATE_ATTACK); + } + } + + } + else if ( &element == &spellElementMap[SPELL_ADORCISM] ) + { + int duration = getSpellEffectDurationFromID(SPELL_ADORCISM, &caster, nullptr, &caster); + monsterStats->setAttribute("adorcised_weapon", std::to_string(duration)); + monsterStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; + if ( itemToAdorcise ) + { + monsterStats->weapon = newItem(itemToAdorcise->type, itemToAdorcise->status, + itemToAdorcise->beatitude, 1, itemToAdorcise->appearance, itemToAdorcise->identified, nullptr); + if ( caster.behavior == &actPlayer ) + { + messagePlayer(caster.skill[2], MESSAGE_INVENTORY, Language::get(6577), itemToAdorcise->getName()); + } + } + else + { + ItemType type = IRON_SWORD; + switch ( local_rng.rand() % 4 ) + { + case 1: + type = IRON_SPEAR; + break; + case 2: + type = IRON_MACE; + break; + case 3: + type = IRON_AXE; + break; + case 0: + default: + type = IRON_SWORD; + break; + } + + monsterStats->STR = getSpellDamageFromID(SPELL_ADORCISM, &caster, nullptr, &caster); + monsterStats->MAXHP = getSpellDamageSecondaryFromID(SPELL_ADORCISM, &caster, nullptr, &caster); + monsterStats->HP = monsterStats->MAXHP; + monsterStats->OLDHP = monsterStats->MAXHP; + + if ( monsterStats->weapon = newItem(type, EXCELLENT, 0, 1, local_rng.rand(), true, nullptr) ) + { + monsterStats->weapon->isDroppable = false; + } + monsterStats->monsterNoDropItems = 1; + } + if ( forceFollower(caster, *monster) ) + { + if ( caster.behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateMonster(caster.skill[2], Compendium_t::CPDM_RECRUITED, monster, 1); + monster->monsterAllyIndex = caster.skill[2]; + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. + } + } + } + } + + if ( caster.behavior == &actPlayer ) + { + monster->flags[USERFLAG2] = true; + serverUpdateEntityFlag(monster, USERFLAG2); + if ( monsterChangesColorWhenAlly(monsterStats) ) + { + int bodypart = 0; + for ( node_t* node = (monster)->children.first; node != nullptr; node = node->next ) + { + if ( bodypart >= LIMB_HUMANOID_TORSO ) + { + Entity* tmp = (Entity*)node->element; + if ( tmp ) + { + tmp->flags[USERFLAG2] = true; + serverUpdateEntityFlag(tmp, USERFLAG2); + } + } + ++bodypart; + } + } + } + } + } + } + } + return monster; +} + +Entity* spellEffectFlameSprite(Entity& caster, spellElement_t& element, real_t x, real_t y) +{ + Entity* monster = nullptr; + { + // try find a summon location around the entity. + int tx = static_cast(std::floor(x)) >> 4; + int ty = static_cast(std::floor(y)) >> 4; + int dist = 1; + std::vector> goodspots; + for ( int iy = std::max(1, ty - dist); iy < std::min(ty + dist, static_cast(map.height)); ++iy ) + { + for ( int ix = std::max(1, tx - dist); ix < std::min(tx + dist, static_cast(map.width)); ++ix ) + { + if ( !checkObstacle((ix << 4) + 8, (iy << 4) + 8, &caster, NULL, true, true, false) ) + { + goodspots.push_back(std::make_pair(ix, iy)); + } + } + } + if ( goodspots.size() == 0 ) + { + return nullptr; + } + else + { + Monster type = &element == &spellElementMap[SPELL_FIRE_SPRITE] ? MOTH_SMALL : FLAME_ELEMENTAL; + if ( !checkObstacle((tx << 4) + 8, (ty << 4) + 8, &caster, NULL, true, true, false, false) ) + { + monster = summonMonster(type, tx * 16.0 + 8, ty * 16.0 + 8, true); + } + while ( !monster && goodspots.size() ) + { + int pick = local_rng.rand() % goodspots.size(); + std::pair tmpPair = goodspots[pick]; + tx = tmpPair.first; + ty = tmpPair.second; + goodspots.erase(goodspots.begin() + pick); + monster = summonMonster(type, tx * 16.0 + 8, ty * 16.0 + 8, true); + } + + if ( monster ) + { + playSoundEntity(monster, 164, 128); + //playSoundEntity(&my, 178, 128); + //createParticleErupt(monster, 983); + //serverSpawnMiscParticles(monster, PARTICLE_EFFECT_ERUPT, 983); + spawnMagicEffectParticles(monster->x, monster->y, monster->z, 2207); + for ( int i = 0; i < 3; ++i ) + { + if ( Entity* fx = createParticleAestheticOrbit(monster, 233, TICKS_PER_SECOND, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->x = monster->x; + fx->y = monster->y; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->z = -7.5; + fx->vel_z = 0.25; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = monster->yaw + (i) * 2 * PI / 3.0; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + + } + } + serverSpawnMiscParticles(monster, PARTICLE_EFFECT_SUMMON_FLAMES, 233, 0, TICKS_PER_SECOND); + + Stat* monsterStats = monster->getStats(); + if ( monsterStats ) + { + if ( &element == &spellElementMap[SPELL_FIRE_SPRITE] ) + { + int duration = getSpellEffectDurationSecondaryFromID(SPELL_FIRE_SPRITE, &caster, nullptr, &caster); + monsterStats->setAttribute("fire_sprite", std::to_string(duration)); + monsterStats->monsterNoDropItems = 1; + monsterStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; + + monsterStats->setAttribute("special_npc", "fire sprite"); + strcpy(monsterStats->name, MonsterData_t::getSpecialNPCName(*monsterStats).c_str()); + + int lvl = getSpellDamageFromID(SPELL_FIRE_SPRITE, &caster, nullptr, &caster); + int maxlvl = getSpellDamageSecondaryFromID(SPELL_FIRE_SPRITE, &caster, nullptr, &caster); + lvl = std::min(lvl, maxlvl); + monsterStats->LVL = lvl; + + if ( forceFollower(caster, *monster) ) + { + if ( caster.behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateMonster(caster.skill[2], Compendium_t::CPDM_RECRUITED, monster, 1); + monster->monsterAllyIndex = caster.skill[2]; + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. + } + } + } + } + else if ( &element == &spellElementMap[SPELL_FLAME_ELEMENTAL] ) + { + int duration = getSpellEffectDurationSecondaryFromID(SPELL_FLAME_ELEMENTAL, &caster, nullptr, &caster); + monsterStats->setAttribute("flame_elemental", std::to_string(duration)); + monsterStats->monsterNoDropItems = 1; + monsterStats->MISC_FLAGS[STAT_FLAG_MONSTER_DISABLE_HC_SCALING] = 1; + + int lvl = getSpellDamageFromID(SPELL_FLAME_ELEMENTAL, &caster, nullptr, &caster); + int maxlvl = getSpellDamageSecondaryFromID(SPELL_FLAME_ELEMENTAL, &caster, nullptr, &caster); + lvl = std::min(lvl, maxlvl); + monsterStats->LVL = lvl; + + if ( forceFollower(caster, *monster) ) + { + if ( caster.behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateMonster(caster.skill[2], Compendium_t::CPDM_RECRUITED, monster, 1); + monster->monsterAllyIndex = caster.skill[2]; + if ( multiplayer == SERVER ) + { + serverUpdateEntitySkill(monster, 42); // update monsterAllyIndex for clients. + } + } + } + } + + if ( caster.behavior == &actPlayer ) + { + monster->flags[USERFLAG2] = true; + serverUpdateEntityFlag(monster, USERFLAG2); + if ( monsterChangesColorWhenAlly(monsterStats) ) + { + int bodypart = 0; + for ( node_t* node = (monster)->children.first; node != nullptr; node = node->next ) + { + if ( bodypart >= LIMB_HUMANOID_TORSO ) + { + Entity* tmp = (Entity*)node->element; + if ( tmp ) + { + tmp->flags[USERFLAG2] = true; + serverUpdateEntityFlag(tmp, USERFLAG2); + } + } + ++bodypart; + } + } + } + } + } + } + } + return monster; +} + +bool Entity::spellEffectPreserveItem(Item* item) +{ + if ( !item ) { return false; } + if ( Stat* myStats = getStats() ) + { + if ( behavior != &actPlayer ) + { + return false; + } + if ( !itemIsEquipped(item, skill[2]) ) + { + return false; + } + if ( myStats->getEffectActive(EFF_PRESERVE) ) + { + if ( spell_t* preserveSpell = getActiveMagicEffect(SPELL_PRESERVE) ) + { + int cost = getSpellDamageFromID(SPELL_PRESERVE, this, nullptr, this); + cost = std::max(1, std::max(getSpellDamageSecondaryFromID(SPELL_PRESERVE, this, nullptr, this), cost)); + if ( item->type == AMULET_LIFESAVING ) + { + cost *= 10; + } + if ( item->type == AMULET_MAGICREFLECTION || item->type == CLOAK_MAGICREFLECTION ) + { + cost *= 2; + } + if ( !safeConsumeMP(cost) ) + { + if ( myStats->MP > 0 ) + { + modMP(-myStats->MP); + } + preserveSpell->sustain = false; + if ( behavior == &actPlayer ) + { + playSoundEntity(this, 163, 128); + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(255, 0, 0), Language::get(6657), item->getName()); + } + return false; + } + else + { + if ( behavior == &actPlayer ) + { + spawnMagicEffectParticles(this->x, this->y, this->z / 2, 174); + playSoundEntity(this, 166, 128); + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6654), item->getName()); + + players[skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_PRESERVE, 10.0, 1.0, nullptr); + } + return true; + } + } + } + } + return false; +} + +int thaumSpellArmorProc(Entity* my, Stat& myStats, bool checkEffectActiveOnly, Entity* attacker, int effectID) +{ + int player = -1; + if ( my && my->behavior == &actPlayer ) + { + player = my->skill[2]; + } + if ( !my && player == -1 ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( stats[i] == &myStats ) + { + player = i; + break; + } + } + } + + int spellID = -1; + if ( effectID == EFF_GUARD_BODY ) + { + spellID = SPELL_GUARD_BODY; + } + else if ( effectID == EFF_GUARD_SPIRIT ) + { + spellID = SPELL_GUARD_SPIRIT; + } + else if ( effectID == EFF_DIVINE_GUARD ) + { + spellID = SPELL_DIVINE_GUARD; + } + + if ( spellID < 0 ) { return 0; } + + if ( myStats.getEffectActive(effectID) ) + { + if ( player >= 0 ) + { + int result = myStats.getEffectActive(effectID); + + if ( !checkEffectActiveOnly ) + { + if ( my ) + { + int baseMinValue = (effectID == EFF_GUARD_SPIRIT) ? 1 : 3; + //int minValue = std::max(baseMinValue, getSpellDamageFromID(spellID, my, nullptr, my)); + //minValue = std::min(minValue, getSpellEffectDurationSecondaryFromID(spellID, my, nullptr, my)); + my->setEffect(effectID, (Uint8)std::max(baseMinValue, myStats.getEffectActive(effectID) - 1), + myStats.EFFECTS_TIMERS[effectID], false, true, true); + if ( my->getActiveMagicEffect(spellID) ) + { + if ( result != myStats.getEffectActive(effectID) ) + { + players[player]->mechanics.updateSustainedSpellEvent(spellID, std::min(150.0, effectID == EFF_GUARD_SPIRIT ? 128.0 : 50.0 + 10 * result), 1.0, attacker); + } + else + { + players[player]->mechanics.updateSustainedSpellEvent(spellID, std::min(150.0, effectID == EFF_GUARD_SPIRIT ? 128.0 : 50.0 + 10 * result) / 10.0, 1.0, attacker); + } + } + } + } + return result; + } + else + { + if ( !checkEffectActiveOnly ) + { + if ( my && my->behavior == &actMonster ) + { + myStats.EFFECTS_TIMERS[effectID] = std::max(1, myStats.EFFECTS_TIMERS[effectID] - TICKS_PER_SECOND); + } + } + return myStats.getEffectActive(effectID); + } + } + + return 0; +} + +bool Entity::pinpointDamageProc(Entity* attacker, int damage) +{ + if ( multiplayer == CLIENT || !attacker ) { return false; } + if ( Stat* myStats = getStats() ) + { + if ( myStats->HP == 0 ) { return false; } + if ( !(attacker->behavior == &actPlayer || (attacker->behavior == &actMonster && attacker->monsterAllyGetPlayerLeader())) ) + { + return false; + } + + if ( myStats->getEffectActive(EFF_PINPOINT) || myStats->getEffectActive(EFF_PINPOINT_DAMAGE) ) + { + if ( damage > 0 ) + { + // find particle to update + bool found = false; + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 1); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr && !found; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->behavior == &actParticleAestheticOrbit + && entity->parent == this->getUID() + && entity->skill[1] == PARTICLE_EFFECT_SMITE_PINPOINT + && entity->actmagicNoLight == 0 ) + { + Entity* caster = uidToEntity(entity->skill[3]); + real_t damageMult = getSpellDamageSecondaryFromID(SPELL_PINPOINT, caster, caster ? caster->getStats() : nullptr, + entity, entity->actmagicSpellbookBonus / 100.0) / 100.0; + entity->skill[4] += std::max(0, (damage)) * damageMult; + found = true; + break; + } + else if ( entity->behavior == &actParticlePinpointTarget + && entity->skill[4] == SPELL_PINPOINT + && entity->parent == this->getUID() + && entity->skill[0] >= 0 ) + { + Uint32 casterUid = static_cast(entity->skill[2]); + Entity* caster = uidToEntity(casterUid); + + for ( int i = 0; i < 3; ++i ) + { + Entity* fx1 = createParticleAestheticOrbit(this, 2401, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_SMITE_PINPOINT); + fx1->yaw = this->yaw + PI / 2 + 2 * i * PI / 3; + fx1->fskill[4] = this->x; + fx1->fskill[5] = this->y; + fx1->x = this->x; + fx1->y = this->y; + fx1->fskill[6] = fx1->yaw; + fx1->skill[3] = caster ? caster->getUID() : 0; + if ( i != 0 ) + { + fx1->actmagicNoLight = 1; + } + if ( i == 0 ) + { + fx1->actmagicSpellbookBonus = entity->actmagicSpellbookBonus; + real_t damageMult = getSpellDamageSecondaryFromID(SPELL_PINPOINT, caster, caster ? caster->getStats() : nullptr, + entity, entity->actmagicSpellbookBonus / 100.0) / 100.0; + fx1->skill[4] += std::max(0, (damage)) * damageMult; + fx1->actmagicFromSpellbook = entity->actmagicFromSpellbook; + } + } + + setEffect(EFF_PINPOINT_DAMAGE, true, 2 * TICKS_PER_SECOND, false); + serverSpawnMiscParticles(this, PARTICLE_EFFECT_SMITE_PINPOINT, 2401, 0, 0); + + entity->skill[0] = -1; // expire this + found = true; + break; + } + } + } + if ( found ) + { + break; + } + } + + if ( found ) + { + return true; + } + } + } + } + return false; +} + +bool Entity::defyFleshProc(Entity* attacker) +{ + if ( multiplayer == CLIENT ) { return false; } + if ( Stat* myStats = getStats() ) + { + if ( myStats->getEffectActive(EFF_DEFY_FLESH) ) + { + // find particle to update + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(this, 1); + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->behavior == &actParticleAestheticOrbit + && entity->parent == this->getUID() + && entity->skill[1] == PARTICLE_EFFECT_DEFY_FLESH_ORBIT ) + { + if ( entity->skill[8] == 0 ) // cooldown timer + { + entity->skill[7] = 1; + Uint8 charges = myStats->getEffectActive(EFF_DEFY_FLESH) & 0xF; + if ( charges == 0 ) + { + setEffect(EFF_DEFY_FLESH, false, 0, true); + } + else + { + --charges; + /*if ( charges == 0 ) + { + setEffect(EFF_DEFY_FLESH, false, 0, true); + } + else*/ + { + charges |= (myStats->getEffectActive(EFF_DEFY_FLESH) & 0xF0); + setEffect(EFF_DEFY_FLESH, charges, myStats->EFFECTS_TIMERS[EFF_DEFY_FLESH], true, true, true); + } + } + } + return true; + } + } + } + } + } + } + + return false; +} + +bool Entity::mistFormDodge(bool checkEffectActiveOnly, Entity* attacker) +{ + if ( Stat* myStats = getStats() ) + { + if ( myStats->getEffectActive(EFF_MIST_FORM) ) + { + if ( checkEffectActiveOnly ) + { + return true; + } + + if ( behavior == &actPlayer ) + { + if ( spell_t* spell = getActiveMagicEffect(SPELL_MIST_FORM) ) + { + int chance = getSpellEffectDurationSecondaryFromID(SPELL_MIST_FORM, this, nullptr, this); + if ( local_rng.rand() % 100 < chance ) + { + int cost = getSpellDamageFromID(SPELL_MIST_FORM, this, nullptr, this); + cost = std::max(1, std::max(cost, getSpellDamageSecondaryFromID(SPELL_MIST_FORM, this, nullptr, this))); + if ( !safeConsumeMP(cost) ) + { + if ( myStats->MP > 0 ) + { + modMP(-myStats->MP); + } + spell->sustain = false; + if ( behavior == &actPlayer ) + { + playSoundEntity(this, 163, 128); + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(255, 0, 0), Language::get(6665)); + } + return false; + } + else + { + if ( behavior == &actPlayer ) + { + spawnPoof(this->x, this->y, this->z / 2, 1.0, true); + playSoundEntity(this, 166, 128); + messagePlayerColor(skill[2], MESSAGE_COMBAT, makeColorRGB(0, 255, 0), Language::get(6666)); + magicOnSpellCastEvent(this, this, attacker, SPELL_MIST_FORM, spell_t::SPELL_LEVEL_EVENT_DEFAULT | spell_t::SPELL_LEVEL_EVENT_MINOR_CHANCE, 1); + } + return true; + } + } + else + { + return false; + } + } + } + else + { + if ( behavior == &actMonster ) + { + myStats->EFFECTS_TIMERS[EFF_MIST_FORM] = std::max(1, myStats->EFFECTS_TIMERS[EFF_MIST_FORM] - TICKS_PER_SECOND); + } + return true; + } + } + } + return false; +} + +bool applyGenericMagicDamage(Entity* caster, Entity* hitentity, Entity& damageSourceProjectile, int spellID, + int damage, bool alertMonsters, bool monsterCollisionOnly, int usingSpellbookID) +{ + if ( !hitentity ) + { + return false; + } + + int trapResist = 0; + int resistance = 0; + DamageGib dmgGib = DMG_DEFAULT; + real_t damageMultiplier = 1.0; + magicSetResistance(hitentity, caster, resistance, damageMultiplier, dmgGib, trapResist, spellID); + Stat* targetStats = hitentity->getStats(); + if ( monsterCollisionOnly ) + { + if ( !targetStats ) + { + return false; + } + if ( hitentity->isInertMimic() ) + { + return false; + } + } + + Entity::modifyDamageMultipliersFromEffects(hitentity, caster, damageMultiplier, DAMAGE_TABLE_MAGIC, &damageSourceProjectile, spellID); + + if ( damageSourceProjectile.behavior == &actBoulder ) + { + // pure dmg + dmgGib = DMG_STRONGEST; + damageMultiplier = 1.0; + resistance = 0; + trapResist = 0; + } + else if ( spellID == SPELL_EARTH_ELEMENTAL && damageSourceProjectile.behavior == &actMonster + && damageSourceProjectile.getMonsterTypeFromSprite() == EARTH_ELEMENTAL ) + { + // pure dmg + dmgGib = DMG_STRONGEST; + damageMultiplier = 1.0; + resistance = 0; + trapResist = 0; + } + if ( spellID == SPELL_HOLY_FIRE ) + { + // pure dmg + dmgGib = DMG_STRONGER; + damageMultiplier = 1.0; + resistance = 0; + trapResist = 0; + } + + absorbMagicEvent(hitentity, caster, damageSourceProjectile, spellID, nullptr, damageMultiplier, dmgGib); + + if ( hitentity->behavior == &actChest || hitentity->isInertMimic() ) + { + damage *= damageMultiplier; + hitentity->chestHandleDamageMagic(damage, damageSourceProjectile, caster); + return true; + } + else if ( (hitentity->behavior == &actMonster || hitentity->behavior == &actPlayer) && targetStats ) + { + bool alertTarget = false; + if ( alertMonsters && caster && caster != hitentity ) + { + alertTarget = hitentity->monsterAlertBeforeHit(caster); + + // alert the monster! + if ( hitentity->monsterState != MONSTER_STATE_ATTACK && (targetStats->type < LICH || targetStats->type >= SHOPKEEPER) + && targetStats->type != GYROBOT ) + { + if ( alertTarget ) + { + bool oldPassable = caster->flags[PASSABLE]; + if ( spellID == SPELL_EARTH_ELEMENTAL && caster->behavior == &actMonster + && caster->getMonsterTypeFromSprite() == EARTH_ELEMENTAL ) + { + caster->flags[PASSABLE] = false; // let monsters aggro + } + hitentity->monsterAcquireAttackTarget(*caster, MONSTER_STATE_PATH, true); + caster->flags[PASSABLE] = oldPassable; + } + } + + // alert other monsters too + if ( alertTarget ) + { + hitentity->alertAlliesOnBeingHit(caster); + } + } + hitentity->updateEntityOnHit(caster, alertTarget); + + if ( spellID == SPELL_IGNITE + || spellID == SPELL_DISARM + || spellID == SPELL_STRIP + || spellID == SPELL_MAXIMISE + || spellID == SPELL_MINIMISE + || spellID == SPELL_COWARDICE + || spellID == SPELL_SEEK_ALLY + || spellID == SPELL_SEEK_FOE + || spellID == SPELL_COMMAND + || spellID == SPELL_CURSE_FLESH + || spellID == SPELL_REVENANT_CURSE ) + { + // alert entities only + return true; + } + + playSoundEntity(hitentity, 28, 128); + int oldHP = targetStats->HP; + + Sint32 preResistanceDamage = damage; + damage *= damageMultiplier; + + if ( spellID == SPELL_ICE_WAVE ) + { + real_t coldMultiplier = 1.0; + if ( targetStats && targetStats->helmet && targetStats->helmet->type == HAT_WARM ) + { + if ( !(hitentity->behavior == &actPlayer && hitentity->effectShapeshift != NOTHING) ) + { + if ( targetStats->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(targetStats) ) + { + coldMultiplier = std::max(0.0, 0.5 - 0.25 * (abs(targetStats->helmet->beatitude))); + } + else + { + coldMultiplier = 0.50; + } + } + } + damage *= coldMultiplier; + } + else if ( spellID == SPELL_FIRE_WALL ) + { + real_t fireMultiplier = 1.0; + if ( targetStats && targetStats->type == DRYAD ) + { + fireMultiplier += 0.2; + if ( !targetStats->helmet && targetStats->getEffectActive(EFF_GROWTH) > 1 ) + { + int bonus = std::min(3, targetStats->getEffectActive(EFF_GROWTH) - 1); + fireMultiplier += 0.05; + } + } + if ( targetStats->type == SALAMANDER ) + { + if ( targetStats->getEffectActive(EFF_SALAMANDER_HEART) == 1 + || targetStats->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + fireMultiplier *= 0.25; + } + else + { + fireMultiplier *= 0.5; + } + } + damage *= fireMultiplier; + } + if ( spellID == SPELL_DEFY_FLESH ) + { + damage = std::max(1, damage); + } + + hitentity->modHP(-damage); + if ( damage > 0 ) + { + Entity* gib = spawnGib(hitentity); + serverSpawnGibForClient(gib); + } + magicOnEntityHit(caster, &damageSourceProjectile, hitentity, targetStats, preResistanceDamage, damage, oldHP, spellID, usingSpellbookID); + magicTrapOnHit(caster, hitentity, targetStats, oldHP, spellID); + + // write the obituary + if ( caster ) + { + caster->killedByMonsterObituary(hitentity, true); + } + + // update enemy bar for attacker + if ( !strcmp(targetStats->name, "") ) + { + updateEnemyBar(caster, hitentity, getMonsterLocalizedName(targetStats->type).c_str(), targetStats->HP, targetStats->MAXHP, + false, dmgGib); + } + else + { + updateEnemyBar(caster, hitentity, targetStats->name, targetStats->HP, targetStats->MAXHP, + false, dmgGib); + } + if ( oldHP > 0 && targetStats->HP <= 0 && caster ) + { + bool xp = true; + if ( damageSourceProjectile.behavior == &actBoulder ) + { + if ( spellID == SPELL_NONE ) + { + xp = false; + } + } + if ( xp ) + { + caster->awardXP(hitentity, true, true); + } + + if ( spellID == SPELL_BOOBY_TRAP && caster->behavior == &actPlayer ) + { + steamStatisticUpdateClient(caster->skill[2], STEAM_STAT_BOOM_DYNAMITE, STEAM_STAT_INT, 1); + } + + spawnBloodVialOnMonsterDeath(hitentity, targetStats, caster); + } + + if ( hitentity->behavior == &actPlayer ) + { + if ( oldHP > targetStats->HP && targetStats->HP > 0 ) + { + int strength = 10; + if ( oldHP - targetStats->HP < 10 ) + { + strength = 5; + } + int player = hitentity->skill[2]; + // entity took damage, shake screen. + if ( multiplayer == SERVER && player > 0 ) + { + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = strength; // turns into .1 + net_packet->data[5] = strength; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + else if ( player == 0 || (splitscreen && player > 0) ) + { + cameravars[player].shakex += strength * 0.01; + cameravars[player].shakey += strength; + } + } + } + + return true; + } + else if ( hitentity->behavior == &actDoor + || hitentity->behavior == &::actIronDoor ) + { + hitentity->doorHandleDamageMagic(damage, damageSourceProjectile, caster); + return true; + } + else if ( hitentity->isDamageableCollider() && hitentity->isColliderDamageableByMagic() ) + { + hitentity->colliderHandleDamageMagic(damage, damageSourceProjectile, caster); + return true; + } + else if ( hitentity->behavior == &::actFurniture ) + { + hitentity->furnitureHandleDamageMagic(damage, damageSourceProjectile, caster); + return true; + } + + return false; +} + +Entity* spellEffectDemesneDoor(Entity& caster, Entity& target) +{ + auto entLists = TileEntityList.getEntitiesWithinRadiusAroundEntity(&target, 1); + int mapx = target.x / 16; + int mapy = target.y / 16; + for ( auto it : entLists ) + { + node_t* node; + for ( node = it->first; node != nullptr; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( static_cast(entity->x / 16) == mapx && static_cast(entity->y / 16) == mapy ) + { + if ( entity->behavior == &actDoor ) + { + entity->doorHealth = 0; + } + if ( entity->behavior == &actGate && entity->gateStatus == 0 ) + { + return nullptr; // no room + } + if ( entity->behavior == &actIronDoor && entity->doorStatus == 0 ) + { + return nullptr; // no room + } + if ( entity->behavior == &actParticleDemesneDoor ) + { + return nullptr; + } + } + } + } + } + + Entity* door = newEntity(1809, 1, map.entities, nullptr); + door->x = target.x; + door->y = target.y; + door->parent = caster.getUID(); + door->z = 0.0; + door->flags[UNCLICKABLE] = true; + door->flags[BLOCKSIGHT] = false; + door->flags[PASSABLE] = true; + door->flags[UPDATENEEDED] = true; + /*door->lightBonus = vec4(0.25, 0.25, + 0.25, 0.f);*/ + door->ditheringOverride = 4; + door->yaw = target.yaw; + if ( ((door->yaw > -PI / 4 && door->yaw < 1 * PI / 4) || door->yaw >= 7 * PI / 4) + || (door->yaw >= 3 * PI / 4 && door->yaw < 5 * PI / 4) ) + { + door->sizex = 1; + door->sizey = 8; + } + else + { + door->sizex = 8; + door->sizey = 1; + } + door->behavior = &actParticleDemesneDoor; + TileEntityList.addEntity(*door); + return door; +} + +int getSpellDamageFromID(int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus, bool applyingDamageOnCast) +{ + int damage = 0; + spellElement_t* element = nullptr; + int skillID = NUMPROFICIENCIES; + if ( auto spell = getSpellFromID(spellID) ) + { + skillID = spell->skillID; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + } + if ( element ) + { + Stat* myStats = parentStats; + if ( !myStats && parent ) + { + myStats = parent->getStats(); + } + damage = element->getDamage(); + if ( abs(element->getDamageMult()) > 0.01 ) + { + real_t bonus = (getBonusFromCasterOfSpellElement(parent, myStats, element, spellID, skillID)); + bonus += addSpellBonus; + if ( applyingDamageOnCast ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateCodex(parent->skill[2], Compendium_t::CPDM_CLASS_PWR_MAX_CASTED, "pwr", + (Sint32)(bonus * 100.0)); + } + } + + damage += damage * bonus * element->getDamageMult(); + if ( element->getDamageMult() > 0.01 && element->getDamage() > 0 ) + { + // range checking for PWR penalties, if we should do _some_ damage, then do at least 1 + damage = std::max(1, damage); + } + } + } + return damage; +} + +int getSpellDamageSecondaryFromID(int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus, bool applyingDamageOnCast) +{ + int damage = 0; + spellElement_t* element = nullptr; + int skillID = NUMPROFICIENCIES; + if ( auto spell = getSpellFromID(spellID) ) + { + skillID = spell->skillID; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + } + if ( element ) + { + Stat* myStats = parentStats; + if ( !myStats && parent ) + { + myStats = parent->getStats(); + } + damage = element->getDamageSecondary(); + if ( abs(element->getDamageSecondaryMult()) > 0.01 ) + { + real_t bonus = (getBonusFromCasterOfSpellElement(parent, myStats, element, spellID, skillID)); + bonus += addSpellBonus; + if ( applyingDamageOnCast ) + { + if ( parent && parent->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdateCodex(parent->skill[2], Compendium_t::CPDM_CLASS_PWR_MAX_CASTED, "pwr", + (Sint32)(bonus * 100.0)); + } + } + damage += damage * bonus * element->getDamageSecondaryMult(); + if ( element->getDamageSecondaryMult() > 0.01 && element->getDamageSecondary() > 0 ) + { + // range checking for PWR penalties, if we should do _some_ damage, then do at least 1 + damage = std::max(1, damage); + } + } + } + return damage; +} +int getSpellEffectDurationFromID(int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus) +{ + int duration = 0; + spellElement_t* element = nullptr; + if ( auto spell = getSpellFromID(spellID) ) + { + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + } + if ( element ) + { + duration = element->duration; + //real_t bonus = (getBonusFromCasterOfSpellElement(parent, parent ? parent->getStats() : nullptr, element, spellID)); + //bonus += addSpellBonus; + //duration += duration * bonus; + } + return duration; +} + +int getSpellEffectDurationSecondaryFromID(int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus) +{ + int duration = 0; + spellElement_t* element = nullptr; + if ( auto spell = getSpellFromID(spellID) ) + { + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + } + if ( element ) + { + duration = element->getDurationSecondary(); + //real_t bonus = (getBonusFromCasterOfSpellElement(parent, parent ? parent->getStats() : nullptr, element, spellID)); + //bonus += addSpellBonus; + //duration += duration * bonus; + } + return duration; +} + +real_t getSpellPropertyFromID(spell_t::SpellBasePropertiesFloat prop, int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus) +{ + spellElement_t* element = nullptr; + spell_t* spell = nullptr; + real_t result = 1.0; + if ( spell = getSpellFromID(spellID) ) + { + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + + Stat* myStats = parentStats; + if ( !myStats && parent ) + { + myStats = parent->getStats(); + } + if ( prop == spell_t::SpellBasePropertiesFloat::SPELLPROP_CAST_TIME ) + { + result = spell->cast_time; + } + else if ( prop == spell_t::SpellBasePropertiesFloat::SPELLPROP_DAMAGE_MULT ) + { + result = element->getDamageMult(); + } + else if ( prop == spell_t::SpellBasePropertiesFloat::SPELLPROP_DAMAGE_SECONDARY_MULT ) + { + result = element->getDamageSecondaryMult(); + } + else if ( prop == spell_t::SpellBasePropertiesFloat::SPELLPROP_CAST_TIME_MULT ) + { + result = spell->cast_time_mult; + } + else if ( prop == spell_t::SpellBasePropertiesFloat::SPELLPROP_MODIFIED_FOCI_CAST_TIME ) + { + result = spell->cast_time; + real_t modifier = 1.0;// +spell->cast_time_mult; + result *= modifier; + } + else if ( prop == spell_t::SpellBasePropertiesFloat::SPELLPROP_MODIFIED_SPELL_CAST_TIME ) + { + result = spell->cast_time; + if ( spell->cast_time_mult > 0.01 ) + { + real_t equipmentModifier = 0.0; + if ( myStats ) + { + if ( myStats->breastplate ) + { + if ( (myStats->breastplate->type == ROBE_HEALER && spell->skillID == PRO_THAUMATURGY) + || (myStats->breastplate->type == ROBE_WIZARD && spell->skillID == PRO_SORCERY) + || (myStats->breastplate->type == ROBE_CULTIST && spell->skillID == PRO_MYSTICISM) + || (myStats->breastplate->type == ROBE_MONK && spell->skillID == PRO_THAUMATURGY) ) + { + if ( myStats->breastplate->beatitude >= 0 || shouldInvertEquipmentBeatitude(myStats) ) + { + equipmentModifier = std::min(0.5, 0.2 + 0.1 * abs(myStats->breastplate->beatitude)); + } + else + { + equipmentModifier = -0.5; + } + } + } + } + if ( parent && parent->behavior == &actPlayer ) + { + equipmentModifier += 0.05 * players[parent->skill[2]]->mechanics.getBreakableCounterTier(); + } + real_t bonus = (getBonusFromCasterOfSpellElement(parent, myStats, element, spellID, spell->skillID)); + if ( spell->skillID == PRO_MYSTICISM || spell->skillID == PRO_SORCERY || spell->skillID == PRO_THAUMATURGY ) + { + if ( myStats && myStats->getModifiedProficiency(spell->skillID) >= SKILL_LEVEL_LEGENDARY ) + { + bonus += 1.0; + } + } + real_t modifier = (statGetDEX(myStats, parent) * (1.0 + std::max(0.0, bonus)) * spell->cast_time_mult) / 100.0; + result += -modifier; + if ( bonus < -0.05 ) + { + result *= (1 - bonus); + } + result *= (1 - std::min(1.0, equipmentModifier)); + if ( spell->cast_time < 1.01 ) + { + result = std::max(0.5, result); + } + else + { + result = std::max(1.0, result); + } + } + } + else if ( prop == spell_t::SpellBasePropertiesFloat::SPELLPROP_MODIFIED_DISTANCE ) + { + if ( spell->distance > 0.0 ) + { + real_t equipmentModifier = 0.0; + if ( myStats ) + { + if ( myStats->breastplate ) + { + if ( (myStats->breastplate->type == ROBE_HEALER && spell->skillID == PRO_THAUMATURGY) + || (myStats->breastplate->type == ROBE_WIZARD && spell->skillID == PRO_SORCERY) + || (myStats->breastplate->type == ROBE_CULTIST && spell->skillID == PRO_MYSTICISM) + || (myStats->breastplate->type == ROBE_MONK && spell->skillID == PRO_THAUMATURGY) ) + { + if ( myStats->breastplate->beatitude >= 0 || shouldInvertEquipmentBeatitude(myStats) ) + { + equipmentModifier = std::min(0.5, 0.2 + 0.1 * abs(myStats->breastplate->beatitude)); + } + else + { + equipmentModifier = -0.5; + } + } + else if ( myStats->breastplate->type == SHAWL ) + { + if ( myStats->breastplate->beatitude >= 0 || shouldInvertEquipmentBeatitude(myStats) ) + { + equipmentModifier = std::min(1.0, 0.35 + 0.35 * abs(myStats->breastplate->beatitude)); + } + else + { + equipmentModifier = -1.0; + } + } + } + } + real_t bonus = (getBonusFromCasterOfSpellElement(parent, myStats, element, spellID, spell->skillID)); + real_t modifier = (statGetPER(myStats, parent) * (1.0 + std::max(0.0, bonus)) * spell->distance_mult); + real_t maxDist = 96.0; + if ( equipmentModifier > 0.01 ) + { + maxDist = 128.0; + } + static ConsoleVariable cvar_spell_max_distance("/spell_max_distance", 96.0); + if ( svFlags & SV_FLAG_CHEATS ) + { + maxDist = *cvar_spell_max_distance; + } + result = std::min(maxDist, (spell->distance + modifier) * (1.0 + equipmentModifier)); + if ( bonus < -0.05 ) + { + result *= (1 + bonus); + } + result = std::max(16.0, result); + } + else + { + return 0.0; + } + } + } + return result; +} + +int getSpellPropertyFromID(spell_t::SpellBasePropertiesInt prop, int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus) +{ + spellElement_t* element = nullptr; + spell_t* spell = nullptr; + int result = 1.0; + if ( spell = getSpellFromID(spellID) ) + { + int propulsion = 0; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + propulsion = element->elementID; + element = (spellElement_t*)element->elements.first->element; + } + } + } + + Stat* myStats = parentStats; + if ( !myStats && parent ) + { + myStats = parent->getStats(); + } + if ( prop == spell_t::SpellBasePropertiesInt::SPELLPROP_FOCI_REFIRE_TICKS ) + { + if ( propulsion == SPELL_ELEMENT_PROPULSION_FOCI_SPRAY ) + { + if ( element ) + { + result = element->getChanneledManaDuration(); + real_t mult = 1.0; + if ( myStats ) + { + real_t modifier = std::min(100, myStats->getModifiedProficiency(spell->skillID)) / 100.0; + modifier *= element->getChanneledManaMult(); + int percent = modifier * 100; + percent += 100; + percent = std::max(0, percent); + result *= percent / 100.0; + } + } + } + } + else if ( prop == spell_t::SpellBasePropertiesInt::SPELLPROP_FOCI_SECONDARY_MANA_COST ) + { + if ( propulsion == SPELL_ELEMENT_PROPULSION_FOCI_SPRAY ) + { + if ( element ) + { + result = element->channeledMana; + } + } + } + else if ( prop == spell_t::SpellBasePropertiesInt::SPELLPROP_MODIFIED_RADIUS ) + { + result = spell->radius; + real_t radiusScale = 0.0; + real_t modifier = 1.0 + spell->radius_mult * radiusScale; + result *= modifier; + } + } + return result; +} + +int getSpellFromSummonedEntityForSpellEvent(Entity* summon) +{ + if ( !summon ) { return SPELL_NONE; } + if ( summon->behavior != &actMonster ) { return SPELL_NONE; } + + Stat* destStats = summon->getStats(); + if ( !destStats ) { return SPELL_NONE; } + + if ( !(summon->monsterAllyGetPlayerLeader() + || achievementObserver.checkUidIsFromPlayer(destStats->leader_uid) >= 0) ) + { + return SPELL_NONE; + } + + if ( summon->monsterAllySummonRank != 0 ) + { + if ( destStats->type == EARTH_ELEMENTAL ) + { + return SPELL_EARTH_ELEMENTAL; + } + else if ( destStats->type == SKELETON ) + { + return SPELL_SUMMON; + } + } + else if ( destStats->type == MONSTER_ADORCISED_WEAPON ) + { + if ( destStats->getAttribute("spirit_weapon") != "" ) + { + return SPELL_SPIRIT_WEAPON; + } + else if ( destStats->getAttribute("adorcised_weapon") != "" ) + { + return SPELL_ADORCISM; + } + } + else if ( destStats->type == MOTH_SMALL && destStats->getAttribute("fire_sprite") != "" ) + { + return SPELL_FIRE_SPRITE; + } + else if ( destStats->type == SKELETON && destStats->getAttribute("revenant_skeleton") != "" ) + { + return SPELL_REVENANT_CURSE; + } + else if ( destStats->type == REVENANT_SKULL && destStats->getAttribute("revenant_skull") != "" ) + { + return SPELL_CURSE_FLESH; + } + + return SPELL_NONE; +} + +int getSpellDamageFromStatic(int spellID, Stat* hitstats) +{ + if ( !hitstats ) + { + return 0; + } + + if ( hitstats->getEffectActive(EFF_STATIC) ) + { + return hitstats->getEffectActive(EFF_STATIC); + } + + return 0; +} + +void updateEntityOldHPBeforeMagicHit(Entity& my, Entity& projectile) +{ + if ( projectile.behavior == &actMagicMissile && projectile.actmagicUpdateOLDHPOnHit == 1 ) + { + if ( my.getStats() ) + { + my.getStats()->OLDHP = my.getStats()->HP; + } + else + { + if ( my.behavior == &actDoor || my.behavior == &actIronDoor ) + { + my.doorOldHealth = my.doorHealth; + } + else if ( my.behavior == &actFurniture ) + { + my.furnitureOldHealth = my.furnitureHealth; + } + else if ( my.behavior == &actChest ) + { + my.chestOldHealth = my.chestHealth; + } + else if ( my.isDamageableCollider() ) + { + my.colliderOldHP = my.colliderCurrentHP; + } + } + } +} \ No newline at end of file diff --git a/src/magic/magic.hpp b/src/magic/magic.hpp index cb7e3c697..66a16f8bd 100644 --- a/src/magic/magic.hpp +++ b/src/magic/magic.hpp @@ -12,6 +12,7 @@ //TODO: Spell icons: http://game-icons.net/ #pragma once +#include "../interface/interface.hpp" class Stat; @@ -79,8 +80,171 @@ static const int SPELL_SLIME_WATER = 57; static const int SPELL_SLIME_FIRE = 58; static const int SPELL_SLIME_TAR = 59; static const int SPELL_SLIME_METAL = 60; -static const int NUM_SPELLS = 61; - +static const int SPELL_FOCI_FIRE = 61; +static const int SPELL_FOCI_SNOW = 62; +static const int SPELL_FOCI_NEEDLES = 63; +static const int SPELL_FOCI_ARCS = 64; +static const int SPELL_FOCI_SANDBLAST = 65; +static const int SPELL_METEOR = 66; +static const int SPELL_FLAMES = 67; +static const int SPELL_ICE_WAVE = 68; +static const int SPELL_CONJURE_FOOD = 69; +static const int SPELL_GUARD_BODY = 70; +static const int SPELL_GUARD_SPIRIT = 71; +static const int SPELL_DIVINE_GUARD = 72; +static const int SPELL_PROF_NIMBLENESS = 73; +static const int SPELL_PROF_GREATER_MIGHT = 74; +static const int SPELL_PROF_COUNSEL = 75; +static const int SPELL_PROF_STURDINESS = 76; +static const int SPELL_BLESS_FOOD = 77; +static const int SPELL_PINPOINT = 78; +static const int SPELL_DONATION = 79; +static const int SPELL_SCRY_ALLIES = 80; +static const int SPELL_SCRY_SHRINES = 81; +static const int SPELL_SCRY_TRAPS = 82; +static const int SPELL_SCRY_TREASURES = 83; +static const int SPELL_PENANCE = 84; +static const int SPELL_CALL_ALLIES = 85; +static const int SPELL_SACRED_PATH = 86; +static const int SPELL_MANIFEST_DESTINY = 87; +static const int SPELL_DETECT_ENEMY = 88; +static const int SPELL_DETECT_ENEMIES = 89; +static const int SPELL_TURN_UNDEAD = 90; +static const int SPELL_HEAL_OTHER = 91; +static const int SPELL_BLOOD_WARD = 92; +static const int SPELL_TRUE_BLOOD = 93; +static const int SPELL_DIVINE_ZEAL = 94; +static const int SPELL_ALTER_INSTRUMENT = 95; +static const int SPELL_MAXIMISE = 96; +static const int SPELL_MINIMISE = 97; +static const int SPELL_JUMP = 98; +static const int SPELL_INCOHERENCE = 99; +static const int SPELL_OVERCHARGE = 100; +static const int SPELL_ENVENOM_WEAPON = 101; +static const int SPELL_PSYCHIC_SPEAR = 102; +static const int SPELL_DEFY_FLESH = 103; +static const int SPELL_GREASE_SPRAY = 104; +static const int SPELL_BLOOD_WAVES = 105; +static const int SPELL_BOOBY_TRAP = 106; +static const int SPELL_COMMAND = 107; +static const int SPELL_METALLURGY = 108; +static const int SPELL_GEOMANCY = 109; +static const int SPELL_FORGE_KEY = 110; +static const int SPELL_FORGE_JEWEL = 111; +static const int SPELL_ENHANCE_WEAPON = 112; +static const int SPELL_RESHAPE_WEAPON = 113; +static const int SPELL_ALTER_ARROW = 114; +static const int SPELL_VOID_CHEST = 115; +static const int SPELL_PUNCTURE_VOID = 116; +static const int SPELL_LEAD_BOLT = 117; +static const int SPELL_MERCURY_BOLT = 118; +static const int SPELL_NUMBING_BOLT = 119; +static const int SPELL_DELAY_PAIN = 120; +static const int SPELL_CURSE_FLESH = 121; +static const int SPELL_REVENANT_CURSE = 122; +static const int SPELL_COWARDICE = 123; +static const int SPELL_COURAGE = 124; +static const int SPELL_SEEK_ALLY = 125; +static const int SPELL_SEEK_FOE = 126; +static const int SPELL_DEEP_SHADE = 127; +static const int SPELL_SHADE_BOLT = 128; +static const int SPELL_SPIRIT_WEAPON = 129; +static const int SPELL_ADORCISM = 130; +static const int SPELL_TABOO = 131; +static const int SPELL_WONDERLIGHT = 132; +static const int SPELL_SPORES = 133; +static const int SPELL_SPORE_BOMB = 134; +static const int SPELL_WINDGATE = 135; +static const int SPELL_VORTEX = 136; +static const int SPELL_TELEKINESIS = 137; +static const int SPELL_KINETIC_PUSH = 138; +static const int SPELL_DISARM = 139; +static const int SPELL_STRIP = 140; +static const int SPELL_ABUNDANCE = 141; +static const int SPELL_GREATER_ABUNDANCE = 142; +static const int SPELL_PRESERVE = 143; +static const int SPELL_RESTORE = 144; +static const int SPELL_SABOTAGE = 145; +static const int SPELL_HARVEST_TRAP = 146; +static const int SPELL_MIST_FORM = 147; +static const int SPELL_HOLOGRAM = 148; +static const int SPELL_FORCE_SHIELD = 149; +static const int SPELL_REFLECTOR = 150; +static const int SPELL_SPLINTER_GEAR = 151; +static const int SPELL_LIGHTEN_LOAD = 152; +static const int SPELL_ATTRACT_ITEMS = 153; +static const int SPELL_RETURN_ITEMS = 154; +static const int SPELL_ABSORB_MAGIC = 155; +static const int SPELL_SEIZE_MAGIC = 156; +static const int SPELL_DEFACE = 157; +static const int SPELL_SUNDER_MONUMENT = 158; +static const int SPELL_DEMESNE_DOOR = 159; +static const int SPELL_TUNNEL = 160; +static const int SPELL_NULL_AREA = 161; +static const int SPELL_SPHERE_SILENCE = 162; +static const int SPELL_FORGE_METAL_SCRAP = 163; +static const int SPELL_FORGE_MAGIC_SCRAP = 164; +static const int SPELL_FIRE_SPRITE = 165; +static const int SPELL_FLAME_ELEMENTAL = 166; +static const int SPELL_SPIN = 167; +static const int SPELL_DIZZY = 168; +static const int SPELL_VANDALISE = 169; +static const int SPELL_DESECRATE = 170; +static const int SPELL_SANCTIFY = 171; +static const int SPELL_SANCTIFY_WATER = 172; +static const int SPELL_CLEANSE_FOOD = 173; +static const int SPELL_ADORCISE_INSTRUMENT = 174; +static const int SPELL_FLAME_CLOAK = 175; +static const int SPELL_CRITICAL_SPELL = 176; +static const int SPELL_MAGIC_WELL = 177; +static const int SPELL_FLAME_SHIELD = 178; +static const int SPELL_LIGHTNING_BOLT = 179; +static const int SPELL_DISRUPT_EARTH = 180; +static const int SPELL_EARTH_SPINES = 181; +static const int SPELL_LIGHTNING_NEXUS = 182; +static const int SPELL_FIRE_WALL = 183; +static const int SPELL_LIFT = 184; +static const int SPELL_SLAM = 185; +static const int SPELL_IGNITE = 186; +static const int SPELL_SHATTER_OBJECTS = 187; +static const int SPELL_KINETIC_FIELD = 188; +static const int SPELL_ICE_BLOCK = 189; +static const int SPELL_METEOR_SHOWER = 190; +static const int SPELL_CHRONOMIC_FIELD = 191; +static const int SPELL_ETERNALS_GAZE = 192; +static const int SPELL_SHATTER_EARTH = 193; +static const int SPELL_EARTH_ELEMENTAL = 194; +static const int SPELL_ROOTS = 195; +static const int SPELL_MUSHROOM = 196; +static const int SPELL_MYCELIUM_BOMB = 197; +static const int SPELL_MYCELIUM_SPORES = 198; +static const int SPELL_HEAL_PULSE = 199; +static const int SPELL_SHRUB = 200; +static const int SPELL_THORNS = 201; +static const int SPELL_BLADEVINES = 202; +static const int SPELL_BASTION_MUSHROOM = 203; +static const int SPELL_BASTION_ROOTS = 204; +static const int SPELL_FOCI_DARK_LIFE = 205; +static const int SPELL_FOCI_DARK_RIFT = 206; +static const int SPELL_FOCI_DARK_SILENCE = 207; +static const int SPELL_FOCI_DARK_VENGEANCE = 208; +static const int SPELL_FOCI_DARK_SUPPRESS = 209; +static const int SPELL_FOCI_LIGHT_PEACE = 210; +static const int SPELL_FOCI_LIGHT_JUSTICE = 211; +static const int SPELL_FOCI_LIGHT_PROVIDENCE = 212; +static const int SPELL_FOCI_LIGHT_PURITY = 213; +static const int SPELL_FOCI_LIGHT_SANCTUARY = 214; +static const int SPELL_SCEPTER_BLAST = 215; +static const int SPELL_MAGICIANS_ARMOR = 216; +static const int SPELL_PROJECT_SPIRIT = 217; +static const int SPELL_DUCK_DIVE = 218; +static const int SPELL_BREATHE_FIRE = 219; +static const int SPELL_HEAL_MINOR = 220; +static const int SPELL_HOLY_FIRE = 221; +static const int SPELL_SIGIL = 222; +static const int SPELL_SANCTUARY = 223; +static const int SPELL_HOLY_BEAM = 224; +static const int NUM_SPELLS = 225; #define SPELLELEMENT_CONFUSE_BASE_DURATION 2//In seconds. #define SPELLELEMENT_BLEED_BASE_DURATION 10//In seconds. @@ -153,6 +317,73 @@ static const int PARTICLE_EFFECT_SHATTERED_GEM = 25; static const int PARTICLE_EFFECT_SHRINE_TELEPORT = 26; static const int PARTICLE_EFFECT_GHOST_TELEPORT = 27; static const int PARTICLE_EFFECT_SLIME_SPRAY = 28; +static const int PARTICLE_EFFECT_FOCI_SPRAY = 29; +static const int PARTICLE_EFFECT_ENSEMBLE_SELF_CAST = 30; +static const int PARTICLE_EFFECT_ENSEMBLE_OTHER_CAST = 31; +static const int PARTICLE_EFFECT_STATIC_ORBIT = 32; +static const int PARTICLE_EFFECT_LIGHTNING_SEQ = 33; +static const int PARTICLE_EFFECT_PINPOINT = 34; +static const int PARTICLE_EFFECT_DESTINY_TELEPORT = 35; +static const int PARTICLE_EFFECT_BOOBY_TRAP = 36; +static const int PARTICLE_EFFECT_SPORE_BOMB = 37; +static const int PARTICLE_EFFECT_WINDGATE = 38; +static const int PARTICLE_EFFECT_DEMESNE_DOOR = 39; +static const int PARTICLE_EFFECT_NULL_PARTICLE = 40; +static const int PARTICLE_EFFECT_VORTEX_ORBIT = 41; +static const int PARTICLE_EFFECT_SPIN = 42; +static const int PARTICLE_EFFECT_AREA_EFFECT = 43; +static const int PARTICLE_EFFECT_ETERNALS_GAZE1 = 44; +static const int PARTICLE_EFFECT_ETERNALS_GAZE2 = 45; +static const int PARTICLE_EFFECT_ETERNALS_GAZE_STATIC = 46; +static const int PARTICLE_EFFECT_SHATTER_EARTH_ORBIT = 47; +static const int PARTICLE_EFFECT_EARTH_ELEMENTAL_DIE = 48; +static const int PARTICLE_EFFECT_MUSHROOM_SPELL = 49; +static const int PARTICLE_EFFECT_MISC_PUDDLE = 50; +static const int PARTICLE_EFFECT_BOLAS = 51; +static const int PARTICLE_EFFECT_BASTION_MUSHROOM = 52; +static const int PARTICLE_EFFECT_FOCI_ORBIT = 53; +static const int PARTICLE_EFFECT_SCEPTER_BLAST_ORBIT1 = 54; +static const int PARTICLE_EFFECT_STASIS_CAGE_ORBIT = 55; +static const int PARTICLE_EFFECT_STASIS_RIFT_ORBIT = 56; +static const int PARTICLE_EFFECT_EARTH_ELEMENTAL_SUMMON_AOE = 57; +static const int PARTICLE_EFFECT_THORNS_ORBIT = 58; +static const int PARTICLE_EFFECT_CONFUSE_ORBIT = 59; +static const int PARTICLE_EFFECT_IGNITE_ORBIT = 60; +static const int PARTICLE_EFFECT_IGNITE = 61; +static const int PARTICLE_EFFECT_SHATTER_OBJECTS = 62; +static const int PARTICLE_EFFECT_SABOTAGE_ORBIT = 63; +static const int PARTICLE_EFFECT_SABOTAGE_TRAP = 64; +static const int PARTICLE_EFFECT_IGNITE_ORBIT_LOOP = 65; +static const int PARTICLE_EFFECT_IGNITE_ORBIT_FOLLOW = 66; +static const int PARTICLE_EFFECT_FLAMES = 67; +static const int PARTICLE_EFFECT_FLAMES_BURNING = 68; +static const int PARTICLE_EFFECT_SUMMON_FLAMES = 69; +static const int PARTICLE_EFFECT_MAGICIANS_ARMOR_ORBIT = 70; +static const int PARTICLE_EFFECT_GUARD_BODY_ORBIT = 71; +static const int PARTICLE_EFFECT_GUARD_SPIRIT_ORBIT = 72; +static const int PARTICLE_EFFECT_GUARD_DIVINE_ORBIT = 73; +static const int PARTICLE_EFFECT_METEOR_STATIONARY_ORBIT = 74; +static const int PARTICLE_EFFECT_DUCK_SPAWN_FEATHER = 75; +static const int PARTICLE_EFFECT_STATIC_MAXIMISE = 76; +static const int PARTICLE_EFFECT_NULL_PARTICLE_NOSOUND = 77; +static const int PARTICLE_EFFECT_CONTROL = 78; +static const int PARTICLE_EFFECT_REVENANT_CURSE = 79; +static const int PARTICLE_EFFECT_TURN_UNDEAD = 80; +static const int PARTICLE_EFFECT_PSYCHIC_SPEAR = 81; +static const int PARTICLE_EFFECT_DEFY_FLESH = 82; +static const int PARTICLE_EFFECT_DEFY_FLESH_ORBIT = 83; +static const int PARTICLE_EFFECT_BLOOD_WAVES_ORBIT = 84; +static const int PARTICLE_EFFECT_BLOOD_BUBBLE = 85; +static const int PARTICLE_EFFECT_RADIANT_ORBIT_FOLLOW = 86; +static const int PARTICLE_EFFECT_HEAT_ORBIT_SPIN = 87; +static const int PARTICLE_EFFECT_FOCI_LIGHT = 88; +static const int PARTICLE_EFFECT_FOCI_DARK = 89; +static const int PARTICLE_EFFECT_HEAL_OTHER = 90; +static const int PARTICLE_EFFECT_MINOR_HEAL = 91; +static const int PARTICLE_EFFECT_BLOOD_WARD_ORBIT = 92; +static const int PARTICLE_EFFECT_SMITE_PINPOINT = 93; +static const int PARTICLE_EFFECT_HOLY_FIRE = 94; +static const int PARTICLE_EFFECT_HOLY_BEAM_ORBIT = 95; // actmagicIsVertical constants static const int MAGIC_ISVERTICAL_NONE = 0; @@ -166,13 +397,89 @@ static const int PARTICLE_TIMER_ACTION_SUMMON_MONSTER = 3; static const int PARTICLE_TIMER_ACTION_SPELL_SUMMON = 4; static const int PARTICLE_TIMER_ACTION_DEVIL_SUMMON_MONSTER = 5; static const int PARTICLE_TIMER_ACTION_MAGIC_SPRAY = 6; +static const int PARTICLE_TIMER_ACTION_FOCI_SPRAY = 7; +static const int PARTICLE_TIMER_ACTION_MAGIC_WAVE = 8; +static const int PARTICLE_TIMER_ACTION_TEST_1 = 9; +static const int PARTICLE_TIMER_ACTION_TEST_2 = 10; +static const int PARTICLE_TIMER_ACTION_IGNITE = 11; +static const int PARTICLE_TIMER_ACTION_SHATTER = 12; +static const int PARTICLE_TIMER_ACTION_VORTEX = 13; +static const int PARTICLE_TIMER_ACTION_LIGHTNING = 14; +static const int PARTICLE_TIMER_ACTION_BOOBY_TRAP = 15; +static const int PARTICLE_TIMER_ACTION_SPORES = 16; +static const int PARTICLE_TIMER_ACTION_DAMAGE_LOS_AREA = 17; +static const int PARTICLE_TIMER_ACTION_DISRUPT_EARTH = 18; +static const int PARTICLE_TIMER_ACTION_ETERNALS_GAZE = 19; +static const int PARTICLE_TIMER_TELEPORT_PULL_TARGET_LOCATION = 20; +static const int PARTICLE_TIMER_ACTION_ETERNALS_GAZE2 = 21; +static const int PARTICLE_TIMER_ACTION_SHATTER_EARTH = 22; +static const int PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL = 23; +static const int PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL_ROLL = 24; +static const int PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL_DIE = 25; +static const int PARTICLE_TIMER_ACTION_ROOTS1 = 26; +static const int PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE = 27; +static const int PARTICLE_TIMER_ACTION_ROOTS_PATH = 28; +static const int PARTICLE_TIMER_ACTION_SPORES_TRAIL = 29; +static const int PARTICLE_TIMER_ACTION_ROOTS_SUSTAIN = 30; +static const int PARTICLE_TIMER_ACTION_BASTION_MUSHROOM = 31; +static const int PARTICLE_TIMER_ACTION_ROOTS_SINGLE_TILE_VOID = 32; +static const int PARTICLE_TIMER_ACTION_TRAP_SABOTAGED = 33; +static const int PARTICLE_TIMER_ACTION_SPLINTER_GEAR = 34; +static const int PARTICLE_TIMER_ACTION_SPIRIT_WEAPON_ATTACK = 35; +static const int PARTICLE_TIMER_ACTION_SWEEP_ATTACK = 36; struct ParticleEmitterHit_t { Uint32 tick = 0; int hits = 0; }; +struct ParticleTimerEffect_t +{ + enum EffectType + { + EFFECT_NONE, + EFFECT_ICE_WAVE, + EFFECT_TEST_1, + EFFECT_TEST_2, + EFFECT_TEST_3, + EFFECT_DISRUPT_EARTH, + EFFECT_LIGHTNING_BOLT, + EFFECT_ROOTS_SELF, + EFFECT_TEST_7, + EFFECT_FIRE_WAVE, + EFFECT_KINETIC_FIELD, + EFFECT_PULSE, + EFFECT_SPORES, + EFFECT_TUNNEL, + EFFECT_CHRONOMIC_FIELD, + EFFECT_ROOTS_TILE, + EFFECT_ROOTS_PATH, + EFFECT_MYCELIUM, + EFFECT_ROOTS_SELF_SUSTAIN, + EFFECT_ROOTS_TILE_VOID + }; + struct Effect_t + { + real_t x = 0.0; + real_t y = 0.0; + EffectType effectType = EFFECT_NONE; + real_t yaw = 0.0; + int sfx = 0; + bool firstEffect = false; + }; + + struct EffectLocations_t + { + real_t yawOffset = 0.0; + real_t xOffset = 0.0; + real_t seconds = 0.0; + real_t dist = 0.0; + int sfx = 0; + }; + std::map effectMap; +}; extern std::map> particleTimerEmitterHitEntities; +extern std::map particleTimerEffects; ParticleEmitterHit_t* getParticleEmitterHitProps(Uint32 emitterUid, Entity* hitentity); bool addSpell(int spell, int player, bool ignoreSkill = false); //Adds a spell to the client's spell list. Note: Do not use this to add custom spells. @@ -186,13 +493,41 @@ typedef struct spellElement_t spellElement_t; //TODO: Don't re-invent the wheel with lists here. typedef struct spellElement_t { - int mana, base_mana; - int overload_multiplier; // what does this do? - int damage; +private: + int damage = 0; + int damage2 = 0; + int duration2 = 0; + real_t damage_mult = 1.0; + real_t damage2_mult = 1.0; + real_t channeledMana_mult = 1.0; + real_t duration_mult = 0.0; + real_t duration2_mult = 0.0; + int channeledMana_duration = TICKS_PER_SECOND; +public: + void setDamage(int _damage) { damage = _damage; } + int getDamage() { return damage; } + void setDamageSecondary(int _damage) { damage2 = _damage; } + int getDamageSecondary() { return damage2; } + void setDurationSecondary(int _duration) { duration2 = _duration; } + int getDurationSecondary() { return duration2; } + real_t getDamageMult() { return damage_mult; } + real_t getDamageSecondaryMult() { return damage2_mult; } + real_t getDurationMult() { return duration_mult; } + real_t getDurationSecondaryMult() { return duration2_mult; } + void setDamageMult(real_t _mult) { damage_mult = _mult; } + void setDamageSecondaryMult(real_t _mult) { damage2_mult = _mult; } + void setDurationMult(real_t _mult) { duration_mult = _mult; } + void setDurationSecondaryMult(real_t _mult) { duration2_mult = _mult; } + real_t getChanneledManaMult() { return channeledMana_mult; } + int getChanneledManaDuration() { return channeledMana_duration; } + void setChanneledManaDuration(int _duration) { channeledMana_duration = _duration; } + void setChanneledManaMult(real_t _mult) { channeledMana_mult = _mult; } int duration; // travel time if it's a missile element, duration for a light spell, duration for curses/enchants/traps/beams/rays/effects/what have you. char element_internal_name[64]; + int elementID = 0; bool can_be_learned; // if a spellElement can't be learned, a player won't be able to build spells with it. - bool channeled; // false by default. Specific spells can set this to true. Channeling it sustains the effect in some fashion. It reconsumes the casting mana after every duration has expired. + int channeledMana = 0; // sustained spell if channeled cost > 0 + bool fociSpell = false; /* I've been thinking about mana consumption. I think it should drain mana 1 by 1 regardless of how much the spell initially cost to cast. So: @@ -421,6 +756,33 @@ extern spellElement_t spellElement_slimeFire; extern spellElement_t spellElement_slimeTar; extern spellElement_t spellElement_slimeMetal; extern spellElement_t spellElement_slime_spray; +extern std::map spellElementMap; + +enum SpellElementIDs_t +{ + SPELL_ELEMENT_NONE = 10000, + SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, + SPELL_ELEMENT_PROPULSION_MISSILE, + SPELL_ELEMENT_METEOR_FLAMES, + SPELL_ELEMENT_PROPULSION_FLOOR_TILE, + SPELL_ELEMENT_PROPULSION_MAGIC_SPRAY, + SPELL_ELEMENT_PROPULSION_MISSILE_NOCOST, + SPELL_ELEMENT_SPRITE_FLAMES, + SPELL_ELEMENT_METEOR_EXPLODE, + SPELL_ELEMENT_MAX +}; + +enum SpellRangefinderType +{ + RANGEFINDER_NONE, + RANGEFINDER_TARGET, + RANGEFINDER_TOUCH, + RANGEFINDER_TOUCH_FLOOR_TILE, + RANGEFINDER_TOUCH_WALL_TILE, + RANGEFINDER_TOUCH_INTERACT, + RANGEFINDER_TOUCH_INTERACT_TEST +}; + /* */ //TODO: Differentiate between touch spells, enchantment spells, personal spells, ranged spells, area of effect spells, close blast/burst spells, and enemy/ally target spells. @@ -439,19 +801,78 @@ typedef struct spell_t //int skill_caster; //The spellcasting skill it was cast with. Lower skill can introduce inefficiencies and other !!FUN!! bool sustain; //If a spell is channeled, should it be sustained? (NOTE: True by default. Set to false when the player decides to cancel/abandon a spell) bool magicstaff; // if true the spell was cast from a magicstaff and thus it may have slightly different behavior - node_t* sustain_node; //Node in the sustained/channeled spells list. - node_t* magic_effects_node; - Uint32 caster; - int channel_duration; //This is the value to reset the timer to when a spell is channeled. + bool spellbook = false; + node_t* sustain_node = nullptr; //Node in the sustained/channeled spells list. + node_t* magic_effects_node = nullptr; + bool hide_from_ui = false; // hide from skillsheet/other UI places + SpellRangefinderType rangefinder = SpellRangefinderType::RANGEFINDER_NONE; + + Uint32 caster = 0; + real_t distance = 0.0; + real_t distance_mult = 1.0; + int skillID = PRO_SORCERY; + real_t cast_time = 1.0; + real_t cast_time_mult = 1.0; + int mana = 1; + int needsDataFreed = 0; + + int radius = 0; + real_t radius_mult = 0.0; + int drop_table = -1; + int life_time = 0; // for floor based effects + real_t life_time_mult = 1.0; + int sustainEffectDissipate = -1; // when the spell is unsustained, clear this effect from the player (unique spell effects) + int channel_duration = 0; //This is the value to reset the timer to when a spell is channeled. + int channel_effectStrength = 1; // how strong to reapply the effect each duration tick list_t elements; //NOTE: This could technically allow a spell to have multiple roots. So you could make a flurry of fireballs, for example. //TODO: Some way to make spells work with "need to cast more to get better at casting the spell." A sort of spell learning curve. The first time you cast it, prone to failure. Less the more you cast it. + enum SpellBasePropertiesFloat + { + SPELLPROP_MODIFIED_DISTANCE, + SPELLPROP_DISTANCE_MULT, + SPELLPROP_CAST_TIME, + SPELLPROP_CAST_TIME_MULT, + SPELLPROP_MODIFIED_FOCI_CAST_TIME, + SPELLPROP_MODIFIED_SPELL_CAST_TIME, + SPELLPROP_DAMAGE_MULT, + SPELLPROP_DAMAGE_SECONDARY_MULT, + SPELLPROP_BASE_PROPERTY_FLOAT_ENUM_END + }; + + enum SpellBasePropertiesInt + { + SPELLPROP_FOCI_REFIRE_TICKS, + SPELLPROP_FOCI_SECONDARY_MANA_COST, + SPELLPROP_MODIFIED_RADIUS, + SPELLPROP_BASE_PROPERTY_INT_ENUM_END + }; + + enum SpellOnCastEventTypes + { + SPELL_LEVEL_EVENT_DEFAULT = 1, + SPELL_LEVEL_EVENT_DMG = 2, + SPELL_LEVEL_EVENT_EFFECT = 4, + SPELL_LEVEL_EVENT_SUMMON = 8, + SPELL_LEVEL_EVENT_SHAPESHIFT = 16, + SPELL_LEVEL_EVENT_SUSTAIN = 32, + SPELL_LEVEL_EVENT_MAGICSTAFF = 64, + SPELL_LEVEL_EVENT_SPELLBOOK = 128, + SPELL_LEVEL_EVENT_ASSIST = 256, + SPELL_LEVEL_EVENT_MINOR_CHANCE = 512, + SPELL_LEVEL_EVENT_ALWAYS = 1024, + SPELL_LEVEL_EVENT_ENUM_END = 2048 + }; + // get localized spell name - const char* getSpellName(); + const char* getSpellName(bool lowercase = false); + const char* getSpellTierName(); } spell_t; extern list_t channeledSpells[MAXPLAYERS]; //Spells the player is currently channeling. //TODO: Universalize it for all entities that can cast spells? //TODO: Cleanup and stuff. -extern std::vector allGameSpells; // to iterate over for quickly finding attributes of all spells. +extern std::map allGameSpells; // to iterate over for quickly finding attributes of all spells. +extern std::map> spellTomeAppearanceToID; // school, then appearance, then spell ID +extern std::map spellTomeIDToAppearance; // spell ID to appearance of school //TODO: Add stock spells. @@ -523,20 +944,39 @@ extern spell_t spell_slime_metal; //TODO: Armor/protection/warding spells. //TODO: Targeting method? +struct CastSpellProps_t +{ + real_t caster_x = 0.0; + real_t caster_y = 0.0; + real_t target_x = 0.0; + real_t target_y = 0.0; + Uint32 targetUID = 0; + int elementIndex = 0; + real_t distanceOffset = 0.0; + int wallDir = 0; + Uint8 optionalData = 0; + Uint8 overcharge = 0; + bool setToMonsterCast(Entity* monster, int spellID); +}; + void setupSpells(); void equipSpell(spell_t* spell, int playernum, Item* spellItem); -Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool trap, bool usingSpellbook = false); -void castSpellInit(Uint32 caster_uid, spell_t* spell, bool usingSpellbook); //Initiates the spell animation, then hands off the torch to it, which, when finished, calls castSpell. +Entity* castSpell(Uint32 caster_uid, spell_t* spell, bool using_magicstaff, bool trap, bool usingSpellbook = false, CastSpellProps_t* castSpellProps = nullptr, bool usingFoci = false); +void castSpellInit(Uint32 caster_uid, spell_t* spell, bool usingSpellbook, bool usingTome); //Initiates the spell animation, then hands off the torch to it, which, when finished, calls castSpell. int spellGetCastSound(spell_t* spell); #ifndef EDITOR // editor doesn't know about stat* int getSpellcastingAbilityFromUsingSpellbook(spell_t* spell, Entity* caster, Stat* casterStats); bool isSpellcasterBeginnerFromSpellbook(int player, Entity* caster, Stat* stat, spell_t* spell, Item* spellbookItem); int getSpellbookBonusPercent(Entity* caster, Stat* stat, Item* spellbookItem); -real_t getBonusFromCasterOfSpellElement(Entity* caster, Stat* casterStats, spellElement_t* spellElement, int spellID); -real_t getSpellBonusFromCasterINT(Entity* caster, Stat* casterStats); -void magicOnEntityHit(Entity* parent, Entity* particle, Entity* hitentity, Stat* hitstats, Sint32 preResistanceDamage, Sint32 damage, Sint32 oldHP, int spellID); +real_t getBonusFromCasterOfSpellElement(Entity* caster, Stat* casterStats, spellElement_t* spellElement, int spellID, int proficiencyWhenNoSpell); +real_t getSpellBonusFromCasterINT(Entity* caster, Stat* casterStats, int skillID); +int getSpellbookBaseINTBonus(Entity* caster, Stat* casterStats, int skillID); +void magicOnEntityHit(Entity* parent, Entity* particle, Entity* hitentity, Stat* hitstats, Sint32 preResistanceDamage, Sint32 damage, Sint32 oldHP, int spellID, int selfCastUsingItem = 0); +void magicTrapOnHit(Entity* parent, Entity* hitentity, Stat* hitstats, Sint32 oldHP, int spellID); +bool applyGenericMagicDamage(Entity* caster, Entity* hitentity, Entity& damageSourceProjectile, int spellID, int damage, bool alertMonsters, + bool monsterCollisionOnly = false, int usingSpellbookID = 0); #endif -bool isSpellcasterBeginner(int player, Entity* caster); +bool isSpellcasterBeginner(int player, Entity* caster, int skillID); void actMagicTrap(Entity* my); void actMagicStatusEffect(Entity* my); void actMagicMissile(Entity* my); @@ -544,7 +984,12 @@ void actMagicClient(Entity* my); void actMagicClientNoLight(Entity* my); void actMagicParticle(Entity* my); void actHUDMagicParticle(Entity* my); +void actTouchCastThirdPersonParticle(Entity* my); void actHUDMagicParticleCircling(Entity* my); +void actMagicParticleCircling2(Entity* my); +void actMagicParticleEnsembleCircling(Entity* my); +void createEnsembleHUDParticleCircling(Entity* parent); +void createEnsembleTargetParticleCircling(Entity* parent); Entity* spawnMagicParticle(Entity* parentent); Entity* spawnMagicParticleCustom(Entity* parentent, int sprite, real_t scale, real_t spreadReduce); void spawnMagicEffectParticles(Sint16 x, Sint16 y, Sint16 z, Uint32 sprite); @@ -562,37 +1007,88 @@ void actParticleExplosionCharge(Entity* my); void actParticleFollowerCommand(Entity* my); void actParticleCharmMonster(Entity* my); void actParticleAestheticOrbit(Entity* my); +void actParticleBolas(Entity* my); void actParticleShadowTag(Entity* my); +void actParticlePinpointTarget(Entity* my); +void actParticleFloorMagic(Entity* my); +void actParticleVortex(Entity* my); +void actParticleWave(Entity* my); +void actParticleRoot(Entity* my); +void actParticleDemesneDoor(Entity* my); +void actRadiusMagic(Entity* my); +void actRadiusMagicBadge(Entity* my); +void actParticleShatterEarth(Entity* my); +void actParticleShatterEarthRock(Entity* my); void createParticleDropRising(Entity* parent, int sprite, double scale); void createParticleDot(Entity* parent); +Entity* createParticleBolas(Entity* parent, int sprite, int duration, Item* item); Entity* createParticleAestheticOrbit(Entity* parent, int sprite, int duration, int particleType); void createParticleRock(Entity* parent, int sprite = -1, bool light = false); void createParticleShatteredGem(real_t x, real_t y, real_t z, int sprite, Entity* parent); void createParticleErupt(Entity* parent, int sprite); +void createParticleErupt(real_t x, real_t y, int sprite); +Entity* createParticleBoobyTrapExplode(Entity* caster, real_t x, real_t y); +Entity* createParticleShatterObjects(Entity* caster); +Entity* createParticleIgnite(Entity* caster); Entity* createParticleSapCenter(Entity* parent, Entity* target, int spell, int sprite, int endSprite); Entity* createParticleTimer(Entity* parent, int duration, int sprite); void createParticleSap(Entity* parent); void createParticleExplosionCharge(Entity* parent, int sprite, int particleCount, double scale); void createParticleFollowerCommand(real_t x, real_t y, real_t z, int sprite, Uint32 uid); +Entity* createParticleCastingIndicator(Entity* parent, real_t x, real_t y, real_t z, Uint32 lifetime, Uint32 followUid); +Entity* createParticleAOEIndicator(Entity* parent, real_t x, real_t y, real_t z, Uint32 lifetime, int size); static const int FOLLOWER_SELECTED_PARTICLE = 1229; static const int FOLLOWER_TARGET_PARTICLE = 1230; void createParticleCharmMonster(Entity* parent); void createParticleShadowTag(Entity* parent, Uint32 casterUid, int duration); +static const int PINPOINT_PARTICLE_START = 1767; +static const int PINPOINT_PARTICLE_END = 1782; +Entity* createParticleSpellPinpointTarget(Entity* parent, Uint32 casterUid, int sprite, int duration, int spellID); +Entity* createFloorMagic(ParticleTimerEffect_t::EffectType particleType, int sprite, real_t x, real_t y, real_t z, real_t dir, Uint32 lifetime); +Entity* createRadiusMagic(int spellID, Entity* caster, real_t x, real_t y, real_t radius, Uint32 lifetime, Entity* follow); +void floorMagicClientReceive(Entity* my); +void particleWaveClientReceive(Entity* my); +void radiusMagicClientReceive(Entity* entity); +Entity* floorMagicSetLightningParticle(Entity* my); +void floorMagicCreateLightningSequence(Entity* spellTimer, int startTickOffset); +void floorMagicCreateSpores(Entity* spawnOnEntity, real_t x, real_t y, Entity* caster, int damage, int spellID); +Entity* floorMagicCreateRoots(real_t x, real_t y, Entity* caster, int damage, int spellID, int duration, int particleTimerAction); +Entity* createVortexMagic(int sprite, real_t x, real_t y, real_t z, real_t dir, Uint32 lifetime); +Entity* createParticleWave(ParticleTimerEffect_t::EffectType particleType, int sprite, real_t x, real_t y, real_t z, real_t dir, Uint32 lifetime, bool light); +Entity* createParticleRoot(int sprite, real_t x, real_t y, real_t z, real_t dir, Uint32 lifetime); +void createMushroomSpellEffect(Entity* caster, real_t x, real_t y); +Entity* createWindMagic(Uint32 casterUID, int x, int y, int duration, int dir, int length); +void createParticleDemesneDoor(real_t x, real_t y, real_t dir); +Entity* createTunnelPortal(real_t x, real_t y, int duration, int dir, Entity* caster); +void tunnelPortalSetAttributes(Entity* portal, int duration, int dir); +Entity* createSpellExplosionArea(int spellID, Entity* caster, real_t x, real_t y, real_t z, real_t radius, int damage, Entity* ohitentity); +void doSpellExplosionArea(int spellID, Entity* my, Entity* caster, real_t x, real_t y, real_t z, real_t radius); +void createParticleSpin(Entity* entity); +void createParticleShatterEarth(Entity* my, Entity* caster, real_t _x, real_t _y, int spellID); +void actEarthElementalDeathGib(Entity* my); +void actLeafParticle(Entity* my); +void actLeafPile(Entity* my); +Entity* spawnLeafPile(real_t x, real_t y, bool trap); +int thaumSpellArmorProc(Entity* my, Stat& myStats, bool checkEffectActiveOnly, Entity* attacker, int effectID); void spawnMagicTower(Entity* parent, real_t x, real_t y, int spellID, Entity* autoHitTarget, bool castedSpell = false); // autoHitTarget is to immediate damage an entity, as all 3 tower magics hitting is unreliable bool magicDig(Entity* parent, Entity* projectile, int numRocks, int randRocks); -spell_t* copySpell(spell_t* spell); -void spellConstructor(spell_t* spell); +spell_t* copySpell(spell_t* spell, int subElementToCopy = -1); +void spellConstructor(spell_t* spell, int ID); +spell_t* spellConstructor(int ID, int difficulty, const char* internal_name, std::vector elements); void spellDeconstructor(void* data); -spellElement_t* newSpellElement(); +void spellChanneledClientDeconstructor(void* data); +void copySpellElement(spellElement_t* spellElement, spellElement_t* spellElementToSet); spellElement_t* copySpellElement(spellElement_t* spellElement); void spellElementConstructor(spellElement_t* element); +void spellElementConstructor(int elementID, int mana, int base_mana, int overload_mult, int damage, int duration, const char* internal_name); void spellElementDeconstructor(void* data); int getCostOfSpell(spell_t* spell, Entity* caster = nullptr); -int getCostOfSpellElement(spellElement_t* spellElement); +int getGoldCostOfSpell(spell_t* spell, int player); +int getSustainCostOfSpell(spell_t* spell, Entity* caster); bool spell_isChanneled(spell_t* spell); bool spellElement_isChanneled(spellElement_t* spellElement); @@ -602,15 +1098,16 @@ int getSpellbookFromSpellID(int spellID); bool spellInList(list_t* list, spell_t* spell); //-----Implementations of spell effects----- -void spell_magicMap(int player); //Magics the map. I mean maps the magic. I mean magically maps the level. +void spell_magicMap(int player, int radius, int x, int y); //Magics the map. I mean maps the magic. I mean magically maps the level. void spell_detectFoodEffectOnMap(int player); void spell_summonFamiliar(int player); // summons some familiars. -void spell_changeHealth(Entity* entity, int amount, bool overdrewFromHP = false); //This function changes an entity's health. +void spell_changeHealth(Entity* entity, int amount, bool overdrewFromHP = false, bool doMessage = true); //This function changes an entity's health. //-----Spell Casting Animation----- //The two hand animation functions. void actLeftHandMagic(Entity* my); void actRightHandMagic(Entity* my); +void actMagicRangefinder(Entity* my); typedef struct spellcastingAnimationManager { @@ -624,43 +1121,213 @@ typedef struct spellcastingAnimationManager int stage; //The current stage of the animation. int circle_count; //How many times it's circled around in the circle stage. int times_to_circle; //How many times to circle around in the circle stage. - + int throw_count = 0; + int active_count = 0; + int overcharge = 0; + int overcharge_init = 0; int consume_interval; //Every consume_interval ticks, eat a mana. int consume_timer; //How many ticks left till next mana consume. int mana_left; //How much mana is left to consume. + int mana_cost; //Tracking cost of spell bool consumeMana; //If false, goes through the motions, even casts the spell -- just doesn't consume any mana. float lefthand_movex; float lefthand_movey; float lefthand_angle; + float vibrate_x = 0.f; + float vibrate_y = 0.f; + + real_t target_x = 0.0; + real_t target_y = 0.0; + real_t caster_x = 0.0; + real_t caster_y = 0.0; + Uint32 targetUid = 0; + int wallDir = 0; + SpellRangefinderType rangefinder = RANGEFINDER_NONE; + void setRangeFinderLocation(); + void resetRangefinder(); + bool hideShieldFromBasicCast(); + void executeAttackSpell(bool swingweapon); + bool spellWaitingAttackInput(); + bool spellIgnoreAttack(); } spellcasting_animation_manager_t; extern spellcasting_animation_manager_t cast_animation[MAXPLAYERS]; -void fireOffSpellAnimation(spellcasting_animation_manager_t* animation_manager, Uint32 caster_uid, spell_t* spell, bool usingSpellbook); +void fireOffSpellAnimation(spellcasting_animation_manager_t* animation_manager, Uint32 caster_uid, spell_t* spell, bool usingSpellbook, bool usingTome); void spellcastingAnimationManager_deactivate(spellcasting_animation_manager_t* animation_manager); -void spellcastingAnimationManager_completeSpell(spellcasting_animation_manager_t* animation_manager); +void spellcastAnimationUpdateReceive(int player, int attackPose, int castTime); +void spellcastAnimationUpdate(int player, int attackPose, int castTime); class Item; spell_t* getSpellFromItem(const int player, Item* item, bool usePlayerInventory); int getSpellIDFromSpellbook(int spellbookType); +int getSpellIDFromFoci(int fociType); int canUseShapeshiftSpellInCurrentForm(const int player, Item& item); //Spell implementation stuff. bool spellEffectDominate(Entity& my, spellElement_t& element, Entity& caster, Entity* parent); -void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int resistance); +void spellEffectAcid(Entity& my, spellElement_t& element, Entity* parent, int damage, int resistance); void spellEffectStealWeapon(Entity& my, spellElement_t& element, Entity* parent, int resistance); -void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, int resistance); -spell_t* spellEffectVampiricAura(Entity* caster, spell_t* spell, int extramagic_to_use); +void spellEffectDrainSoul(Entity& my, spellElement_t& element, Entity* parent, int damage, int resistance); +spell_t* spellEffectVampiricAura(Entity* caster, spell_t* spell); int getCharmMonsterDifficulty(Entity& my, Stat& myStats); void spellEffectCharmMonster(Entity& my, spellElement_t& element, Entity* parent, int resistance, bool magicstaff); -Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell, int customDuration = 0); // returns nullptr if target was monster, otherwise returns pointer to new creature -void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int resistance); +Entity* spellEffectPolymorph(Entity* target, Entity* parent, bool fromMagicSpell, int customDuration = 0, Monster customMonster = NOTHING); // returns nullptr if target was monster, otherwise returns pointer to new creature +void spellEffectPoison(Entity& my, spellElement_t& element, Entity* parent, int damage, int resistance); void spellEffectSprayWeb(Entity& my, spellElement_t& element, Entity* parent, int resistance); bool spellEffectFear(Entity* my, spellElement_t& element, Entity* forceParent, Entity* target, int resistance); bool spellEffectTeleportPull(Entity* my, spellElement_t& element, Entity* parent, Entity* target, int resistance); void spellEffectShadowTag(Entity& my, spellElement_t& element, Entity* parent, int resistance); bool spellEffectDemonIllusion(Entity& my, spellElement_t& element, Entity* parent, Entity* target, int resistance); - -void freeSpells(); \ No newline at end of file +Entity* spellEffectAdorcise(Entity& caster, spellElement_t& element, real_t x, real_t y, Item* itemToAdorcise); +Entity* spellEffectFlameSprite(Entity& caster, spellElement_t& element, real_t x, real_t y); +Entity* spellEffectHologram(Entity& caster, spellElement_t& element, real_t x, real_t y); +Entity* spellEffectDemesneDoor(Entity& caster, Entity& doorFrame); +void magicSetResistance(Entity* entity, Entity* parent, int& resistance, real_t& damageMultiplier, DamageGib& dmgGib, int& trapResist, int spellID); +Sint32 convertResistancePointsToMagicValue(Sint32 value, int resistance); +int getSpellDamageFromID(int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus = 0.0, bool applyingDamageOnCast = true); +int getSpellDamageSecondaryFromID(int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus = 0.0, bool applyingDamageOnCast = true); +int getSpellEffectDurationFromID(int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus = 0.0); +int getSpellEffectDurationSecondaryFromID(int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus = 0.0); +real_t getSpellPropertyFromID(spell_t::SpellBasePropertiesFloat prop, int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus = 0.0); +int getSpellPropertyFromID(spell_t::SpellBasePropertiesInt prop, int spellID, Entity* parent, Stat* parentStats, Entity* magicSourceParticle, real_t addSpellBonus = 0.0); +int getSpellDamageFromStatic(int spellID, Stat* hitstats); +void updateEntityOldHPBeforeMagicHit(Entity& my, Entity& projectile); +bool absorbMagicEvent(Entity* entity, Entity* parent, Entity& damageSourceProjectile, int spellID, real_t* result, real_t& damageMultiplier, DamageGib& dmgGib); +void thrownItemUpdateSpellTrail(Entity& my, real_t _x, real_t _y); +int getSpellFromSummonedEntityForSpellEvent(Entity* summon); +const char* magicLightColorForSprite(Entity* my, int sprite, bool darker); +void doParticleEffectForTouchSpell(Entity& my, Entity* focalLimb, Monster monsterType); +bool magicOnSpellCastEvent(Entity* parent, Entity* projectile, Entity* hitentity, int spellID, Uint32 eventType, int eventValue, bool allowedLevelup = true); // return true on level up +void freeSpells(); +void createParticleFociLight(Entity* entity, int spellID, bool updateClients); +void createParticleFociDark(Entity* entity, int spellID, bool updateClients); +bool jewelItemRecruit(Entity* parent, Entity* entity, int itemStatus, const char** msg); +bool entityWantsJewel(int tier, Entity& entity, Stat& stats, bool checkTypeOnly); +bool spellIsNaturallyLearnedByRaceOrClass(Entity* caster, Stat& stat, int spellID, int player = -1); + +struct AOEIndicators_t +{ + static Uint32 uids; + enum SurfaceCacheTypes : int + { + CACHE_NONE, + CACHE_VORTEX, + CACHE_CASTING, + CACHE_BOOBY_TRAP, + CACHE_BOOBY_TRAP2, + CACHE_IGNITE, + CACHE_IGNITE2, + CACHE_SHATTER_OBJECTS, + CACHE_SHATTER_OBJECTS2, + CACHE_FLAME_CLOAK, + CACHE_EXPLOSION_AREA, + CACHE_MUSHROOM_1, + CACHE_MUSHROOM_2, + CACHE_MUSHROOM_3, + CACHE_MUSHROOM_4, + CACHE_MAGICIANS_ARMOR, + CACHE_THAUM_ARMOR, + CACHE_PSYCHIC_SPEAR, + CACHE_RADIUS_MAGIC_GENERIC + }; + struct Indicator_t + { + TempTexture* texture = nullptr; + SDL_Surface* surfaceOld = nullptr; + int radiusMax = 128; + int radiusMin = 32; + int radius = 32; + int size = 132; + int lifetime = 1; + int gradient = 8; + Uint32 uid = 0; + bool loop = true; + bool castingTarget = false; + Uint32 framesPerTick = 1; + Uint32 ticksPerUpdate = 1; + Uint32 delayTicks = 0; + Uint32 indicatorColor = 0xFFFFFFFF; + real_t arc = 0.0; + bool expired = false; + int loopType = 0; + Uint32 loopTicks = 0; + Uint32 loopTimer = 0; + real_t expireAlphaRate = 0.9; + SurfaceCacheTypes cacheType = CACHE_NONE; + + struct PrevData_t + { + Uint8 r = 0; + Uint8 g = 0; + Uint8 b = 0; + Uint8 a = 0; + real_t radMin = 0.0; + real_t radMax = 0.0; + int size = 0; + }; + PrevData_t prevData; + + void updateIndicator(); + Indicator_t(int _radiusMin, int _radiusMax, int _size, int _lifetime, Uint32 _uid) + { + radiusMax = _radiusMax; + radiusMin = _radiusMin; + radius = radiusMin; + size = _size; + lifetime = _lifetime; + uid = _uid; + } + ~Indicator_t() + { + if ( texture ) + { + delete texture; + texture = nullptr; + } + if ( surfaceOld ) + { + if ( cacheType == CACHE_NONE ) + { + SDL_FreeSurface(surfaceOld); + } + surfaceOld = nullptr; + } + } + }; + static std::map, SDL_Surface*>> surfaceCache; + static void cleanup(); + static std::map indicators; + + static TempTexture* getTexture(Uint32 uid) + { + auto find = indicators.find(uid); + if ( find != indicators.end() ) + { + return find->second.texture; + } + return nullptr; + } + static SDL_Surface* getSurface(Uint32 uid) + { + auto find = indicators.find(uid); + if ( find != indicators.end() ) + { + return find->second.surfaceOld; + } + return nullptr; + } + static Indicator_t* getIndicator(Uint32 uid) + { + auto find = indicators.find(uid); + if ( find != indicators.end() ) + { + return &find->second; + } + return nullptr; + } + static void update(); + static Uint32 createIndicator(int _radiusMin, int _radiusMax, int _size, int _lifetime); +}; \ No newline at end of file diff --git a/src/magic/setupSpells.cpp b/src/magic/setupSpells.cpp index e865bd52f..7af8de5cb 100644 --- a/src/magic/setupSpells.cpp +++ b/src/magic/setupSpells.cpp @@ -13,477 +13,497 @@ #include "../game.hpp" #include "../stat.hpp" #include "magic.hpp" +#include "../mod_tools.hpp" -std::vector allGameSpells; +std::map allGameSpells; +std::map> spellTomeAppearanceToID; +std::map spellTomeIDToAppearance; +spell_t* createSimpleSpell(int spellID, int difficulty, int mana, int base_mana, int overload_mult, int damage, int duration, const char* internal_name, int sustainedMP = 0); void setupSpells() ///TODO: Verify this function. { + for ( auto it : allGameSpells ) + { + spell_t* spell = it.second; + list_RemoveNode(spell->sustain_node); + list_FreeAll(&spell->elements); + if ( spell->needsDataFreed ) + { + free(spell); + } + } allGameSpells.clear(); + spellTomeAppearanceToID.clear(); + spellTomeIDToAppearance.clear(); + spellElementMap.clear(); node_t* node = NULL; spellElement_t* element = NULL; spellElementConstructor(&spellElement_unintelligible); - spellElement_unintelligible.mana = 0; - spellElement_unintelligible.base_mana = 0; - spellElement_unintelligible.overload_multiplier = 0; //NOTE: Might crash due to divide by zero? - spellElement_unintelligible.damage = 0; + //spellElement_unintelligible.mana = 0; + //spellElement_unintelligible.base_mana = 0; + //spellElement_unintelligible.overload_multiplier = 0; //NOTE: Might crash due to divide by zero? + spellElement_unintelligible.setDamage(0); spellElement_unintelligible.duration = 0; spellElement_unintelligible.can_be_learned = false; strcpy(spellElement_unintelligible.element_internal_name, "spell_element_unintelligible"); spellElementConstructor(&spellElement_missile); - spellElement_missile.mana = 1; - spellElement_missile.base_mana = 1; - spellElement_missile.overload_multiplier = 1; - spellElement_missile.damage = 0; + //spellElement_missile.mana = 1; + //spellElement_missile.base_mana = 1; + //spellElement_missile.overload_multiplier = 1; + spellElement_missile.setDamage(0); spellElement_missile.duration = 75; //1.25 seconds. //spellElement_missile.name = "Missile"; strcpy(spellElement_missile.element_internal_name, "spell_element_missile"); + copySpellElement(&spellElement_missile, &spellElementMap[SPELL_ELEMENT_PROPULSION_MISSILE]); + copySpellElement(&spellElement_missile, &spellElementMap[SPELL_ELEMENT_PROPULSION_MISSILE_NOCOST]); + //spellElementMap[SPELL_ELEMENT_PROPULSION_MISSILE_NOCOST].mana = 0; + //spellElementMap[SPELL_ELEMENT_PROPULSION_MISSILE_NOCOST].base_mana = 0; spellElementConstructor(&spellElement_force); - spellElement_force.mana = 4; - spellElement_force.base_mana = 4; - spellElement_force.overload_multiplier = 1; - spellElement_force.damage = 15; + //spellElement_force.mana = 4; + //spellElement_force.base_mana = 4; + //spellElement_force.overload_multiplier = 1; + spellElement_force.setDamage(15); spellElement_force.duration = 0; strcpy(spellElement_force.element_internal_name, "spell_element_forcebolt"); spellElementConstructor(&spellElement_fire); - spellElement_fire.mana = 6; - spellElement_fire.base_mana = 6; - spellElement_fire.overload_multiplier = 1; - spellElement_fire.damage = 25; + //spellElement_fire.mana = 6; + //spellElement_fire.base_mana = 6; + //spellElement_fire.overload_multiplier = 1; + spellElement_fire.setDamage(25); spellElement_fire.duration = 0; strcpy(spellElement_fire.element_internal_name, "spell_element_fireball"); spellElementConstructor(&spellElement_lightning); - spellElement_lightning.mana = 5; - spellElement_lightning.base_mana = 5; - spellElement_lightning.overload_multiplier = 1; - spellElement_lightning.damage = 25; + //spellElement_lightning.mana = 5; + //spellElement_lightning.base_mana = 5; + //spellElement_lightning.overload_multiplier = 1; + spellElement_lightning.setDamage(25); spellElement_lightning.duration = 75; strcpy(spellElement_lightning.element_internal_name, "spell_element_lightning"); spellElementConstructor(&spellElement_light); - spellElement_light.mana = 1; - spellElement_light.base_mana = 1; - spellElement_light.overload_multiplier = 1; - spellElement_light.damage = 0; + //spellElement_light.mana = 1; + //spellElement_light.base_mana = 1; + //spellElement_light.overload_multiplier = 1; + spellElement_light.setDamage(0); spellElement_light.duration = 750; //500 a better value? //NOTE: 750 is original value. strcpy(spellElement_light.element_internal_name, "spell_element_light"); spellElementConstructor(&spellElement_dig); - spellElement_dig.mana = 20; - spellElement_dig.base_mana = 20; - spellElement_dig.overload_multiplier = 1; - spellElement_dig.damage = 0; + //spellElement_dig.mana = 20; + //spellElement_dig.base_mana = 20; + //spellElement_dig.overload_multiplier = 1; + spellElement_dig.setDamage(0); spellElement_dig.duration = 0; strcpy(spellElement_dig.element_internal_name, "spell_element_dig"); spellElementConstructor(&spellElement_invisible); - spellElement_invisible.mana = 2; - spellElement_invisible.base_mana = 2; - spellElement_invisible.overload_multiplier = 1; - spellElement_invisible.damage = 0; + //spellElement_invisible.mana = 2; + //spellElement_invisible.base_mana = 2; + //spellElement_invisible.overload_multiplier = 1; + spellElement_invisible.setDamage(0); spellElement_invisible.duration = 100; strcpy(spellElement_invisible.element_internal_name, "spell_element_invisibility"); spellElementConstructor(&spellElement_identify); - spellElement_identify.mana = 10; - spellElement_identify.base_mana = 10; - spellElement_identify.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? - spellElement_identify.damage = 0; + //spellElement_identify.mana = 10; + //spellElement_identify.base_mana = 10; + //spellElement_identify.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? + spellElement_identify.setDamage(0); spellElement_identify.duration = 0; strcpy(spellElement_identify.element_internal_name, "spell_element_identify"); spellElementConstructor(&spellElement_magicmapping); - spellElement_magicmapping.mana = 40; - spellElement_magicmapping.base_mana = 40; - spellElement_magicmapping.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? - spellElement_magicmapping.damage = 0; + //spellElement_magicmapping.mana = 40; + //spellElement_magicmapping.base_mana = 40; + //spellElement_magicmapping.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? + spellElement_magicmapping.setDamage(0); spellElement_magicmapping.duration = 0; strcpy(spellElement_magicmapping.element_internal_name, "spell_element_magicmapping"); spellElementConstructor(&spellElement_heal); - spellElement_heal.mana = 1; - spellElement_heal.base_mana = 1; - spellElement_heal.overload_multiplier = 1; - spellElement_heal.damage = 1; + //spellElement_heal.mana = 10; + //spellElement_heal.base_mana = 10; + //spellElement_heal.overload_multiplier = 1; + spellElement_heal.setDamage(10); spellElement_heal.duration = 0; strcpy(spellElement_heal.element_internal_name, "spell_element_healing"); spellElementConstructor(&spellElement_confuse); - spellElement_confuse.mana = 4; - spellElement_confuse.base_mana = 4; - spellElement_confuse.overload_multiplier = 1; - spellElement_confuse.damage = 0; + //spellElement_confuse.mana = 4; + //spellElement_confuse.base_mana = 4; + //spellElement_confuse.overload_multiplier = 1; + spellElement_confuse.setDamage(0); spellElement_confuse.duration = TICKS_PER_SECOND * SPELLELEMENT_CONFUSE_BASE_DURATION; //TODO: Decide on something. strcpy(spellElement_confuse.element_internal_name, "spell_element_confuse"); spellElementConstructor(&spellElement_cure_ailment); - spellElement_cure_ailment.mana = 10; - spellElement_cure_ailment.base_mana = 10; - spellElement_cure_ailment.overload_multiplier = 0; - spellElement_cure_ailment.damage = 0; + //spellElement_cure_ailment.mana = 10; + //spellElement_cure_ailment.base_mana = 10; + //spellElement_cure_ailment.overload_multiplier = 0; + spellElement_cure_ailment.setDamage(0); spellElement_cure_ailment.duration = 0; strcpy(spellElement_cure_ailment.element_internal_name, "spell_element_cureailment"); spellElementConstructor(&spellElement_locking); - spellElement_locking.mana = 10; - spellElement_locking.base_mana = 10; - spellElement_locking.overload_multiplier = 0; - spellElement_locking.damage = 0; + //spellElement_locking.mana = 10; + //spellElement_locking.base_mana = 10; + //spellElement_locking.overload_multiplier = 0; + spellElement_locking.setDamage(0); spellElement_locking.duration = 0; strcpy(spellElement_locking.element_internal_name, "spell_element_locking"); spellElementConstructor(&spellElement_opening); - spellElement_opening.mana = 5; - spellElement_opening.base_mana = 5; - spellElement_opening.overload_multiplier = 0; - spellElement_opening.damage = 0; + //spellElement_opening.mana = 5; + //spellElement_opening.base_mana = 5; + //spellElement_opening.overload_multiplier = 0; + spellElement_opening.setDamage(0); spellElement_opening.duration = 0; strcpy(spellElement_opening.element_internal_name, "spell_element_opening"); spellElementConstructor(&spellElement_sleep); - spellElement_sleep.mana = 3; - spellElement_sleep.base_mana = 3; - spellElement_sleep.overload_multiplier = 0; - spellElement_sleep.damage = 0; + //spellElement_sleep.mana = 3; + //spellElement_sleep.base_mana = 3; + //spellElement_sleep.overload_multiplier = 0; + spellElement_sleep.setDamage(0); spellElement_sleep.duration = 0; strcpy(spellElement_sleep.element_internal_name, "spell_element_sleep"); spellElementConstructor(&spellElement_cold); - spellElement_cold.mana = 5; - spellElement_cold.base_mana = 5; - spellElement_cold.overload_multiplier = 1; - spellElement_cold.damage = 20; + //spellElement_cold.mana = 5; + //spellElement_cold.base_mana = 5; + //spellElement_cold.overload_multiplier = 1; + spellElement_cold.setDamage(20); spellElement_cold.duration = 180; strcpy(spellElement_cold.element_internal_name, "spell_element_cold"); spellElementConstructor(&spellElement_slow); - spellElement_slow.mana = 3; - spellElement_slow.base_mana = 3; - spellElement_slow.overload_multiplier = 1; - spellElement_slow.damage = 0; + //spellElement_slow.mana = 3; + //spellElement_slow.base_mana = 3; + //spellElement_slow.overload_multiplier = 1; + spellElement_slow.setDamage(0); spellElement_slow.duration = 180; strcpy(spellElement_slow.element_internal_name, "spell_element_slow"); spellElementConstructor(&spellElement_levitation); - spellElement_levitation.mana = 1; - spellElement_levitation.base_mana = 1; - spellElement_levitation.overload_multiplier = 1; - spellElement_levitation.damage = 0; + //spellElement_levitation.mana = 1; + //spellElement_levitation.base_mana = 1; + //spellElement_levitation.overload_multiplier = 1; + spellElement_levitation.setDamage(0); spellElement_levitation.duration = 30; strcpy(spellElement_levitation.element_internal_name, "spell_element_levitation"); spellElementConstructor(&spellElement_teleportation); - spellElement_teleportation.mana = 20; - spellElement_teleportation.base_mana = 20; - spellElement_teleportation.overload_multiplier = 0; - spellElement_teleportation.damage = 0; + //spellElement_teleportation.mana = 20; + //spellElement_teleportation.base_mana = 20; + //spellElement_teleportation.overload_multiplier = 0; + spellElement_teleportation.setDamage(0); spellElement_teleportation.duration = 0; strcpy(spellElement_teleportation.element_internal_name, "spell_element_teleportation"); spellElementConstructor(&spellElement_selfPolymorph); - spellElement_selfPolymorph.mana = 40; - spellElement_selfPolymorph.base_mana = 40; - spellElement_selfPolymorph.overload_multiplier = 0; - spellElement_selfPolymorph.damage = 0; + //spellElement_selfPolymorph.mana = 40; + //spellElement_selfPolymorph.base_mana = 40; + //spellElement_selfPolymorph.overload_multiplier = 0; + spellElement_selfPolymorph.setDamage(0); spellElement_selfPolymorph.duration = 0; strcpy(spellElement_selfPolymorph.element_internal_name, "spell_element_self_polymorph"); spellElementConstructor(&spellElement_magicmissile); - spellElement_magicmissile.mana = 6; - spellElement_magicmissile.base_mana = 6; - spellElement_magicmissile.overload_multiplier = 1; - spellElement_magicmissile.damage = 30; + //spellElement_magicmissile.mana = 6; + //spellElement_magicmissile.base_mana = 6; + //spellElement_magicmissile.overload_multiplier = 1; + spellElement_magicmissile.setDamage(30); spellElement_magicmissile.duration = 0; strcpy(spellElement_magicmissile.element_internal_name, "spell_element_magicmissile"); spellElementConstructor(&spellElement_removecurse); - spellElement_removecurse.mana = 20; - spellElement_removecurse.base_mana = 20; - spellElement_removecurse.overload_multiplier = 0; - spellElement_removecurse.damage = 0; + //spellElement_removecurse.mana = 20; + //spellElement_removecurse.base_mana = 20; + //spellElement_removecurse.overload_multiplier = 0; + spellElement_removecurse.setDamage(0); spellElement_removecurse.duration = 0; strcpy(spellElement_removecurse.element_internal_name, "spell_element_removecurse"); spellElementConstructor(&spellElement_summon); - spellElement_summon.mana = 17; - spellElement_summon.base_mana = 17; - spellElement_summon.overload_multiplier = 1; - spellElement_summon.damage = 0; + //spellElement_summon.mana = 17; + //spellElement_summon.base_mana = 17; + //spellElement_summon.overload_multiplier = 1; + spellElement_summon.setDamage(0); spellElement_summon.duration = 0; strcpy(spellElement_summon.element_internal_name, "spell_element_summon"); spellElementConstructor(&spellElement_stoneblood); - spellElement_stoneblood.mana = 20; - spellElement_stoneblood.base_mana = 20; - spellElement_stoneblood.overload_multiplier = 1; - spellElement_stoneblood.damage = 0; + //spellElement_stoneblood.mana = 20; + //spellElement_stoneblood.base_mana = 20; + //spellElement_stoneblood.overload_multiplier = 1; + spellElement_stoneblood.setDamage(0); spellElement_stoneblood.duration = TICKS_PER_SECOND * SPELLELEMENT_STONEBLOOD_BASE_DURATION; strcpy(spellElement_stoneblood.element_internal_name, "spell_element_stoneblood"); spellElementConstructor(&spellElement_bleed); - spellElement_bleed.mana = 10; - spellElement_bleed.base_mana = 10; - spellElement_bleed.overload_multiplier = 1; - spellElement_bleed.damage = 30; + //spellElement_bleed.mana = 10; + //spellElement_bleed.base_mana = 10; + //spellElement_bleed.overload_multiplier = 1; + spellElement_bleed.setDamage(30); spellElement_bleed.duration = TICKS_PER_SECOND * SPELLELEMENT_BLEED_BASE_DURATION; //TODO: Decide on something.; strcpy(spellElement_bleed.element_internal_name, "spell_element_bleed"); spellElementConstructor(&spellElement_missile_trio); - spellElement_missile_trio.mana = 1; - spellElement_missile_trio.base_mana = 1; - spellElement_missile_trio.overload_multiplier = 1; - spellElement_missile_trio.damage = 0; + //spellElement_missile_trio.mana = 1; + //spellElement_missile_trio.base_mana = 1; + //spellElement_missile_trio.overload_multiplier = 1; + spellElement_missile_trio.setDamage(0); spellElement_missile_trio.duration = 25; //1 second. strcpy(spellElement_missile_trio.element_internal_name, "spell_element_trio"); spellElementConstructor(&spellElement_slime_spray); - spellElement_slime_spray.mana = 1; - spellElement_slime_spray.base_mana = 1; - spellElement_slime_spray.overload_multiplier = 1; - spellElement_slime_spray.damage = 0; + //spellElement_slime_spray.mana = 1; + //spellElement_slime_spray.base_mana = 1; + //spellElement_slime_spray.overload_multiplier = 1; + spellElement_slime_spray.setDamage(0); spellElement_slime_spray.duration = 100; strcpy(spellElement_slime_spray.element_internal_name, "spell_element_slime_spray"); spellElementConstructor(&spellElement_dominate); - spellElement_dominate.mana = 20; - spellElement_dominate.base_mana = 20; - spellElement_dominate.overload_multiplier = 1; - spellElement_dominate.damage = 0; + //spellElement_dominate.mana = 20; + //spellElement_dominate.base_mana = 20; + //spellElement_dominate.overload_multiplier = 1; + spellElement_dominate.setDamage(0); spellElement_dominate.duration = 0; strcpy(spellElement_dominate.element_internal_name, "spell_element_dominate"); spellElementConstructor(&spellElement_reflectMagic); - spellElement_reflectMagic.mana = 10; - spellElement_reflectMagic.base_mana = 10; - spellElement_reflectMagic.overload_multiplier = 1; - spellElement_reflectMagic.damage = 0; + //spellElement_reflectMagic.mana = 10; + //spellElement_reflectMagic.base_mana = 10; + //spellElement_reflectMagic.overload_multiplier = 1; + spellElement_reflectMagic.setDamage(0); spellElement_reflectMagic.duration = 3000; strcpy(spellElement_reflectMagic.element_internal_name, "spell_element_reflect_magic"); spellElementConstructor(&spellElement_acidSpray); - spellElement_acidSpray.mana = 10; - spellElement_acidSpray.base_mana = 10; - spellElement_acidSpray.overload_multiplier = 1; - spellElement_acidSpray.damage = 10; + //spellElement_acidSpray.mana = 10; + //spellElement_acidSpray.base_mana = 10; + //spellElement_acidSpray.overload_multiplier = 1; + spellElement_acidSpray.setDamage(10); spellElement_acidSpray.duration = TICKS_PER_SECOND * SPELLELEMENT_ACIDSPRAY_BASE_DURATION; //TODO: Decide on something.; strcpy(spellElement_acidSpray.element_internal_name, "spell_element_acid_spray"); spellElementConstructor(&spellElement_stealWeapon); - spellElement_stealWeapon.mana = 50; - spellElement_stealWeapon.base_mana = 50; - spellElement_stealWeapon.overload_multiplier = 1; - spellElement_stealWeapon.damage = 0; + //spellElement_stealWeapon.mana = 50; + //spellElement_stealWeapon.base_mana = 50; + //spellElement_stealWeapon.overload_multiplier = 1; + spellElement_stealWeapon.setDamage(0); spellElement_stealWeapon.duration = 0; strcpy(spellElement_stealWeapon.element_internal_name, "spell_element_steal_weapon"); spellElementConstructor(&spellElement_drainSoul); - spellElement_drainSoul.mana = 17; - spellElement_drainSoul.base_mana = 17; - spellElement_drainSoul.overload_multiplier = 1; - spellElement_drainSoul.damage = 18; + //spellElement_drainSoul.mana = 17; + //spellElement_drainSoul.base_mana = 17; + //spellElement_drainSoul.overload_multiplier = 1; + spellElement_drainSoul.setDamage(18); spellElement_drainSoul.duration = 0; strcpy(spellElement_drainSoul.element_internal_name, "spell_element_drain_soul"); spellElementConstructor(&spellElement_vampiricAura); - spellElement_vampiricAura.mana = 5; - spellElement_vampiricAura.base_mana = 5; - spellElement_vampiricAura.overload_multiplier = 1; - spellElement_vampiricAura.damage = 0; + //spellElement_vampiricAura.mana = 5; + //spellElement_vampiricAura.base_mana = 5; + //spellElement_vampiricAura.overload_multiplier = 1; + spellElement_vampiricAura.setDamage(0); spellElement_vampiricAura.duration = 85; //TODO: Decide on something. strcpy(spellElement_vampiricAura.element_internal_name, "spell_element_vampiric_aura"); spellElementConstructor(&spellElement_amplifyMagic); - spellElement_amplifyMagic.mana = 7; - spellElement_amplifyMagic.base_mana = 7; - spellElement_amplifyMagic.overload_multiplier = 1; - spellElement_amplifyMagic.damage = 0; + //spellElement_amplifyMagic.mana = 7; + //spellElement_amplifyMagic.base_mana = 7; + //spellElement_amplifyMagic.overload_multiplier = 1; + spellElement_amplifyMagic.setDamage(0); spellElement_amplifyMagic.duration = 85; //TODO: Decide on something. strcpy(spellElement_amplifyMagic.element_internal_name, "spell_element_amplify_magic"); spellElementConstructor(&spellElement_charmMonster); - spellElement_charmMonster.mana = 49; - spellElement_charmMonster.base_mana = 49; - spellElement_charmMonster.overload_multiplier = 1; - spellElement_charmMonster.damage = 0; + //spellElement_charmMonster.mana = 49; + //spellElement_charmMonster.base_mana = 49; + //spellElement_charmMonster.overload_multiplier = 1; + spellElement_charmMonster.setDamage(0); spellElement_charmMonster.duration = 300; strcpy(spellElement_charmMonster.element_internal_name, "spell_element_charm"); spellElementConstructor(&spellElement_shapeshift); - spellElement_shapeshift.mana = 1; - spellElement_shapeshift.base_mana = 1; - spellElement_shapeshift.overload_multiplier = 1; - spellElement_shapeshift.damage = 0; + //spellElement_shapeshift.mana = 1; + //spellElement_shapeshift.base_mana = 1; + //spellElement_shapeshift.overload_multiplier = 1; + spellElement_shapeshift.setDamage(0); spellElement_shapeshift.duration = 0; strcpy(spellElement_shapeshift.element_internal_name, "spell_element_shapeshift"); spellElementConstructor(&spellElement_sprayWeb); - spellElement_sprayWeb.mana = 7; - spellElement_sprayWeb.base_mana = 7; - spellElement_sprayWeb.overload_multiplier = 1; - spellElement_sprayWeb.damage = 0; + //spellElement_sprayWeb.mana = 7; + //spellElement_sprayWeb.base_mana = 7; + //spellElement_sprayWeb.overload_multiplier = 1; + spellElement_sprayWeb.setDamage(0); spellElement_sprayWeb.duration = 0; strcpy(spellElement_sprayWeb.element_internal_name, "spell_element_spray_web"); spellElementConstructor(&spellElement_poison); - spellElement_poison.mana = 4; - spellElement_poison.base_mana = 4; - spellElement_poison.overload_multiplier = 1; - spellElement_poison.damage = 10; + //spellElement_poison.mana = 4; + //spellElement_poison.base_mana = 4; + //spellElement_poison.overload_multiplier = 1; + spellElement_poison.setDamage(10); spellElement_poison.duration = 0; strcpy(spellElement_poison.element_internal_name, "spell_element_poison"); spellElementConstructor(&spellElement_speed); - spellElement_speed.mana = 11; - spellElement_speed.base_mana = 11; - spellElement_speed.overload_multiplier = 1; - spellElement_speed.damage = 0; + //spellElement_speed.mana = 11; + //spellElement_speed.base_mana = 11; + //spellElement_speed.overload_multiplier = 1; + spellElement_speed.setDamage(0); spellElement_speed.duration = 30 * TICKS_PER_SECOND; strcpy(spellElement_speed.element_internal_name, "spell_element_speed"); spellElementConstructor(&spellElement_fear); - spellElement_fear.mana = 28; - spellElement_fear.base_mana = 28; - spellElement_fear.overload_multiplier = 1; - spellElement_fear.damage = 0; + //spellElement_fear.mana = 28; + //spellElement_fear.base_mana = 28; + //spellElement_fear.overload_multiplier = 1; + spellElement_fear.setDamage(0); spellElement_fear.duration = 0; strcpy(spellElement_fear.element_internal_name, "spell_element_fear"); spellElementConstructor(&spellElement_strike); - spellElement_strike.mana = 23; - spellElement_strike.base_mana = 23; - spellElement_strike.overload_multiplier = 1; - spellElement_strike.damage = 1; + //spellElement_strike.mana = 23; + //spellElement_strike.base_mana = 23; + //spellElement_strike.overload_multiplier = 1; + spellElement_strike.setDamage(1); spellElement_strike.duration = 0; strcpy(spellElement_strike.element_internal_name, "spell_element_strike"); spellElementConstructor(&spellElement_weakness); - spellElement_weakness.mana = 1; - spellElement_weakness.base_mana = 1; - spellElement_weakness.overload_multiplier = 1; - spellElement_weakness.damage = 0; - spellElement_weakness.duration = 0; + //spellElement_weakness.mana = 1; + //spellElement_weakness.base_mana = 1; + //spellElement_weakness.overload_multiplier = 1; + spellElement_weakness.setDamage(0); + spellElement_weakness.duration = 50; strcpy(spellElement_weakness.element_internal_name, "spell_element_weakness"); spellElementConstructor(&spellElement_detectFood); - spellElement_detectFood.mana = 14; - spellElement_detectFood.base_mana = 14; - spellElement_detectFood.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? - spellElement_detectFood.damage = 0; + //spellElement_detectFood.mana = 14; + //spellElement_detectFood.base_mana = 14; + //spellElement_detectFood.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? + spellElement_detectFood.setDamage(0); spellElement_detectFood.duration = 0; strcpy(spellElement_detectFood.element_internal_name, "spell_element_detect_food"); spellElementConstructor(&spellElement_trollsBlood); - spellElement_trollsBlood.mana = 25; - spellElement_trollsBlood.base_mana = 25; - spellElement_trollsBlood.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? - spellElement_trollsBlood.damage = 0; + //spellElement_trollsBlood.mana = 25; + //spellElement_trollsBlood.base_mana = 25; + //spellElement_trollsBlood.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? + spellElement_trollsBlood.setDamage(0); spellElement_trollsBlood.duration = 80 * TICKS_PER_SECOND; strcpy(spellElement_trollsBlood.element_internal_name, "spell_element_trolls_blood"); spellElementConstructor(&spellElement_flutter); - spellElement_flutter.mana = 10; - spellElement_flutter.base_mana = 10; - spellElement_flutter.overload_multiplier = 1; - spellElement_flutter.damage = 0; + //spellElement_flutter.mana = 10; + //spellElement_flutter.base_mana = 10; + //spellElement_flutter.overload_multiplier = 1; + spellElement_flutter.setDamage(0); spellElement_flutter.duration = 6 * TICKS_PER_SECOND; strcpy(spellElement_flutter.element_internal_name, "spell_element_flutter"); spellElementConstructor(&spellElement_dash); - spellElement_dash.mana = 5; - spellElement_dash.base_mana = 5; - spellElement_dash.overload_multiplier = 1; - spellElement_dash.damage = 0; + //spellElement_dash.mana = 5; + //spellElement_dash.base_mana = 5; + //spellElement_dash.overload_multiplier = 1; + spellElement_dash.setDamage(0); spellElement_dash.duration = 1 * TICKS_PER_SECOND; strcpy(spellElement_dash.element_internal_name, "spell_element_dash"); spellElementConstructor(&spellElement_salvageItem); - spellElement_salvageItem.mana = 6; - spellElement_salvageItem.base_mana = 6; - spellElement_salvageItem.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? - spellElement_salvageItem.damage = 0; + //spellElement_salvageItem.mana = 6; + //spellElement_salvageItem.base_mana = 6; + //spellElement_salvageItem.overload_multiplier = 0; //NOTE: Might segfault due to divide by zero? + spellElement_salvageItem.setDamage(0); spellElement_salvageItem.duration = 0; strcpy(spellElement_salvageItem.element_internal_name, "spell_element_salvage"); spellElementConstructor(&spellElement_shadowTag); - spellElement_shadowTag.mana = 4; - spellElement_shadowTag.base_mana = 4; - spellElement_shadowTag.overload_multiplier = 1; - spellElement_shadowTag.damage = 0; + //spellElement_shadowTag.mana = 4; + //spellElement_shadowTag.base_mana = 4; + //spellElement_shadowTag.overload_multiplier = 1; + spellElement_shadowTag.setDamage(0); spellElement_shadowTag.duration = 0; strcpy(spellElement_shadowTag.element_internal_name, "spell_element_shadow_tag"); spellElementConstructor(&spellElement_telePull); - spellElement_telePull.mana = 19; - spellElement_telePull.base_mana = 19; - spellElement_telePull.overload_multiplier = 1; - spellElement_telePull.damage = 0; + //spellElement_telePull.mana = 19; + //spellElement_telePull.base_mana = 19; + //spellElement_telePull.overload_multiplier = 1; + spellElement_telePull.setDamage(0); spellElement_telePull.duration = 0; strcpy(spellElement_telePull.element_internal_name, "spell_element_telepull"); spellElementConstructor(&spellElement_demonIllusion); - spellElement_demonIllusion.mana = 24; - spellElement_demonIllusion.base_mana = 24; - spellElement_demonIllusion.overload_multiplier = 1; - spellElement_demonIllusion.damage = 0; + //spellElement_demonIllusion.mana = 24; + //spellElement_demonIllusion.base_mana = 24; + //spellElement_demonIllusion.overload_multiplier = 1; + spellElement_demonIllusion.setDamage(0); spellElement_demonIllusion.duration = 0; strcpy(spellElement_demonIllusion.element_internal_name, "spell_element_demon_illu"); spellElementConstructor(&spellElement_ghostBolt); - spellElement_ghostBolt.mana = 5; - spellElement_ghostBolt.base_mana = 5; - spellElement_ghostBolt.overload_multiplier = 1; - spellElement_ghostBolt.damage = 0; + //spellElement_ghostBolt.mana = 5; + //spellElement_ghostBolt.base_mana = 5; + //spellElement_ghostBolt.overload_multiplier = 1; + spellElement_ghostBolt.setDamage(0); spellElement_ghostBolt.duration = 75; strcpy(spellElement_ghostBolt.element_internal_name, "spell_element_ghost_bolt"); spellElementConstructor(&spellElement_slimeAcid); - spellElement_slimeAcid.mana = 5; - spellElement_slimeAcid.base_mana = 5; - spellElement_slimeAcid.overload_multiplier = 1; - spellElement_slimeAcid.damage = 5; + //spellElement_slimeAcid.mana = 5; + //spellElement_slimeAcid.base_mana = 5; + //spellElement_slimeAcid.overload_multiplier = 1; + spellElement_slimeAcid.setDamage(5); spellElement_slimeAcid.duration = 100; strcpy(spellElement_slimeAcid.element_internal_name, "spell_element_slime_acid"); spellElementConstructor(&spellElement_slimeWater); - spellElement_slimeWater.mana = 5; - spellElement_slimeWater.base_mana = 5; - spellElement_slimeWater.overload_multiplier = 1; - spellElement_slimeWater.damage = 5; + //spellElement_slimeWater.mana = 5; + //spellElement_slimeWater.base_mana = 5; + //spellElement_slimeWater.overload_multiplier = 1; + spellElement_slimeWater.setDamage(5); spellElement_slimeWater.duration = 100; strcpy(spellElement_slimeWater.element_internal_name, "spell_element_slime_water"); spellElementConstructor(&spellElement_slimeFire); - spellElement_slimeFire.mana = 5; - spellElement_slimeFire.base_mana = 5; - spellElement_slimeFire.overload_multiplier = 1; - spellElement_slimeFire.damage = 5; + //spellElement_slimeFire.mana = 5; + //spellElement_slimeFire.base_mana = 5; + //spellElement_slimeFire.overload_multiplier = 1; + spellElement_slimeFire.setDamage(5); spellElement_slimeFire.duration = 100; strcpy(spellElement_slimeFire.element_internal_name, "spell_element_slime_fire"); spellElementConstructor(&spellElement_slimeTar); - spellElement_slimeTar.mana = 5; - spellElement_slimeTar.base_mana = 5; - spellElement_slimeTar.overload_multiplier = 1; - spellElement_slimeTar.damage = 5; + //spellElement_slimeTar.mana = 5; + //spellElement_slimeTar.base_mana = 5; + //spellElement_slimeTar.overload_multiplier = 1; + spellElement_slimeTar.setDamage(5); spellElement_slimeTar.duration = 100; strcpy(spellElement_slimeTar.element_internal_name, "spell_element_slime_tar"); spellElementConstructor(&spellElement_slimeMetal); - spellElement_slimeMetal.mana = 5; - spellElement_slimeMetal.base_mana = 5; - spellElement_slimeMetal.overload_multiplier = 1; - spellElement_slimeMetal.damage = 5; + //spellElement_slimeMetal.mana = 5; + //spellElement_slimeMetal.base_mana = 5; + //spellElement_slimeMetal.overload_multiplier = 1; + spellElement_slimeMetal.setDamage(5); spellElement_slimeMetal.duration = 100; strcpy(spellElement_slimeMetal.element_internal_name, "spell_element_slime_metal"); - spellConstructor(&spell_forcebolt); + spellConstructor(&spell_forcebolt, SPELL_FORCEBOLT); strcpy(spell_forcebolt.spell_internal_name, "spell_forcebolt"); - spell_forcebolt.ID = SPELL_FORCEBOLT; spell_forcebolt.difficulty = 0; node = list_AddNodeLast(&spell_forcebolt.elements); node->element = copySpellElement(&spellElement_missile); @@ -501,9 +521,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_magicmissile); + spellConstructor(&spell_magicmissile, SPELL_MAGICMISSILE); strcpy(spell_magicmissile.spell_internal_name, "spell_magicmissile"); - spell_magicmissile.ID = SPELL_MAGICMISSILE; spell_magicmissile.difficulty = 60; node = list_AddNodeLast(&spell_magicmissile.elements); node->element = copySpellElement(&spellElement_missile); @@ -521,9 +540,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_cold); + spellConstructor(&spell_cold, SPELL_COLD); strcpy(spell_cold.spell_internal_name, "spell_cold"); - spell_cold.ID = SPELL_COLD; spell_cold.difficulty = 40; node = list_AddNodeLast(&spell_cold.elements); node->element = copySpellElement(&spellElement_missile); @@ -541,9 +559,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_fireball); + spellConstructor(&spell_fireball, SPELL_FIREBALL); strcpy(spell_fireball.spell_internal_name, "spell_fireball"); - spell_fireball.ID = SPELL_FIREBALL; spell_fireball.difficulty = 20; spell_fireball.elements.first = NULL; spell_fireball.elements.last = NULL; @@ -563,9 +580,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_lightning); + spellConstructor(&spell_lightning, SPELL_LIGHTNING); strcpy(spell_lightning.spell_internal_name, "spell_lightning"); - spell_lightning.ID = SPELL_LIGHTNING; spell_lightning.difficulty = 60; spell_lightning.elements.first = NULL; spell_lightning.elements.last = NULL; @@ -585,9 +601,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_removecurse); + spellConstructor(&spell_removecurse, SPELL_REMOVECURSE); strcpy(spell_removecurse.spell_internal_name, "spell_removecurse"); - spell_removecurse.ID = SPELL_REMOVECURSE; spell_removecurse.difficulty = 60; spell_removecurse.elements.first = NULL; spell_removecurse.elements.last = NULL; @@ -598,9 +613,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; //Tell the element what list it resides in. - spellConstructor(&spell_light); + spellConstructor(&spell_light, SPELL_LIGHT); strcpy(spell_light.spell_internal_name, "spell_light"); - spell_light.ID = SPELL_LIGHT; spell_light.difficulty = 0; spell_light.elements.first = NULL; spell_light.elements.last = NULL; @@ -610,11 +624,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*) node->element; element->node = node; //Tell the element what list it resides in. - element->channeled = true; + element->channeledMana = 1; - spellConstructor(&spell_identify); + spellConstructor(&spell_identify, SPELL_IDENTIFY); strcpy(spell_identify.spell_internal_name, "spell_identify"); - spell_identify.ID = SPELL_IDENTIFY; spell_identify.difficulty = 60; spell_identify.elements.first = NULL; spell_identify.elements.last = NULL; @@ -625,9 +638,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; //Tell the element what list it resides in. - spellConstructor(&spell_magicmapping); + spellConstructor(&spell_magicmapping, SPELL_MAGICMAPPING); strcpy(spell_magicmapping.spell_internal_name, "spell_magicmapping"); - spell_magicmapping.ID = SPELL_MAGICMAPPING; spell_magicmapping.difficulty = 60; spell_magicmapping.elements.first = NULL; spell_magicmapping.elements.last = NULL; @@ -638,9 +650,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_sleep); + spellConstructor(&spell_sleep, SPELL_SLEEP); strcpy(spell_sleep.spell_internal_name, "spell_sleep"); - spell_sleep.ID = SPELL_SLEEP; spell_sleep.difficulty = 20; node = list_AddNodeLast(&spell_sleep.elements); node->element = copySpellElement(&spellElement_missile); @@ -658,9 +669,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_confuse); + spellConstructor(&spell_confuse, SPELL_CONFUSE); strcpy(spell_confuse.spell_internal_name, "spell_confuse"); - spell_confuse.ID = SPELL_CONFUSE; spell_confuse.difficulty = 20; node = list_AddNodeLast(&spell_confuse.elements); node->element = copySpellElement(&spellElement_missile); @@ -677,11 +687,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*) node->element; element->node = node; - element->mana = 15; //Set the spell's mana to 15 so that it lasts ~30 seconds. + //element->mana = 15; //Set the spell's mana to 15 so that it lasts ~30 seconds. - spellConstructor(&spell_slow); + spellConstructor(&spell_slow, SPELL_SLOW); strcpy(spell_slow.spell_internal_name, "spell_slow"); - spell_slow.ID = SPELL_SLOW; spell_slow.difficulty = 20; node = list_AddNodeLast(&spell_slow.elements); node->element = copySpellElement(&spellElement_missile); @@ -699,9 +708,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_opening); + spellConstructor(&spell_opening, SPELL_OPENING); strcpy(spell_opening.spell_internal_name, "spell_opening"); - spell_opening.ID = SPELL_OPENING; spell_opening.difficulty = 20; node = list_AddNodeLast(&spell_opening.elements); node->element = copySpellElement(&spellElement_missile); @@ -719,9 +727,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_locking); + spellConstructor(&spell_locking, SPELL_LOCKING); strcpy(spell_locking.spell_internal_name, "spell_locking"); - spell_locking.ID = SPELL_LOCKING; spell_locking.difficulty = 20; node = list_AddNodeLast(&spell_locking.elements); node->element = copySpellElement(&spellElement_missile); @@ -739,9 +746,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_levitation); + spellConstructor(&spell_levitation, SPELL_LEVITATION); strcpy(spell_levitation.spell_internal_name, "spell_levitation"); - spell_levitation.ID = SPELL_LEVITATION; spell_levitation.difficulty = 80; spell_levitation.elements.first = NULL; spell_levitation.elements.last = NULL; @@ -751,11 +757,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*) node->element; element->node = node; //Tell the element what list it resides in. - element->channeled = true; + element->channeledMana = 1; - spellConstructor(&spell_invisibility); + spellConstructor(&spell_invisibility, SPELL_INVISIBILITY); strcpy(spell_invisibility.spell_internal_name, "spell_invisibility"); - spell_invisibility.ID = SPELL_INVISIBILITY; spell_invisibility.difficulty = 80; spell_invisibility.elements.first = NULL; spell_invisibility.elements.last = NULL; @@ -765,11 +770,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*) node->element; element->node = node; //Tell the element what list it resides in. - element->channeled = true; + element->channeledMana = 1; - spellConstructor(&spell_teleportation); + spellConstructor(&spell_teleportation, SPELL_TELEPORTATION); strcpy(spell_teleportation.spell_internal_name, "spell_teleportation"); - spell_teleportation.ID = SPELL_TELEPORTATION; spell_teleportation.difficulty = 80; spell_teleportation.elements.first = NULL; spell_teleportation.elements.last = NULL; @@ -780,9 +784,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; //Tell the element what list it resides in. - spellConstructor(&spell_polymorph); + spellConstructor(&spell_polymorph, SPELL_SELF_POLYMORPH); strcpy(spell_polymorph.spell_internal_name, "spell_self_polymorph"); - spell_polymorph.ID = SPELL_SELF_POLYMORPH; spell_polymorph.difficulty = 60; spell_polymorph.elements.first = NULL; spell_polymorph.elements.last = NULL; @@ -793,9 +796,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; //Tell the element what list it resides in. - spellConstructor(&spell_healing); + spellConstructor(&spell_healing, SPELL_HEALING); strcpy(spell_healing.spell_internal_name, "spell_healing"); - spell_healing.ID = SPELL_HEALING; spell_healing.difficulty = 20; spell_healing.elements.first = NULL; spell_healing.elements.last = NULL; @@ -805,11 +807,9 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*) node->element; element->node = node; - element->mana = 10; - spellConstructor(&spell_extrahealing); + /*spellConstructor(&spell_extrahealing, SPELL_EXTRAHEALING); strcpy(spell_extrahealing.spell_internal_name, "spell_extrahealing"); - spell_extrahealing.ID = SPELL_EXTRAHEALING; spell_extrahealing.difficulty = 60; spell_extrahealing.elements.first = NULL; spell_extrahealing.elements.last = NULL; @@ -819,7 +819,17 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*) node->element; element->node = node; - element->mana = 40; + element->mana = 40;*/ + + createSimpleSpell( + SPELL_EXTRAHEALING, + 60, + 40, + 40, + 1, + 40, + 0, + "spell_extrahealing"); /*spellConstructor(&spell_restoreability); //spell_restoreability.mana = 1; @@ -827,9 +837,8 @@ void setupSpells() ///TODO: Verify this function. spell_restoreability.ID = SPELL_RESTOREABILITY; spell_restoreability.difficulty = 100; //Basically unlearnable (since unimplemented).*/ - spellConstructor(&spell_cureailment); + spellConstructor(&spell_cureailment, SPELL_CUREAILMENT); strcpy(spell_cureailment.spell_internal_name, "spell_cureailment"); - spell_cureailment.ID = SPELL_CUREAILMENT; spell_cureailment.difficulty = 20; spell_cureailment.elements.first = NULL; spell_cureailment.elements.last = NULL; @@ -840,9 +849,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_dig); + spellConstructor(&spell_dig, SPELL_DIG); strcpy(spell_dig.spell_internal_name, "spell_dig"); - spell_dig.ID = SPELL_DIG; spell_dig.difficulty = 40; spell_dig.elements.first = NULL; spell_dig.elements.last = NULL; @@ -862,9 +870,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_stoneblood); + spellConstructor(&spell_stoneblood, SPELL_STONEBLOOD); strcpy(spell_stoneblood.spell_internal_name, "spell_stoneblood"); - spell_stoneblood.ID = SPELL_STONEBLOOD; spell_stoneblood.difficulty = 80; node = list_AddNodeLast(&spell_stoneblood.elements); node->element = copySpellElement(&spellElement_missile_trio); @@ -882,9 +889,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_bleed); + spellConstructor(&spell_bleed, SPELL_BLEED); strcpy(spell_bleed.spell_internal_name, "spell_bleed"); - spell_bleed.ID = SPELL_BLEED; spell_bleed.difficulty = 80; node = list_AddNodeLast(&spell_bleed.elements); node->element = copySpellElement(&spellElement_missile); @@ -902,9 +908,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_summon); + spellConstructor(&spell_summon, SPELL_SUMMON); strcpy(spell_summon.spell_internal_name, "spell_summon"); - spell_summon.ID = SPELL_SUMMON; spell_summon.difficulty = 40; spell_summon.elements.first = NULL; spell_summon.elements.last = NULL; @@ -915,9 +920,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_dominate); + spellConstructor(&spell_dominate, SPELL_DOMINATE); strcpy(spell_dominate.spell_internal_name, "spell_dominate"); - spell_dominate.ID = SPELL_DOMINATE; spell_dominate.difficulty = 100; node = list_AddNodeLast(&spell_dominate.elements); node->element = copySpellElement(&spellElement_missile); @@ -935,9 +939,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*) node->element; element->node = node; - spellConstructor(&spell_reflectMagic); + spellConstructor(&spell_reflectMagic, SPELL_REFLECT_MAGIC); strcpy(spell_reflectMagic.spell_internal_name, "spell_reflect_magic"); - spell_reflectMagic.ID = SPELL_REFLECT_MAGIC; spell_reflectMagic.difficulty = 80; spell_reflectMagic.elements.first = nullptr; spell_reflectMagic.elements.last = nullptr; @@ -947,11 +950,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*) node->element; element->node = node; //Tell the element what list it resides in. - element->channeled = true; + element->channeledMana = 1; - spellConstructor(&spell_acidSpray); + spellConstructor(&spell_acidSpray, SPELL_ACID_SPRAY); strcpy(spell_acidSpray.spell_internal_name, "spell_acid_spray"); - spell_acidSpray.ID = SPELL_ACID_SPRAY; spell_acidSpray.difficulty = 80; node = list_AddNodeLast(&spell_acidSpray.elements); node->element = copySpellElement(&spellElement_missile_trio); @@ -969,9 +971,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_stealWeapon); + spellConstructor(&spell_stealWeapon, SPELL_STEAL_WEAPON); strcpy(spell_stealWeapon.spell_internal_name, "spell_steal_weapon"); - spell_stealWeapon.ID = SPELL_STEAL_WEAPON; spell_stealWeapon.difficulty = 100; node = list_AddNodeLast(&spell_stealWeapon.elements); node->element = copySpellElement(&spellElement_missile); @@ -989,9 +990,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_drainSoul); + spellConstructor(&spell_drainSoul, SPELL_DRAIN_SOUL); strcpy(spell_drainSoul.spell_internal_name, "spell_drain_soul"); - spell_drainSoul.ID = SPELL_DRAIN_SOUL; spell_drainSoul.difficulty = 80; node = list_AddNodeLast(&spell_drainSoul.elements); node->element = copySpellElement(&spellElement_missile); @@ -1009,9 +1009,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_vampiricAura); + spellConstructor(&spell_vampiricAura, SPELL_VAMPIRIC_AURA); strcpy(spell_vampiricAura.spell_internal_name, "spell_vampiric_aura"); - spell_vampiricAura.ID = SPELL_VAMPIRIC_AURA; spell_vampiricAura.difficulty = 80; spell_vampiricAura.elements.first = nullptr; spell_vampiricAura.elements.last = nullptr; @@ -1021,11 +1020,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*)node->element; element->node = node; //Tell the element what list it resides in. - element->channeled = true; + element->channeledMana = 1; - spellConstructor(&spell_amplifyMagic); + spellConstructor(&spell_amplifyMagic, SPELL_AMPLIFY_MAGIC); strcpy(spell_amplifyMagic.spell_internal_name, "spell_amplify_magic"); - spell_amplifyMagic.ID = SPELL_AMPLIFY_MAGIC; spell_amplifyMagic.difficulty = 80; spell_amplifyMagic.elements.first = nullptr; spell_amplifyMagic.elements.last = nullptr; @@ -1035,11 +1033,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*)node->element; element->node = node; //Tell the element what list it resides in. - element->channeled = true; + element->channeledMana = 1; - spellConstructor(&spell_charmMonster); + spellConstructor(&spell_charmMonster, SPELL_CHARM_MONSTER); strcpy(spell_charmMonster.spell_internal_name, "spell_charm"); - spell_charmMonster.ID = SPELL_CHARM_MONSTER; spell_charmMonster.difficulty = 80; node = list_AddNodeLast(&spell_charmMonster.elements); node->element = copySpellElement(&spellElement_missile); @@ -1057,9 +1054,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_revertForm); + spellConstructor(&spell_revertForm, SPELL_REVERT_FORM); strcpy(spell_revertForm.spell_internal_name, "spell_revert_form"); - spell_revertForm.ID = SPELL_REVERT_FORM; spell_revertForm.difficulty = 0; spell_revertForm.elements.first = NULL; spell_revertForm.elements.last = NULL; @@ -1069,11 +1065,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*)node->element; element->node = node; - element->mana = 5; + //element->mana = 5; - spellConstructor(&spell_ratForm); + spellConstructor(&spell_ratForm, SPELL_RAT_FORM); strcpy(spell_ratForm.spell_internal_name, "spell_rat_form"); - spell_ratForm.ID = SPELL_RAT_FORM; spell_ratForm.difficulty = 0; spell_ratForm.elements.first = NULL; spell_ratForm.elements.last = NULL; @@ -1083,11 +1078,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*)node->element; element->node = node; - element->mana = 8; + //element->mana = 8; - spellConstructor(&spell_spiderForm); + spellConstructor(&spell_spiderForm, SPELL_SPIDER_FORM); strcpy(spell_spiderForm.spell_internal_name, "spell_spider_form"); - spell_spiderForm.ID = SPELL_SPIDER_FORM; spell_spiderForm.difficulty = 40; spell_spiderForm.elements.first = NULL; spell_spiderForm.elements.last = NULL; @@ -1097,11 +1091,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*)node->element; element->node = node; - element->mana = 16; + //element->mana = 16; - spellConstructor(&spell_trollForm); + spellConstructor(&spell_trollForm, SPELL_TROLL_FORM); strcpy(spell_trollForm.spell_internal_name, "spell_troll_form"); - spell_trollForm.ID = SPELL_TROLL_FORM; spell_trollForm.difficulty = 60; spell_trollForm.elements.first = NULL; spell_trollForm.elements.last = NULL; @@ -1111,11 +1104,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*)node->element; element->node = node; - element->mana = 24; + //element->mana = 24; - spellConstructor(&spell_impForm); + spellConstructor(&spell_impForm, SPELL_IMP_FORM); strcpy(spell_impForm.spell_internal_name, "spell_imp_form"); - spell_impForm.ID = SPELL_IMP_FORM; spell_impForm.difficulty = 80; spell_impForm.elements.first = NULL; spell_impForm.elements.last = NULL; @@ -1125,11 +1117,10 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*)node->element; element->node = node; - element->mana = 32; + //element->mana = 32; - spellConstructor(&spell_sprayWeb); + spellConstructor(&spell_sprayWeb, SPELL_SPRAY_WEB); strcpy(spell_sprayWeb.spell_internal_name, "spell_spray_web"); - spell_sprayWeb.ID = SPELL_SPRAY_WEB; spell_sprayWeb.difficulty = 20; node = list_AddNodeLast(&spell_sprayWeb.elements); node->element = copySpellElement(&spellElement_missile_trio); @@ -1147,9 +1138,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_poison); + spellConstructor(&spell_poison, SPELL_POISON); strcpy(spell_poison.spell_internal_name, "spell_poison"); - spell_poison.ID = SPELL_POISON; spell_poison.difficulty = 40; node = list_AddNodeLast(&spell_poison.elements); node->element = copySpellElement(&spellElement_missile); @@ -1167,9 +1157,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_speed); + spellConstructor(&spell_speed, SPELL_SPEED); strcpy(spell_speed.spell_internal_name, "spell_speed"); - spell_speed.ID = SPELL_SPEED; spell_speed.difficulty = 40; spell_speed.elements.first = NULL; spell_speed.elements.last = NULL; @@ -1180,9 +1169,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_fear); + spellConstructor(&spell_fear, SPELL_FEAR); strcpy(spell_fear.spell_internal_name, "spell_fear"); - spell_fear.ID = SPELL_FEAR; spell_fear.difficulty = 80; spell_fear.elements.first = NULL; spell_fear.elements.last = NULL; @@ -1193,9 +1181,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_weakness); + spellConstructor(&spell_weakness, SPELL_WEAKNESS); strcpy(spell_weakness.spell_internal_name, "spell_weakness"); - spell_weakness.ID = SPELL_WEAKNESS; spell_weakness.difficulty = 100; node = list_AddNodeLast(&spell_weakness.elements); node->element = copySpellElement(&spellElement_missile); @@ -1213,9 +1200,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_strike); + spellConstructor(&spell_strike, SPELL_STRIKE); strcpy(spell_strike.spell_internal_name, "spell_strike"); - spell_strike.ID = SPELL_STRIKE; spell_strike.difficulty = 80; spell_strike.elements.first = NULL; spell_strike.elements.last = NULL; @@ -1226,9 +1212,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_detectFood); + spellConstructor(&spell_detectFood, SPELL_DETECT_FOOD); strcpy(spell_detectFood.spell_internal_name, "spell_detect_food"); - spell_detectFood.ID = SPELL_DETECT_FOOD; spell_detectFood.difficulty = 40; spell_detectFood.elements.first = NULL; spell_detectFood.elements.last = NULL; @@ -1239,9 +1224,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_trollsBlood); + spellConstructor(&spell_trollsBlood, SPELL_TROLLS_BLOOD); strcpy(spell_trollsBlood.spell_internal_name, "spell_trolls_blood"); - spell_trollsBlood.ID = SPELL_TROLLS_BLOOD; spell_trollsBlood.difficulty = 40; spell_trollsBlood.elements.first = NULL; spell_trollsBlood.elements.last = NULL; @@ -1252,9 +1236,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_flutter); + spellConstructor(&spell_flutter, SPELL_FLUTTER); strcpy(spell_flutter.spell_internal_name, "spell_flutter"); - spell_flutter.ID = SPELL_FLUTTER; spell_flutter.difficulty = 60; spell_flutter.elements.first = NULL; spell_flutter.elements.last = NULL; @@ -1265,9 +1248,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_dash); + spellConstructor(&spell_dash, SPELL_DASH); strcpy(spell_dash.spell_internal_name, "spell_dash"); - spell_dash.ID = SPELL_DASH; spell_dash.difficulty = 40; spell_dash.elements.first = NULL; spell_dash.elements.last = NULL; @@ -1278,9 +1260,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_shadowTag); + spellConstructor(&spell_shadowTag, SPELL_SHADOW_TAG); strcpy(spell_shadowTag.spell_internal_name, "spell_shadow_tag"); - spell_shadowTag.ID = SPELL_SHADOW_TAG; spell_shadowTag.difficulty = 20; node = list_AddNodeLast(&spell_shadowTag.elements); node->element = copySpellElement(&spellElement_missile); @@ -1298,9 +1279,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_telePull); + spellConstructor(&spell_telePull, SPELL_TELEPULL); strcpy(spell_telePull.spell_internal_name, "spell_telepull"); - spell_telePull.ID = SPELL_TELEPULL; spell_telePull.difficulty = 60; node = list_AddNodeLast(&spell_telePull.elements); node->element = copySpellElement(&spellElement_missile); @@ -1318,9 +1298,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_demonIllusion); + spellConstructor(&spell_demonIllusion, SPELL_DEMON_ILLUSION); strcpy(spell_demonIllusion.spell_internal_name, "spell_demon_illu"); - spell_demonIllusion.ID = SPELL_DEMON_ILLUSION; spell_demonIllusion.difficulty = 80; node = list_AddNodeLast(&spell_demonIllusion.elements); node->element = copySpellElement(&spellElement_missile); @@ -1338,9 +1317,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_salvageItem); + spellConstructor(&spell_salvageItem, SPELL_SALVAGE); strcpy(spell_salvageItem.spell_internal_name, "spell_salvage"); - spell_salvageItem.ID = SPELL_SALVAGE; spell_salvageItem.difficulty = 20; spell_salvageItem.elements.first = NULL; spell_salvageItem.elements.last = NULL; @@ -1351,9 +1329,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_ghost_bolt); + spellConstructor(&spell_ghost_bolt, SPELL_GHOST_BOLT); strcpy(spell_ghost_bolt.spell_internal_name, "spell_ghost_bolt"); - spell_ghost_bolt.ID = SPELL_GHOST_BOLT; spell_ghost_bolt.difficulty = 100; spell_ghost_bolt.elements.first = NULL; spell_ghost_bolt.elements.last = NULL; @@ -1373,9 +1350,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_slime_acid); + spellConstructor(&spell_slime_acid, SPELL_SLIME_ACID); strcpy(spell_slime_acid.spell_internal_name, "spell_slime_acid"); - spell_slime_acid.ID = SPELL_SLIME_ACID; spell_slime_acid.difficulty = 100; spell_slime_acid.elements.first = NULL; spell_slime_acid.elements.last = NULL; @@ -1395,9 +1371,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_slime_water); + spellConstructor(&spell_slime_water, SPELL_SLIME_WATER); strcpy(spell_slime_water.spell_internal_name, "spell_slime_water"); - spell_slime_water.ID = SPELL_SLIME_WATER; spell_slime_water.difficulty = 100; spell_slime_water.elements.first = NULL; spell_slime_water.elements.last = NULL; @@ -1417,9 +1392,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_slime_fire); + spellConstructor(&spell_slime_fire, SPELL_SLIME_FIRE); strcpy(spell_slime_fire.spell_internal_name, "spell_slime_fire"); - spell_slime_fire.ID = SPELL_SLIME_FIRE; spell_slime_fire.difficulty = 100; spell_slime_fire.elements.first = NULL; spell_slime_fire.elements.last = NULL; @@ -1439,9 +1413,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_slime_tar); + spellConstructor(&spell_slime_tar, SPELL_SLIME_TAR); strcpy(spell_slime_tar.spell_internal_name, "spell_slime_tar"); - spell_slime_tar.ID = SPELL_SLIME_TAR; spell_slime_tar.difficulty = 100; spell_slime_tar.elements.first = NULL; spell_slime_tar.elements.last = NULL; @@ -1461,9 +1434,8 @@ void setupSpells() ///TODO: Verify this function. element = (spellElement_t*)node->element; element->node = node; - spellConstructor(&spell_slime_metal); + spellConstructor(&spell_slime_metal, SPELL_SLIME_METAL); strcpy(spell_slime_metal.spell_internal_name, "spell_slime_metal"); - spell_slime_metal.ID = SPELL_SLIME_METAL; spell_slime_metal.difficulty = 100; spell_slime_metal.elements.first = NULL; spell_slime_metal.elements.last = NULL; @@ -1482,4 +1454,2118 @@ void setupSpells() ///TODO: Verify this function. node->deconstructor = &spellElementDeconstructor; element = (spellElement_t*)node->element; element->node = node; + + spell_t* spell = nullptr; + + spellElementConstructor(SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_spray"); + + spellElementConstructor(SPELL_FOCI_FIRE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_fire"); + spell = spellConstructor( + SPELL_FOCI_FIRE, // ID + 100, // difficulty + "spell_foci_fire", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_FIRE } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_SNOW, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_snow"); + spell = spellConstructor( + SPELL_FOCI_SNOW, // ID + 100, // difficulty + "spell_foci_snow", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_SNOW } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_NEEDLES, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_needles"); + spell = spellConstructor( + SPELL_FOCI_NEEDLES, // ID + 100, // difficulty + "spell_foci_needles", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_NEEDLES } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_ARCS, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_arcs"); + spell = spellConstructor( + SPELL_FOCI_ARCS, // ID + 100, // difficulty + "spell_foci_arcs", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_ARCS } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_SANDBLAST, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_sandblast"); + spell = spellConstructor( + SPELL_FOCI_SANDBLAST, // ID + 100, // difficulty + "spell_foci_sandblast", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_SANDBLAST } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_ELEMENT_METEOR_FLAMES, + 5, // mana + 5, // base mana + 1, // overload + 5, // damage + 0, // duration + "spell_element_flames"); + spellElementMap[SPELL_ELEMENT_METEOR_FLAMES].setDamageMult(1.0); + spellElementMap[SPELL_ELEMENT_METEOR_FLAMES].duration = 250; + spellElementMap[SPELL_ELEMENT_METEOR_FLAMES].setDurationMult(1.0); + + spellElementConstructor(SPELL_METEOR, + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 0, // duration + "spell_element_meteor"); + spell = spellConstructor( + SPELL_METEOR, // ID + 100, // difficulty + "spell_meteor", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_METEOR, SPELL_ELEMENT_METEOR_FLAMES, SPELL_ELEMENT_METEOR_FLAMES, SPELL_ELEMENT_METEOR_FLAMES } + ); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spellElementConstructor(SPELL_METEOR_SHOWER, + 1, // mana + 1, // base mana + 1, // overload + 10, // damage + 0, // duration + "spell_element_meteor_shower"); + spell = spellConstructor( + SPELL_METEOR_SHOWER, // ID + 100, // difficulty + "spell_meteor_shower", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_METEOR_SHOWER, SPELL_ELEMENT_METEOR_FLAMES, SPELL_METEOR, SPELL_ELEMENT_METEOR_FLAMES, SPELL_METEOR, SPELL_ELEMENT_METEOR_FLAMES } + ); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spellElementConstructor(SPELL_ELEMENT_SPRITE_FLAMES, + 2, // mana + 2, // base mana + 1, // overload + 2, // damage + 0, // duration + "spell_element_flames"); + spell = spellConstructor( + SPELL_FLAMES, // ID + 100, // difficulty + "spell_flames", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_ELEMENT_SPRITE_FLAMES } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_ELEMENT_PROPULSION_FLOOR_TILE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_element_propulsion_floor_tile"); + //spellElementConstructor(SPELL_ICE_WAVE, + // 5, // mana + // 5, // base mana + // 1, // overload + // 0, // damage + // 50, // duration + // "spell_element_ice_wave"); + + //spell = spellConstructor( + // SPELL_ICE_WAVE, // ID + // 100, // difficulty + // "spell_ice_wave", // internal name + // // elements + // { SPELL_ELEMENT_PROPULSION_FLOOR_TILE, SPELL_ICE_WAVE, SPELL_ICE_WAVE, SPELL_ICE_WAVE, SPELL_ICE_WAVE, SPELL_ICE_WAVE, SPELL_ICE_WAVE, SPELL_ICE_WAVE, SPELL_ICE_WAVE, SPELL_ICE_WAVE } + //); + //spell->hide_from_ui = true; + //spell->rangefinder = SpellRangefinderType::RANGEFINDER_TARGET; + //spell->distance = 16.0; + + spell = createSimpleSpell( + SPELL_CONJURE_FOOD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_conjure_food"); + + spell = createSimpleSpell( + SPELL_GUARD_BODY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_guard_body", + 1); + spell->sustainEffectDissipate = EFF_GUARD_BODY; + + spell = createSimpleSpell( + SPELL_GUARD_SPIRIT, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_guard_spirit", + 1); + spell->sustainEffectDissipate = EFF_GUARD_SPIRIT; + + spell = createSimpleSpell( + SPELL_DIVINE_GUARD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_divine_guard", + 1); + spell->sustainEffectDissipate = EFF_DIVINE_GUARD; + + spell = createSimpleSpell( + SPELL_PROF_NIMBLENESS, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_prof_nimbleness"); + + spell = createSimpleSpell( + SPELL_PROF_GREATER_MIGHT, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_prof_greater_might"); + + spell = createSimpleSpell( + SPELL_PROF_COUNSEL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_prof_counsel"); + + spell = createSimpleSpell( + SPELL_PROF_STURDINESS, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_prof_sturdiness"); + + spell = createSimpleSpell( + SPELL_BLESS_FOOD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_bless_food"); + + spell = createSimpleSpell( + SPELL_PINPOINT, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_pinpoint"); + + spell = createSimpleSpell( + SPELL_DONATION, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_donation"); + + spell = createSimpleSpell( + SPELL_SCRY_ALLIES, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_scry_allies"); + + spell = createSimpleSpell( + SPELL_SCRY_SHRINES, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_scry_shrines"); + + spell = createSimpleSpell( + SPELL_SCRY_TRAPS, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_scry_traps"); + + spell = createSimpleSpell( + SPELL_SCRY_TREASURES, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_scry_treasures"); + + spell = createSimpleSpell( + SPELL_PENANCE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_penance"); + + spell = createSimpleSpell( + SPELL_CALL_ALLIES, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_call_allies"); + + spell = createSimpleSpell( + SPELL_SACRED_PATH, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_sacred_path"); + + spell = createSimpleSpell( + SPELL_MANIFEST_DESTINY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_manifest_destiny"); + + spell = createSimpleSpell( + SPELL_DETECT_ENEMY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_detect_enemy"); + + spell = createSimpleSpell( + SPELL_TURN_UNDEAD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_turn_undead"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_HEAL_OTHER, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_heal_other"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_BLOOD_WARD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_blood_ward", + 1); + spell->sustainEffectDissipate = EFF_BLOOD_WARD; + + spell = createSimpleSpell( + SPELL_TRUE_BLOOD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_true_blood", + 1); + spell->sustainEffectDissipate = EFF_TRUE_BLOOD; + + spell = createSimpleSpell( + SPELL_DIVINE_ZEAL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_divine_zeal", + 1); + spell->sustainEffectDissipate = EFF_DIVINE_ZEAL; + + spell = createSimpleSpell( + SPELL_ALTER_INSTRUMENT, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_alter_instrument"); + + spell = createSimpleSpell( + SPELL_MAXIMISE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_maximise"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_MINIMISE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_minimise"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_JUMP, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_jump"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spellElementConstructor(SPELL_INCOHERENCE, + 1, // mana + 1, // base mana + 1, // overload + 50, // damage + 0, // duration + "spell_element_incoherence"); + spell = spellConstructor( + SPELL_INCOHERENCE, // ID + 100, // difficulty + "spell_incoherence", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_INCOHERENCE } + ); + + spell = createSimpleSpell( + SPELL_OVERCHARGE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_overcharge", + 1); + spell->sustainEffectDissipate = EFF_OVERCHARGE; + + spell = createSimpleSpell( + SPELL_ENVENOM_WEAPON, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_envenom_weapon", + 1); + spell->sustainEffectDissipate = EFF_ENVENOM_WEAPON; + + spellElementConstructor(SPELL_PSYCHIC_SPEAR, + 1, // mana + 1, // base mana + 1, // overload + 50, // damage + 0, // duration + "spell_element_psychic_spear"); + spell = spellConstructor( + SPELL_PSYCHIC_SPEAR, // ID + 100, // difficulty + "spell_psychic_spear", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_PSYCHIC_SPEAR } + ); + + spellElementConstructor(SPELL_DEFY_FLESH, + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 0, // duration + "spell_element_defy_flesh"); + spell = spellConstructor( + SPELL_DEFY_FLESH, // ID + 100, // difficulty + "spell_defy_flesh", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_DEFY_FLESH } + ); + + spellElementConstructor(SPELL_ELEMENT_PROPULSION_MAGIC_SPRAY, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_propulsion_magic_spray"); + spellElementConstructor(SPELL_GREASE_SPRAY, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_element_grease_spray"); + spell = spellConstructor( + SPELL_GREASE_SPRAY, // ID + 100, // difficulty + "spell_grease_spray", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MAGIC_SPRAY, SPELL_GREASE_SPRAY } + ); + + spellElementConstructor(SPELL_BLOOD_WAVES, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 0, // duration + "spell_element_blood_waves"); + spell = spellConstructor( + SPELL_BLOOD_WAVES, // ID + 100, // difficulty + "spell_blood_waves", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_BLOOD_WAVES } + ); + + spell = createSimpleSpell( + SPELL_BOOBY_TRAP, + 100, // difficulty + 5, // mana + 5, // base mana + 1, // overload + 25, // damage + 50, // duration + "spell_booby_trap"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_COMMAND, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 25, // damage + 50, // duration + "spell_command"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_METALLURGY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_metallurgy"); + + spell = createSimpleSpell( + SPELL_GEOMANCY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_geomancy"); + + spell = createSimpleSpell( + SPELL_FORGE_KEY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_forge_key"); + + spell = createSimpleSpell( + SPELL_FORGE_JEWEL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_forge_jewel"); + + spell = createSimpleSpell( + SPELL_ENHANCE_WEAPON, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_enhance_weapon"); + + spell = createSimpleSpell( + SPELL_RESHAPE_WEAPON, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_reshape_weapon"); + + spell = createSimpleSpell( + SPELL_ALTER_ARROW, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_alter_arrow"); + + spell = createSimpleSpell( + SPELL_VOID_CHEST, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 25, // damage + 50, // duration + "spell_void_chest"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_PUNCTURE_VOID, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_puncture_void"); + + spell = createSimpleSpell( + SPELL_CURSE_FLESH, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_curse_flesh"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_REVENANT_CURSE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_revenant_curse"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_SPIRIT_WEAPON, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 2, // damage + 1, // duration + "spell_spirit_weapon"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_ADORCISM, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_adorcism"); + + spellElementConstructor(SPELL_LEAD_BOLT, + 0, // mana + 0, // base mana + 1, // overload + 5, // damage + 0, // duration + "spell_element_lead_bolt"); + spell = spellConstructor( + SPELL_LEAD_BOLT, // ID + 100, // difficulty + "spell_lead_bolt", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE_NOCOST, SPELL_LEAD_BOLT } + ); + + spellElementConstructor(SPELL_MERCURY_BOLT, + 10, // mana + 10, // base mana + 1, // overload + 10, // damage + 0, // duration + "spell_element_mercury_bolt"); + spell = spellConstructor( + SPELL_MERCURY_BOLT, // ID + 100, // difficulty + "spell_mercury_bolt", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE_NOCOST, SPELL_MERCURY_BOLT } + ); + + spellElementConstructor(SPELL_NUMBING_BOLT, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 0, // duration + "spell_element_numbing_bolt"); + spell = spellConstructor( + SPELL_NUMBING_BOLT, // ID + 100, // difficulty + "spell_numbing_bolt", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_NUMBING_BOLT } + ); + + spell = createSimpleSpell( + SPELL_DELAY_PAIN, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_delay_pain"); + + spell = createSimpleSpell( + SPELL_SEEK_ALLY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_seek_ally"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_SEEK_FOE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_seek_foe"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_TABOO, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_taboo"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_COURAGE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_courage"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_COWARDICE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_cowardice"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_DEEP_SHADE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_deep_shade", + 1); + + spellElementConstructor(SPELL_SHADE_BOLT, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 0, // duration + "spell_element_shade_bolt"); + spell = spellConstructor( + SPELL_SHADE_BOLT, // ID + 100, // difficulty + "spell_shade_bolt", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_SHADE_BOLT } + ); + + spellElementConstructor(SPELL_WONDERLIGHT, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 0, // duration + "spell_element_wonderlight"); + spell = spellConstructor( + SPELL_WONDERLIGHT, // ID + 100, // difficulty + "spell_wonderlight", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_WONDERLIGHT } + ); + + spell = createSimpleSpell( + SPELL_SPORES, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_spores", + 1); + spell->sustainEffectDissipate = EFF_SPORES; + + spellElementConstructor(SPELL_SPORE_BOMB, + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 0, // duration + "spell_element_spore_bomb"); + spell = spellConstructor( + SPELL_SPORE_BOMB, // ID + 100, // difficulty + "spell_spore_bomb", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_SPORE_BOMB } + ); + + spell = createSimpleSpell( + SPELL_WINDGATE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_windgate"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_WALL_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_TELEKINESIS, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_telekinesis"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_INTERACT; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_KINETIC_PUSH, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_kinetic_push"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_DISARM, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_disarm"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_STRIP, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_strip"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_ABUNDANCE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_abundance", + 1); + spell->sustainEffectDissipate = EFF_ABUNDANCE; + + spell = createSimpleSpell( + SPELL_GREATER_ABUNDANCE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_greater_abundance", + 1); + spell->sustainEffectDissipate = EFF_GREATER_ABUNDANCE; + + spell = createSimpleSpell( + SPELL_PRESERVE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_preserve", + 1); + spell->sustainEffectDissipate = EFF_PRESERVE; + + spell = createSimpleSpell( + SPELL_RESTORE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_restore"); + + spell = createSimpleSpell( + SPELL_SABOTAGE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 50, // damage + 1, // duration + "spell_sabotage"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_HARVEST_TRAP, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_harvest_trap"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_MIST_FORM, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_mist_form", + 1); + spell->sustainEffectDissipate = EFF_MIST_FORM; + + spell = createSimpleSpell( + SPELL_HOLOGRAM, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_hologram"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_FORCE_SHIELD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_force_shield"); + + spell = createSimpleSpell( + SPELL_REFLECTOR, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_reflector"); + + //spell = createSimpleSpell( + // SPELL_SPLINTER_GEAR, + // 100, // difficulty + // 1, // mana + // 1, // base mana + // 1, // overload + // 25, // damage + // 1, // duration + // "spell_splinter_gear"); + //spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + //spell->distance = 64.0; + + spellElementConstructor(SPELL_SPLINTER_GEAR, + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 0, // duration + "spell_element_splinter_gear"); + spell = spellConstructor( + SPELL_SPLINTER_GEAR, // ID + 100, // difficulty + "spell_splinter_gear", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_SPLINTER_GEAR } + ); + + spell = createSimpleSpell( + SPELL_LIGHTEN_LOAD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_lighten_load", + 1); + spell->sustainEffectDissipate = EFF_LIGHTEN_LOAD; + + spell = createSimpleSpell( + SPELL_ATTRACT_ITEMS, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_attract_items", + 1); + spell->sustainEffectDissipate = EFF_ATTRACT_ITEMS; + + spell = createSimpleSpell( + SPELL_RETURN_ITEMS, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_return_items", + 1); + spell->sustainEffectDissipate = EFF_RETURN_ITEM; + + spell = createSimpleSpell( + SPELL_DEFACE, + 100, // difficulty + 10, // mana + 10, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_deface"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_DEMESNE_DOOR, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 25, // damage + 1, // duration + "spell_demesne_door"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_TUNNEL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_tunnel"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_WALL_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_NULL_AREA, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_null_area"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spellElementConstructor(SPELL_SPHERE_SILENCE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 0, // duration + "spell_element_sphere_silence"); + spell = spellConstructor( + SPELL_SPHERE_SILENCE, // ID + 100, // difficulty + "spell_sphere_silence", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_SPHERE_SILENCE } + ); + + spell = createSimpleSpell( + SPELL_FORGE_METAL_SCRAP, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_forge_metal_scrap"); + + spell = createSimpleSpell( + SPELL_FORGE_MAGIC_SCRAP, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_forge_magic_scrap"); + + spell = createSimpleSpell( + SPELL_FIRE_SPRITE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_fire_sprite"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_FLAME_ELEMENTAL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_flame_elemental"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spellElementConstructor(SPELL_SPIN, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 0, // duration + "spell_element_spin"); + spell = spellConstructor( + SPELL_SPIN, // ID + 100, // difficulty + "spell_spin", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_SPIN } + ); + + spellElementConstructor(SPELL_DIZZY, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 0, // duration + "spell_element_dizzy"); + spell = spellConstructor( + SPELL_DIZZY, // ID + 100, // difficulty + "spell_dizzy", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_DIZZY } + ); + + spell = createSimpleSpell( + SPELL_VANDALISE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_vandalise"); + + spell = createSimpleSpell( + SPELL_DESECRATE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_desecrate"); + + spell = createSimpleSpell( + SPELL_SANCTIFY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_sanctify"); + + spell = createSimpleSpell( + SPELL_SANCTIFY_WATER, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_sanctify_water"); + + spell = createSimpleSpell( + SPELL_CLEANSE_FOOD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_cleanse_food"); + + spell = createSimpleSpell( + SPELL_ADORCISE_INSTRUMENT, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_adorcise_instrument"); + + spell = createSimpleSpell( + SPELL_CRITICAL_SPELL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_critical_spell"); + + spell = createSimpleSpell( + SPELL_MAGIC_WELL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_magic_well"); + + spell = createSimpleSpell( + SPELL_FLAME_CLOAK, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_flame_cloak", + 1); + spell->sustainEffectDissipate = EFF_FLAME_CLOAK; + + spell = createSimpleSpell( + SPELL_FLAME_SHIELD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_flame_shield"); + + spell = createSimpleSpell( + SPELL_ABSORB_MAGIC, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_absorb_magic"); + + spell = createSimpleSpell( + SPELL_LIGHTNING_BOLT, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 20, // damage + 1, // duration + "spell_lightning_bolt"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_DISRUPT_EARTH, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_disrupt_earth"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_EARTH_SPINES, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_earth_spines"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_FIRE_WALL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_fire_wall"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_ICE_WAVE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_ice_wave"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_SLAM, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_slam"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_KINETIC_FIELD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_kinetic_field"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_CHRONOMIC_FIELD, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_chronomic_field"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_ETERNALS_GAZE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 12, // damage + 1, // duration + "spell_eternals_gaze"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_SHATTER_EARTH, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 20, // damage + 1, // duration + "spell_shatter_earth"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_EARTH_ELEMENTAL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 20, // damage + 1, // duration + "spell_earth_elemental"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_ROOTS, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_roots"); + + spell = createSimpleSpell( + SPELL_MUSHROOM, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_mushroom"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spellElementConstructor(SPELL_MYCELIUM_BOMB, + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 0, // duration + "spell_element_mycelium_bomb"); + spell = spellConstructor( + SPELL_MYCELIUM_BOMB, // ID + 100, // difficulty + "spell_mycelium_bomb", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_MYCELIUM_BOMB } + ); + + spell = createSimpleSpell( + SPELL_MYCELIUM_SPORES, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 750, // duration + "spell_mycelium_spores", + 1); + + spell = createSimpleSpell( + SPELL_HEAL_PULSE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_heal_pulse"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_SHRUB, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 5, // damage + 1, // duration + "spell_shrub"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + spell->distance = 64.0; + + spell = createSimpleSpell( + SPELL_THORNS, + 100, // difficulty + 5, // mana + 1, // base mana + 1, // overload + 4, // damage + 200, // duration + "spell_thorns", + 1); + spell->sustainEffectDissipate = EFF_THORNS; + + spell = createSimpleSpell( + SPELL_BLADEVINES, + 100, // difficulty + 10, // mana + 1, // base mana + 1, // overload + 6, // damage + 200, // duration + "spell_bladevines", + 4); + spell->sustainEffectDissipate = EFF_BLADEVINES; + + spell = createSimpleSpell( + SPELL_BASTION_MUSHROOM, + 100, // difficulty + 10, // mana + 1, // base mana + 1, // overload + 6, // damage + 200, // duration + "spell_bastion_mushroom", + 4); + spell->sustainEffectDissipate = EFF_BASTION_MUSHROOM; + + spell = createSimpleSpell( + SPELL_BASTION_ROOTS, + 100, // difficulty + 10, // mana + 1, // base mana + 1, // overload + 6, // damage + 200, // duration + "spell_bastion_roots", + 4); + spell->sustainEffectDissipate = EFF_BASTION_ROOTS; + + spellElementConstructor(SPELL_FOCI_DARK_LIFE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_dark_life"); + spell = spellConstructor( + SPELL_FOCI_DARK_LIFE, // ID + 100, // difficulty + "spell_foci_dark_life", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_DARK_LIFE } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_DARK_RIFT, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_dark_rift"); + spell = spellConstructor( + SPELL_FOCI_DARK_RIFT, // ID + 100, // difficulty + "spell_foci_dark_rift", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_DARK_RIFT } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_DARK_SILENCE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_dark_silence"); + spell = spellConstructor( + SPELL_FOCI_DARK_SILENCE, // ID + 100, // difficulty + "spell_foci_dark_silence", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_DARK_SILENCE } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_DARK_VENGEANCE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_dark_vengeance"); + spell = spellConstructor( + SPELL_FOCI_DARK_VENGEANCE, // ID + 100, // difficulty + "spell_foci_dark_vengeance", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_DARK_VENGEANCE } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_DARK_SUPPRESS, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_dark_suppress"); + spell = spellConstructor( + SPELL_FOCI_DARK_SUPPRESS, // ID + 100, // difficulty + "spell_foci_dark_suppress", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_DARK_SUPPRESS } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_LIGHT_PEACE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_light_peace"); + spell = spellConstructor( + SPELL_FOCI_LIGHT_PEACE, // ID + 100, // difficulty + "spell_foci_light_peace", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_LIGHT_PEACE } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_LIGHT_JUSTICE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_light_justice"); + spell = spellConstructor( + SPELL_FOCI_LIGHT_JUSTICE, // ID + 100, // difficulty + "spell_foci_light_justice", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_LIGHT_JUSTICE} + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_LIGHT_PROVIDENCE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_light_providence"); + spell = spellConstructor( + SPELL_FOCI_LIGHT_PROVIDENCE, // ID + 100, // difficulty + "spell_foci_light_providence", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_LIGHT_PROVIDENCE } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_LIGHT_PURITY, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_light_purity"); + spell = spellConstructor( + SPELL_FOCI_LIGHT_PURITY, // ID + 100, // difficulty + "spell_foci_light_purity", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_LIGHT_PURITY } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_FOCI_LIGHT_SANCTUARY, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_foci_light_sanctuary"); + spell = spellConstructor( + SPELL_FOCI_LIGHT_SANCTUARY, // ID + 100, // difficulty + "spell_foci_light_sanctuary", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_FOCI_SPRAY, SPELL_FOCI_LIGHT_SANCTUARY } + ); + spell->hide_from_ui = true; + + spellElementConstructor(SPELL_SCEPTER_BLAST, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_scepter_blast"); + spell = spellConstructor( + SPELL_SCEPTER_BLAST, // ID + 100, // difficulty + "spell_scepter_blast", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_SCEPTER_BLAST } + ); + spell->hide_from_ui = true; + + spell = createSimpleSpell( + SPELL_IGNITE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_ignite"); + + spell = createSimpleSpell( + SPELL_SHATTER_OBJECTS, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_shatter_objects"); + + spell = createSimpleSpell( + SPELL_MAGICIANS_ARMOR, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_magicians_armor"); + + spell = createSimpleSpell( + SPELL_PROJECT_SPIRIT, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_project_spirit"); + + spellElementConstructor(SPELL_BREATHE_FIRE, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_element_breathe_fire"); + spell = spellConstructor( + SPELL_BREATHE_FIRE, // ID + 100, // difficulty + "spell_breathe_fire", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MAGIC_SPRAY, SPELL_BREATHE_FIRE } + ); + + createSimpleSpell( + SPELL_HEAL_MINOR, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 50, // duration + "spell_heal_minor"); + + spell = createSimpleSpell( + SPELL_HOLY_FIRE, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_holy_fire"); + spell->hide_from_ui = true; + + spell = createSimpleSpell( + SPELL_SIGIL, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_sigil"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + + spell = createSimpleSpell( + SPELL_SANCTUARY, + 100, // difficulty + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 1, // duration + "spell_sanctuary"); + spell->rangefinder = SpellRangefinderType::RANGEFINDER_TOUCH_FLOOR_TILE; + + spellElementConstructor(SPELL_HOLY_BEAM, + 1, // mana + 1, // base mana + 1, // overload + 0, // damage + 100, // duration + "spell_element_holy_beam"); + spell = spellConstructor( + SPELL_HOLY_BEAM, // ID + 100, // difficulty + "spell_holy_beam", // internal name + // elements + { SPELL_ELEMENT_PROPULSION_MISSILE, SPELL_HOLY_BEAM } + ); + + //static const int SPELL_LIGHTNING_NEXUS = 182; + //static const int SPELL_LIFT = 184; + //static const int SPELL_IGNITE = 186; + //static const int SPELL_SHATTER_OBJECTS = 187; + //static const int SPELL_KINETIC_FIELD = 188; + //static const int SPELL_ICE_BLOCK = 189; + + std::vector subElementsToCopy; + std::map spellSchoolCounters; + for ( int i = 0; i < NUM_SPELLS; ++i ) + { + auto find = ItemTooltips.spellItems.find(i); + if ( find != ItemTooltips.spellItems.end() ) + { + if ( allGameSpells.find(i) == allGameSpells.end() ) + { + continue; + } + if ( !find->second.hasExpandedJSON ) + { + continue; + } + if ( auto spell = getSpellFromID(i) ) + { + auto& info = find->second; + spell->life_time = info.life_time; + spell->difficulty = info.difficulty; + spell->distance = info.distance; + spell->distance_mult = info.distance_mult; + spell->mana = info.mana; + spell->cast_time = info.cast_time; + spell->cast_time_mult = info.cast_time_mult; + spell->skillID = info.skillID; + spell->radius = info.radius; + spell->radius_mult = info.radius_mult; + spell->drop_table = info.drop_table; + if ( spell->drop_table < 0 ) + { + spell->hide_from_ui = true; + } + spellTomeAppearanceToID[spell->skillID][spellSchoolCounters[spell->skillID]] = i; + spellTomeIDToAppearance[i] = spellSchoolCounters[spell->skillID]; + ++spellSchoolCounters[spell->skillID]; + spellElement_t* element = nullptr; + std::vector elementList; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + node_t* node = element->elements.first; + element = (spellElement_t*)element->elements.first->element; + + node = node->next; + while ( node ) + { + elementList.push_back((spellElement_t*)node->element); + node = node->next; + } + } + } + } + if ( element ) + { + element->channeledMana = info.sustain_mana; + if ( find->second.fociId >= 0 ) + { + element->fociSpell = true; + } + element->setChanneledManaDuration(info.sustain_duration); + element->setChanneledManaMult(info.sustain_mult); + element->setDamage(info.damage); + element->setDamageMult(info.damage_mult); + element->setDamageSecondary(info.damage2); + element->setDamageSecondaryMult(info.damage2_mult); + element->duration = info.duration; + element->setDurationSecondary(info.duration2); + } + + while ( elementList.size() ) + { + if ( element = elementList.at(0) ) + { + // additional hacks for meteor shower + if ( !strcmp(element->element_internal_name, spellElementMap[SPELL_METEOR].element_internal_name) ) + { + auto find = ItemTooltips.spellItems.find(SPELL_METEOR); + if ( find != ItemTooltips.spellItems.end() ) + { + auto& info = find->second; + + element->channeledMana = info.sustain_mana; + if ( find->second.fociId >= 0 ) + { + element->fociSpell = true; + } + element->setChanneledManaDuration(info.sustain_duration); + element->setChanneledManaMult(info.sustain_mult); + element->setDamage(info.damage); + element->setDamageMult(info.damage_mult); + element->setDamageSecondary(info.damage2); + element->setDamageSecondaryMult(info.damage2_mult); + element->duration = info.duration; + element->setDurationSecondary(info.duration2); + } + } + } + elementList.erase(elementList.begin()); + } + } + } + } } + +spell_t* createSimpleSpell(int spellID, int difficulty, int mana, int base_mana, int overload_mult, int damage, + int duration, const char* internal_name, int sustainedMP) +{ + std::string elementName = internal_name; + if ( elementName.find("element_") == std::string::npos ) + { + auto find = elementName.find("spell_"); + if ( find != std::string::npos ) + { + elementName.insert(find + strlen("spell_"), "element_"); + } + } + spellElementConstructor(spellID, + mana, // mana + base_mana, // base mana + overload_mult, // overload + damage, // damage + duration, // duration + elementName.c_str()); + spellElementMap[spellID].channeledMana = sustainedMP; + spell_t* spell = spellConstructor( + // ID + spellID, + // difficulty + difficulty, + // internal name + internal_name, + // elements + { spellID } + ); + return spell; +} \ No newline at end of file diff --git a/src/magic/spell.cpp b/src/magic/spell.cpp index 0f5091ead..518d833ae 100644 --- a/src/magic/spell.cpp +++ b/src/magic/spell.cpp @@ -81,6 +81,7 @@ spellElement_t spellElement_slimeFire; spellElement_t spellElement_slimeTar; spellElement_t spellElement_slimeMetal; spellElement_t spellElement_slime_spray; +std::map spellElementMap; spell_t spell_forcebolt; spell_t spell_magicmissile; @@ -141,6 +142,7 @@ spell_t spell_slime_water; spell_t spell_slime_fire; spell_t spell_slime_tar; spell_t spell_slime_metal; +std::map spellMap; bool addSpell(int spell, int player, bool ignoreSkill) { @@ -154,191 +156,206 @@ bool addSpell(int spell, int player, bool ignoreSkill) spell_t* new_spell = nullptr; - switch ( spell ) + auto find = allGameSpells.find(spell); + assert(find != allGameSpells.end()); + if ( find != allGameSpells.end() ) { - case SPELL_FORCEBOLT: - new_spell = copySpell(&spell_forcebolt); - break; - case SPELL_MAGICMISSILE: - new_spell = copySpell(&spell_magicmissile); - break; - case SPELL_COLD: - new_spell = copySpell(&spell_cold); - break; - case SPELL_FIREBALL: - new_spell = copySpell(&spell_fireball); - break; - case SPELL_LIGHTNING: - new_spell = copySpell(&spell_lightning); - break; - case SPELL_REMOVECURSE: - new_spell = copySpell(&spell_removecurse); - break; - case SPELL_LIGHT: - new_spell = copySpell(&spell_light); - break; - case SPELL_IDENTIFY: - new_spell = copySpell(&spell_identify); - break; - case SPELL_MAGICMAPPING: - new_spell = copySpell(&spell_magicmapping); - break; - case SPELL_SLEEP: - new_spell = copySpell(&spell_sleep); - break; - case SPELL_CONFUSE: - new_spell = copySpell(&spell_confuse); - break; - case SPELL_SLOW: - new_spell = copySpell(&spell_slow); - break; - case SPELL_OPENING: - new_spell = copySpell(&spell_opening); - break; - case SPELL_LOCKING: - new_spell = copySpell(&spell_locking); - break; - case SPELL_LEVITATION: - new_spell = copySpell(&spell_levitation); - break; - case SPELL_INVISIBILITY: - new_spell = copySpell(&spell_invisibility); - break; - case SPELL_TELEPORTATION: - new_spell = copySpell(&spell_teleportation); - break; - case SPELL_HEALING: - new_spell = copySpell(&spell_healing); - break; - case SPELL_EXTRAHEALING: - new_spell = copySpell(&spell_extrahealing); - break; - case SPELL_CUREAILMENT: - new_spell = copySpell(&spell_cureailment); - break; - case SPELL_DIG: - new_spell = copySpell(&spell_dig); - break; - case SPELL_STONEBLOOD: - new_spell = copySpell(&spell_stoneblood); - break; - case SPELL_BLEED: - new_spell = copySpell(&spell_bleed); - break; - case SPELL_SUMMON: - new_spell = copySpell(&spell_summon); - break; - case SPELL_DOMINATE: - new_spell = copySpell(&spell_dominate); - break; - case SPELL_REFLECT_MAGIC: - new_spell = copySpell(&spell_reflectMagic); - break; - case SPELL_ACID_SPRAY: - new_spell = copySpell(&spell_acidSpray); - break; - case SPELL_STEAL_WEAPON: - new_spell = copySpell(&spell_stealWeapon); - break; - case SPELL_DRAIN_SOUL: - new_spell = copySpell(&spell_drainSoul); - break; - case SPELL_VAMPIRIC_AURA: - new_spell = copySpell(&spell_vampiricAura); - break; - case SPELL_CHARM_MONSTER: - new_spell = copySpell(&spell_charmMonster); - break; - case SPELL_REVERT_FORM: - new_spell = copySpell(&spell_revertForm); - break; - case SPELL_RAT_FORM: - new_spell = copySpell(&spell_ratForm); - break; - case SPELL_SPIDER_FORM: - new_spell = copySpell(&spell_spiderForm); - break; - case SPELL_TROLL_FORM: - new_spell = copySpell(&spell_trollForm); - break; - case SPELL_IMP_FORM: - new_spell = copySpell(&spell_impForm); - break; - case SPELL_SPRAY_WEB: - new_spell = copySpell(&spell_sprayWeb); - break; - case SPELL_POISON: - new_spell = copySpell(&spell_poison); - break; - case SPELL_SPEED: - new_spell = copySpell(&spell_speed); - break; - case SPELL_FEAR: - new_spell = copySpell(&spell_fear); - break; - case SPELL_STRIKE: - new_spell = copySpell(&spell_strike); - break; - case SPELL_DETECT_FOOD: - new_spell = copySpell(&spell_detectFood); - break; - case SPELL_WEAKNESS: - new_spell = copySpell(&spell_weakness); - break; - case SPELL_AMPLIFY_MAGIC: - new_spell = copySpell(&spell_amplifyMagic); - break; - case SPELL_SHADOW_TAG: - new_spell = copySpell(&spell_shadowTag); - break; - case SPELL_TELEPULL: - new_spell = copySpell(&spell_telePull); - break; - case SPELL_DEMON_ILLUSION: - new_spell = copySpell(&spell_demonIllusion); - break; - case SPELL_TROLLS_BLOOD: - new_spell = copySpell(&spell_trollsBlood); - break; - case SPELL_SALVAGE: - new_spell = copySpell(&spell_salvageItem); - break; - case SPELL_FLUTTER: - new_spell = copySpell(&spell_flutter); - break; - case SPELL_DASH: - new_spell = copySpell(&spell_dash); - break; - case SPELL_SELF_POLYMORPH: - new_spell = copySpell(&spell_polymorph); - break; - case SPELL_CRAB_FORM: - new_spell = copySpell(&spell_spiderForm); - break; - case SPELL_CRAB_WEB: - new_spell = copySpell(&spell_sprayWeb); - break; - case SPELL_GHOST_BOLT: - new_spell = copySpell(&spell_ghost_bolt); - break; - case SPELL_SLIME_ACID: - new_spell = copySpell(&spell_slime_acid); - break; - case SPELL_SLIME_WATER: - new_spell = copySpell(&spell_slime_water); - break; - case SPELL_SLIME_FIRE: - new_spell = copySpell(&spell_slime_fire); - break; - case SPELL_SLIME_TAR: - new_spell = copySpell(&spell_slime_tar); - break; - case SPELL_SLIME_METAL: - new_spell = copySpell(&spell_slime_metal); - break; - default: - return false; + new_spell = copySpell(find->second); } + else + { + switch ( spell ) + { + case SPELL_FORCEBOLT: + new_spell = copySpell(&spell_forcebolt); + break; + case SPELL_MAGICMISSILE: + new_spell = copySpell(&spell_magicmissile); + break; + case SPELL_COLD: + new_spell = copySpell(&spell_cold); + break; + case SPELL_FIREBALL: + new_spell = copySpell(&spell_fireball); + break; + case SPELL_LIGHTNING: + new_spell = copySpell(&spell_lightning); + break; + case SPELL_REMOVECURSE: + new_spell = copySpell(&spell_removecurse); + break; + case SPELL_LIGHT: + new_spell = copySpell(&spell_light); + break; + case SPELL_IDENTIFY: + new_spell = copySpell(&spell_identify); + break; + case SPELL_MAGICMAPPING: + new_spell = copySpell(&spell_magicmapping); + break; + case SPELL_SLEEP: + new_spell = copySpell(&spell_sleep); + break; + case SPELL_CONFUSE: + new_spell = copySpell(&spell_confuse); + break; + case SPELL_SLOW: + new_spell = copySpell(&spell_slow); + break; + case SPELL_OPENING: + new_spell = copySpell(&spell_opening); + break; + case SPELL_LOCKING: + new_spell = copySpell(&spell_locking); + break; + case SPELL_LEVITATION: + new_spell = copySpell(&spell_levitation); + break; + case SPELL_INVISIBILITY: + new_spell = copySpell(&spell_invisibility); + break; + case SPELL_TELEPORTATION: + new_spell = copySpell(&spell_teleportation); + break; + case SPELL_HEALING: + new_spell = copySpell(&spell_healing); + break; + case SPELL_EXTRAHEALING: + new_spell = copySpell(&spell_extrahealing); + break; + case SPELL_CUREAILMENT: + new_spell = copySpell(&spell_cureailment); + break; + case SPELL_DIG: + new_spell = copySpell(&spell_dig); + break; + case SPELL_STONEBLOOD: + new_spell = copySpell(&spell_stoneblood); + break; + case SPELL_BLEED: + new_spell = copySpell(&spell_bleed); + break; + case SPELL_SUMMON: + new_spell = copySpell(&spell_summon); + break; + case SPELL_DOMINATE: + new_spell = copySpell(&spell_dominate); + break; + case SPELL_REFLECT_MAGIC: + new_spell = copySpell(&spell_reflectMagic); + break; + case SPELL_ACID_SPRAY: + new_spell = copySpell(&spell_acidSpray); + break; + case SPELL_STEAL_WEAPON: + new_spell = copySpell(&spell_stealWeapon); + break; + case SPELL_DRAIN_SOUL: + new_spell = copySpell(&spell_drainSoul); + break; + case SPELL_VAMPIRIC_AURA: + new_spell = copySpell(&spell_vampiricAura); + break; + case SPELL_CHARM_MONSTER: + new_spell = copySpell(&spell_charmMonster); + break; + case SPELL_REVERT_FORM: + new_spell = copySpell(&spell_revertForm); + break; + case SPELL_RAT_FORM: + new_spell = copySpell(&spell_ratForm); + break; + case SPELL_SPIDER_FORM: + new_spell = copySpell(&spell_spiderForm); + break; + case SPELL_TROLL_FORM: + new_spell = copySpell(&spell_trollForm); + break; + case SPELL_IMP_FORM: + new_spell = copySpell(&spell_impForm); + break; + case SPELL_SPRAY_WEB: + new_spell = copySpell(&spell_sprayWeb); + break; + case SPELL_POISON: + new_spell = copySpell(&spell_poison); + break; + case SPELL_SPEED: + new_spell = copySpell(&spell_speed); + break; + case SPELL_FEAR: + new_spell = copySpell(&spell_fear); + break; + case SPELL_STRIKE: + new_spell = copySpell(&spell_strike); + break; + case SPELL_DETECT_FOOD: + new_spell = copySpell(&spell_detectFood); + break; + case SPELL_WEAKNESS: + new_spell = copySpell(&spell_weakness); + break; + case SPELL_AMPLIFY_MAGIC: + new_spell = copySpell(&spell_amplifyMagic); + break; + case SPELL_SHADOW_TAG: + new_spell = copySpell(&spell_shadowTag); + break; + case SPELL_TELEPULL: + new_spell = copySpell(&spell_telePull); + break; + case SPELL_DEMON_ILLUSION: + new_spell = copySpell(&spell_demonIllusion); + break; + case SPELL_TROLLS_BLOOD: + new_spell = copySpell(&spell_trollsBlood); + break; + case SPELL_SALVAGE: + new_spell = copySpell(&spell_salvageItem); + break; + case SPELL_FLUTTER: + new_spell = copySpell(&spell_flutter); + break; + case SPELL_DASH: + new_spell = copySpell(&spell_dash); + break; + case SPELL_SELF_POLYMORPH: + new_spell = copySpell(&spell_polymorph); + break; + case SPELL_CRAB_FORM: + new_spell = copySpell(&spell_spiderForm); + break; + case SPELL_CRAB_WEB: + new_spell = copySpell(&spell_sprayWeb); + break; + case SPELL_GHOST_BOLT: + new_spell = copySpell(&spell_ghost_bolt); + break; + case SPELL_SLIME_ACID: + new_spell = copySpell(&spell_slime_acid); + break; + case SPELL_SLIME_WATER: + new_spell = copySpell(&spell_slime_water); + break; + case SPELL_SLIME_FIRE: + new_spell = copySpell(&spell_slime_fire); + break; + case SPELL_SLIME_TAR: + new_spell = copySpell(&spell_slime_tar); + break; + case SPELL_SLIME_METAL: + new_spell = copySpell(&spell_slime_metal); + break; + default: + return false; + } + } + + if ( !new_spell ) + { + return false; + } + if ( spellInList(&players[player]->magic.spellList, new_spell) ) { // check inventory for shapeshift spells @@ -388,16 +405,16 @@ bool addSpell(int spell, int player, bool ignoreSkill) else { // can't learn, already have it. - if ( !(spell == SPELL_FORCEBOLT && skillCapstoneUnlocked(player, PRO_SPELLCASTING)) ) - { - messagePlayer(player, MESSAGE_STATUS, Language::get(439), new_spell->getSpellName()); - } + //if ( !(spell == SPELL_FORCEBOLT && skillCapstoneUnlocked(player, PRO_LEGACY_SPELLCASTING)) ) + //{ + //} + messagePlayer(player, MESSAGE_STATUS, Language::get(439), new_spell->getSpellName()); spellDeconstructor((void*)new_spell); return false; } } - int skillLVL = stats[player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[player], players[player]->entity); - if ( stats[player]->getModifiedProficiency(PRO_MAGIC) >= 100 ) + int skillLVL = stats[player]->getModifiedProficiency(new_spell->skillID) + statGetINT(stats[player], players[player]->entity); + if ( stats[player]->getModifiedProficiency(new_spell->skillID) >= 100 ) { skillLVL = 100; } @@ -422,7 +439,15 @@ bool addSpell(int spell, int player, bool ignoreSkill) { if ( players[player]->entity ) { - players[player]->entity->increaseSkill(PRO_MAGIC); + if ( players[player]->mechanics.learnedSpells.find(new_spell->ID) != players[player]->mechanics.learnedSpells.end() ) + { + // no level up + } + else + { + players[player]->mechanics.learnedSpells.insert(new_spell->ID); + players[player]->entity->increaseSkill(new_spell->skillID); + } } } @@ -458,20 +483,113 @@ bool addSpell(int spell, int player, bool ignoreSkill) return true; } -void spellConstructor(spell_t* spell) +void spellConstructor(spell_t* spell, int ID) { - spell->ID = -1; + *spell = spell_t(); + spell->ID = ID; strcpy(spell->spell_internal_name, "spell_default"); spell->elements.first = NULL; spell->elements.last = NULL; + spell->difficulty = 100; spell->sustain = true; spell->magicstaff = false; + spell->spellbook = false; spell->sustain_node = NULL; spell->magic_effects_node = NULL; spell->caster = -1; spell->channel_duration = 0; + spell->rangefinder = SpellRangefinderType::RANGEFINDER_NONE; + spell->hide_from_ui = false; + spell->distance = 0.0; + spell->distance_mult = 1.0; + spell->skillID = PRO_SORCERY; + spell->cast_time = 1.0; + spell->cast_time_mult = 1.0; + spell->radius = 0.0; + spell->radius_mult = 0.0; + spell->drop_table = -1; + spell->life_time = 0; + spell->life_time_mult = 1.0; + spell->sustainEffectDissipate = -1; + spell->channel_duration = 0; + spell->channel_effectStrength = 1; //spell->timer = 0; - allGameSpells.push_back(spell); + assert(allGameSpells.find(ID) == allGameSpells.end()); + allGameSpells[ID] = spell; +} + +spell_t* spellConstructor(int ID, int difficulty, const char* internal_name, std::vector elements) +{ + spell_t* spell = (spell_t*)malloc(sizeof(spell_t)); + if ( spell ) + { + spellConstructor(spell, ID); + strcpy(spell->spell_internal_name, internal_name); + spell->difficulty = difficulty; + spell->needsDataFreed = 1; + + list_t* list = &spell->elements; + for ( auto elementID : elements ) + { + auto find = spellElementMap.find(elementID); + if ( find != spellElementMap.end() ) + { + node_t* node = list_AddNodeLast(list); + node->element = copySpellElement(&find->second); + node->size = sizeof(spellElement_t); + node->deconstructor = &spellElementDeconstructor; + spellElement_t* element = (spellElement_t*)node->element; + element->node = node; //Tell the element what list it resides in. + if ( list == &spell->elements ) + { + list = &element->elements; // next elements beyond the first will append to the inner list + } + } + } + } + + return spell; +} + +void spellChanneledClientDeconstructor(void* data) +{ + spell_t* spell; + + if ( data != NULL ) + { + spell = (spell_t*)data; + + if ( spell_isChanneled(spell) ) + { + if ( spell->sustain_node ) + { + /*if ( multiplayer != CLIENT ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( spell->sustain_node->list && spell->sustain_node->list == &channeledSpells[i] ) + { + if ( i > 0 && !players[i]->isLocalPlayer() && !client_disconnected[i] ) + { + strcpy((char*)net_packet->data, "UNCH"); + net_packet->data[4] = i; + SDLNet_Write32(spell->ID, &net_packet->data[5]); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + break; + } + } + }*/ + //list_RemoveNode(spell->sustain_node); + } + } + + list_FreeAll(&spell->elements); + free(spell); + } } void spellDeconstructor(void* data) @@ -486,6 +604,26 @@ void spellDeconstructor(void* data) { if ( spell->sustain_node ) { + if ( multiplayer != CLIENT ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( spell->sustain_node->list && spell->sustain_node->list == &channeledSpells[i] ) + { + if ( i > 0 && !players[i]->isLocalPlayer() && !client_disconnected[i] ) + { + strcpy((char*)net_packet->data, "UNCH"); + net_packet->data[4] = i; + SDLNet_Write32(spell->ID, &net_packet->data[5]); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 9; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + break; + } + } + } list_RemoveNode(spell->sustain_node); } } @@ -497,17 +635,33 @@ void spellDeconstructor(void* data) void spellElementConstructor(spellElement_t* element) { - element->mana = 0; - element->base_mana = 0; - element->overload_multiplier = 1; - element->damage = 0; + //element->mana = 0; + //element->base_mana = 0; + //element->overload_multiplier = 1; + element->setDamage(0); element->duration = 0; + element->elementID = 0; element->can_be_learned = true; strcpy(element->element_internal_name, "element_default"); element->elements.first = NULL; element->elements.last = NULL; element->node = NULL; - element->channeled = false; + element->channeledMana = 0; +} + +void spellElementConstructor(int elementID, int mana, int base_mana, int overload_mult, int damage, int duration, const char* internal_name) +{ + assert(spellElementMap.find(elementID) == spellElementMap.end()); + + auto& element = spellElementMap[elementID]; + spellElementConstructor(&element); + //element.mana = mana; + //element.base_mana = base_mana; + //element.overload_multiplier = overload_mult; + element.setDamage(damage); + element.duration = duration; + element.elementID = elementID; + strcpy(element.element_internal_name, internal_name); } void spellElementDeconstructor(void* data) @@ -522,20 +676,14 @@ void spellElementDeconstructor(void* data) } } -spellElement_t* newSpellelement() -{ - spellElement_t* element = (spellElement_t*) malloc(sizeof(spellElement_t)); - spellElementConstructor(element); - return element; -} - -spell_t* copySpell(spell_t* spell) +spell_t* copySpell(spell_t* spell, int subElementIndexToCopy) { node_t* node; spell_t* result = (spell_t*) malloc(sizeof(spell_t)); *result = *spell; // copy over all the static data members. + result->needsDataFreed = 0; result->sustain_node = NULL; result->magic_effects_node = NULL; result->elements.first = NULL; @@ -552,11 +700,55 @@ spell_t* copySpell(spell_t* spell) spellElement_t* newElement = (spellElement_t*)tempNode->element; newElement->node = tempNode; + + if ( subElementIndexToCopy >= 0 ) // only select 1 sub element + { + if ( list_Size(&newElement->elements) > subElementIndexToCopy ) + { + node_t* nextnode = nullptr; + int index = 0; + for ( auto node2 = newElement->elements.first; node2; node2 = nextnode ) + { + nextnode = node2->next; + if ( index != subElementIndexToCopy ) + { + list_RemoveNode(node2); + } + ++index; + } + } + } } return result; } +void copySpellElement(spellElement_t* spellElement, spellElement_t* spellElementToSet) +{ + if ( !spellElement || !spellElementToSet ) + { + return; + } + *spellElementToSet = *spellElement; // copy over all the static data members. + + spellElementToSet->node = NULL; + spellElementToSet->elements.first = NULL; + spellElementToSet->elements.last = NULL; + + for ( node_t* node = spellElement->elements.first; node != NULL; node = node->next ) + { + spellElement_t* tempElement = (spellElement_t*)node->element; + + node_t* tempNode = list_AddNodeLast(&spellElementToSet->elements); + tempNode->deconstructor = &spellElementDeconstructor; + tempNode->size = sizeof(spellElement_t); + tempNode->element = copySpellElement(tempElement); + + spellElement_t* newElement = (spellElement_t*)tempNode->element; + newElement->node = tempNode; + } +} + spellElement_t* copySpellElement(spellElement_t* spellElement) { node_t* node; @@ -584,7 +776,39 @@ spellElement_t* copySpellElement(spellElement_t* spellElement) return result; } -int getCostOfSpell(spell_t* spell, Entity* caster) +int getGoldCostOfSpell(spell_t* spell, int player) +{ + if ( player < 0 || player >= MAXPLAYERS ) { return 0; } + if ( !spell ) + { + return 0; + } + + int cost = 0; + if ( spell->ID == SPELL_LEAD_BOLT ) + { + cost = getSpellDamageSecondaryFromID(SPELL_LEAD_BOLT, players[player]->entity, stats[player], players[player]->entity); + int minCost = getSpellEffectDurationSecondaryFromID(SPELL_LEAD_BOLT, players[player]->entity, stats[player], players[player]->entity); + cost = std::max(minCost, cost); + } + else if ( spell->ID == SPELL_MERCURY_BOLT ) + { + cost = getSpellDamageSecondaryFromID(SPELL_MERCURY_BOLT, players[player]->entity, stats[player], players[player]->entity); + int minCost = getSpellEffectDurationSecondaryFromID(SPELL_MERCURY_BOLT, players[player]->entity, stats[player], players[player]->entity); + cost = std::max(minCost, cost); + } + else if ( spell->ID == SPELL_FORGE_MAGIC_SCRAP ) + { + cost = 400; + } + else if ( spell->ID == SPELL_FORGE_METAL_SCRAP ) + { + cost = 200; + } + return cost; +} + +int getSustainCostOfSpell(spell_t* spell, Entity* caster) { int cost = 0; if ( !spell ) @@ -596,14 +820,27 @@ int getCostOfSpell(spell_t* spell, Entity* caster) for ( node = spell->elements.first; node != NULL; node = node->next ) { spellElement_t* spellElement = (spellElement_t*)node->element; - cost += getCostOfSpellElement(spellElement); + cost += spellElement->channeledMana; } + return cost; +} - if ( spell->ID == SPELL_FORCEBOLT && caster && caster->skillCapstoneUnlockedEntity(PRO_SPELLCASTING) ) +int getCostOfSpell(spell_t* spell, Entity* caster) +{ + int cost = 0; + if ( !spell ) + { + return 0; + } + + cost = spell->mana; + + /*if ( spell->ID == SPELL_FORCEBOLT && caster && caster->skillCapstoneUnlockedEntity(PRO_LEGACY_SPELLCASTING) ) { cost = 0; } - else if ( spell->ID == SPELL_SUMMON && caster ) + else */ + if ( spell->ID == SPELL_SUMMON && caster ) { Stat* casterStats = caster->getStats(); if ( casterStats ) @@ -612,6 +849,18 @@ int getCostOfSpell(spell_t* spell, Entity* caster) cost += 5 * (summonLevel / 5); } } + else if ( spell->ID == SPELL_BREATHE_FIRE && caster ) + { + Stat* casterStats = caster->getStats(); + if ( casterStats ) + { + cost += (casterStats->MAXMP / 50); + if ( casterStats->MP < 0.4 * casterStats->MAXMP ) + { + cost *= 2; + } + } + } else if ( caster && (spell->ID == SPELL_TELEPORTATION || spell->ID == SPELL_TELEPULL) ) { if ( caster->creatureShadowTaggedThisUid != 0 && uidToEntity(caster->creatureShadowTaggedThisUid) ) @@ -623,20 +872,6 @@ int getCostOfSpell(spell_t* spell, Entity* caster) return cost; } -int getCostOfSpellElement(spellElement_t* spellElement) -{ - int cost = spellElement->mana; - - node_t* node; - for ( node = spellElement->elements.first; node != NULL; node = node->next ) - { - spellElement_t* spellElement2 = (spellElement_t*)node->element; - cost += getCostOfSpellElement(spellElement2); - } - - return cost; -} - bool spell_isChanneled(spell_t* spell) { node_t* node = NULL; @@ -653,14 +888,14 @@ bool spell_isChanneled(spell_t* spell) return false; } -real_t getSpellBonusFromCasterINT(Entity* caster, Stat* casterStats) +int getSpellbookBaseINTBonus(Entity* caster, Stat* casterStats, int skillID) { if ( caster && caster->behavior != &actPlayer ) { - return 0.0; + return 0; } - real_t bonus = 0.0; + int bonus = 0; if ( !casterStats && caster ) { casterStats = caster->getStats(); @@ -670,30 +905,140 @@ real_t getSpellBonusFromCasterINT(Entity* caster, Stat* casterStats) int INT = statGetINT(casterStats, caster); if ( INT > 0 ) { - bonus += INT / 100.0; + bonus += INT; + if ( skillID == PRO_SORCERY ) + { + int bonusStat = statGetINT(casterStats, caster); + if ( bonusStat > 0 ) + { + bonus += bonusStat; + } + } + } + if ( skillID == PRO_MYSTICISM ) + { + int bonusStat = statGetCHR(casterStats, caster); + if ( bonusStat > 0 ) + { + bonus += bonusStat; + } } + else if ( skillID == PRO_THAUMATURGY ) + { + int bonusStat = statGetCON(casterStats, caster); + if ( bonusStat > 0 ) + { + bonus += bonusStat; + } + } + bonus /= 2; } return bonus; } -real_t getBonusFromCasterOfSpellElement(Entity* caster, Stat* casterStats, spellElement_t* spellElement, int spellID) +real_t getSpellBonusFromCasterINT(Entity* caster, Stat* casterStats, int skillID) { if ( caster && caster->behavior != &actPlayer ) { return 0.0; } + real_t bonus = 0.0; + if ( !casterStats && caster ) + { + casterStats = caster->getStats(); + } + if ( casterStats ) + { + int INT = statGetINT(casterStats, caster); + if ( INT > 0 ) + { + bonus += INT / 100.0; + if ( skillID == PRO_SORCERY ) + { + int bonusStat = statGetINT(casterStats, caster); + if ( bonusStat > 0 ) + { + bonus += bonusStat / 100.0; + } + } + } + if ( skillID == PRO_MYSTICISM ) + { + int bonusStat = statGetCHR(casterStats, caster); + if ( bonusStat > 0 ) + { + bonus += bonusStat / 100.0; + } + } + else if ( skillID == PRO_THAUMATURGY ) + { + int bonusStat = statGetCON(casterStats, caster); + if ( bonusStat > 0 ) + { + bonus += bonusStat / 100.0; + } + } + if ( skillID == PRO_SORCERY + || skillID == PRO_MYSTICISM + || skillID == PRO_THAUMATURGY ) + { + real_t mult = 1.0; + if ( casterStats->getModifiedProficiency(skillID) >= SKILL_LEVEL_EXPERT ) + { + real_t ratio = (casterStats->getModifiedProficiency(skillID) - SKILL_LEVEL_EXPERT) / (real_t)40.0; + mult += (ratio * (60 / 100.0)); // 0.6 max + } + if ( casterStats->getModifiedProficiency(skillID) >= SKILL_LEVEL_LEGENDARY ) + { + mult += 0.4; + } + bonus *= mult; + } + } + + return bonus; +} + +real_t getBonusFromCasterOfSpellElement(Entity* caster, Stat* casterStats, spellElement_t* spellElement, int spellID, int proficiencyWhenNoSpell) +{ if ( !casterStats && caster ) { casterStats = caster->getStats(); } + if ( caster && caster->behavior != &actPlayer ) + { + if ( casterStats ) + { + if ( Uint8 effectStrength = casterStats->getEffectActive(EFF_INCOHERENCE) ) + { + real_t mult = std::min(0.9, 0.2 + (effectStrength - 1) * 0.1); + real_t bonus = -mult; + return std::max(-0.9, bonus); + } + } + return 0.0; + } + real_t bonus = 0.0; - bonus += getSpellBonusFromCasterINT(caster, casterStats); + int spellSkillID = SPELL_NONE; + if ( spellID == SPELL_NONE ) + { + bonus += getSpellBonusFromCasterINT(caster, casterStats, proficiencyWhenNoSpell); + } + else + { + if ( auto spell = getSpellFromID(spellID) ) + { + bonus += getSpellBonusFromCasterINT(caster, casterStats, spell->skillID); + spellSkillID = spell->skillID; + } + } if ( casterStats ) { - if ( casterStats->EFFECTS[EFF_PWR] ) + if ( casterStats->getEffectActive(EFF_PWR) ) { bonus += 0.25; int percentMP = static_cast(100.0 * (real_t)casterStats->MP / std::max(1, casterStats->MAXMP)); @@ -701,6 +1046,49 @@ real_t getBonusFromCasterOfSpellElement(Entity* caster, Stat* casterStats, spell percentMP = (100 - percentMP) / 10; bonus += 0.5 * percentMP / 10.0; } + if ( casterStats->type == DRYAD ) + { + if ( !casterStats->helmet && casterStats->getEffectActive(EFF_GROWTH) > 1 ) + { + bonus += (casterStats->getEffectActive(EFF_GROWTH) - 1) * 0.1; + if ( casterStats->getEffectActive(EFF_BASTION_ROOTS) ) + { + bonus += (casterStats->getEffectActive(EFF_GROWTH) - 1) * 0.1; + } + } + } + bonus += (casterStats->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_EFF_1)) / 100.0; + if ( casterStats->getEffectActive(EFF_COUNSEL) ) + { + if ( spellSkillID == PRO_SORCERY + || spellSkillID == PRO_MYSTICISM ) + { + bonus += 0.2 + (0.1 * std::max(0, ((int)(casterStats->getEffectActive(EFF_COUNSEL) & 0xF) - 1))); + } + } + if ( casterStats->getEffectActive(EFF_RATION_SOUR) ) + { + bonus += 0.25; + } + if ( casterStats->shield && itemTypeIsFoci(casterStats->shield->type) ) + { + if ( auto spell = getSpellFromID(getSpellIDFromFoci(casterStats->shield->type)) ) + { + if ( spell->skillID == spellSkillID ) + { + real_t shieldBonus = 0.0; + if ( casterStats->shield->beatitude >= 0 || shouldInvertEquipmentBeatitude(casterStats) ) + { + shieldBonus += (0.10 + (0.05 * std::min(10, abs(casterStats->shield->beatitude)))); + } + else + { + shieldBonus -= ((0.10 * std::min(10, abs(casterStats->shield->beatitude)))); + } + bonus += shieldBonus; + } + } + } if ( casterStats->helmet ) { if ( casterStats->helmet->type == HAT_MITER ) @@ -714,7 +1102,7 @@ real_t getBonusFromCasterOfSpellElement(Entity* caster, Stat* casterStats, spell { hatBonus -= ((0.10 * abs(casterStats->helmet->beatitude))); } - if ( spellID == SPELL_HEALING || spellID == SPELL_EXTRAHEALING ) + if ( spellSkillID == PRO_THAUMATURGY ) { hatBonus *= 2; } @@ -731,17 +1119,23 @@ real_t getBonusFromCasterOfSpellElement(Entity* caster, Stat* casterStats, spell { hatBonus -= ((0.10 * abs(casterStats->helmet->beatitude))); } - if ( ItemTooltips.spellItems.find(spellID) != ItemTooltips.spellItems.end() ) + if ( spellSkillID == PRO_SORCERY ) + { + hatBonus *= 2; + } + /*if ( ItemTooltips.spellItems.find(spellID) != ItemTooltips.spellItems.end() ) { auto& entry = ItemTooltips.spellItems[spellID]; if ( entry.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_DAMAGE) != entry.spellTags.end() ) { hatBonus *= 2; } - } + }*/ bonus += hatBonus; } else if ( casterStats->helmet->type == HAT_CIRCLET + || casterStats->helmet->type == HAT_CIRCLET_SORCERY + || casterStats->helmet->type == HAT_CIRCLET_THAUMATURGY || casterStats->helmet->type == HAT_CIRCLET_WISDOM ) { if ( casterStats->helmet->beatitude >= 0 || shouldInvertEquipmentBeatitude(casterStats) ) @@ -754,25 +1148,50 @@ real_t getBonusFromCasterOfSpellElement(Entity* caster, Stat* casterStats, spell } } } + /*if ( Uint8 effectStrength = casterStats->getEffectActive(EFF_OVERCHARGE) ) + { + bonus += 0.25 * effectStrength; + }*/ + + if ( casterStats->type == SALAMANDER ) + { + if ( Uint8 effectStrength = casterStats->getEffectActive(EFF_SALAMANDER_HEART) ) + { + if ( effectStrength == 1 || effectStrength == 2 ) + { + real_t ratio = (statGetCHR(casterStats, caster) * 2 + 10) / 100.0; + bonus += 1.0 * (ratio); + } + } + } + + if ( Uint8 effectStrength = casterStats->getEffectActive(EFF_INCOHERENCE) ) + { + real_t mult = std::min(0.9, 0.2 + (effectStrength - 1) * 0.1); + bonus = -mult; + } } + - return bonus; + return std::max(-0.9, bonus); } bool spellElement_isChanneled(spellElement_t* spellElement) { node_t* node = NULL; - if ( spellElement->channeled ) + if ( spellElement->channeledMana > 0 && !spellElement->fociSpell ) { return true; } for ( node = spellElement->elements.first; node != NULL; node = node->next ) { - spellElement_t* tempElement = (spellElement_t*)node->element; - if ( spellElement_isChanneled(tempElement) ) + if ( spellElement_t* tempElement = (spellElement_t*)node->element ) { - return true; + if ( spellElement_isChanneled(tempElement) ) + { + return true; + } } } @@ -789,11 +1208,47 @@ void equipSpell(spell_t* spell, int playernum, Item* spellItem) } } -const char* spell_t::getSpellName() +const char* spell_t::getSpellName(bool lowercase) { if ( ItemTooltips.spellItems.find(ID) != ItemTooltips.spellItems.end() ) { - return ItemTooltips.spellItems[ID].name.c_str(); + if ( lowercase ) + { + return ItemTooltips.spellItems[ID].name_lowercase.c_str(); + } + else + { + return ItemTooltips.spellItems[ID].name.c_str(); + } + } + return ""; +} + +const char* spell_t::getSpellTierName() +{ + if ( difficulty < 20 ) + { + return "I"; + } + else if ( difficulty >= 20 && difficulty < 40 ) + { + return "II"; + } + else if ( difficulty >= 40 && difficulty < 60 ) + { + return "III"; + } + else if ( difficulty >= 60 && difficulty < 80 ) + { + return "IV"; + } + else if ( difficulty >= 80 && difficulty < 100 ) + { + return "V"; + } + else if ( difficulty >= 100 ) + { + return "VI"; } return ""; } @@ -802,6 +1257,15 @@ spell_t* getSpellFromID(int ID) { spell_t* spell = nullptr; + auto find = allGameSpells.find(ID); + assert(ID != SPELL_NONE); + assert(find != allGameSpells.end()); + if ( find != allGameSpells.end() ) + { + return find->second; + } + + switch (ID) { case SPELL_FORCEBOLT: @@ -997,6 +1461,17 @@ spell_t* getSpellFromID(int ID) int getSpellbookFromSpellID(int spellID) { ItemType itemType = WOODEN_SHIELD; + + auto find = ItemTooltips.spellItems.find(spellID); + if ( find != ItemTooltips.spellItems.end() ) + { + if ( find->second.spellbookId >= 0 && find->second.spellbookId < NUMITEMS ) + { + itemType = (ItemType)find->second.spellbookId; + return itemType; + } + } + switch (spellID) { case SPELL_FORCEBOLT: @@ -1171,8 +1646,36 @@ int getSpellbookFromSpellID(int spellID) return itemType; } +int getSpellIDFromFoci(int fociType) +{ + if ( fociType >= 0 && fociType < NUMITEMS ) + { + if ( items[fociType].hasAttribute("foci_spell") ) + { + auto& spellID = items[fociType].attributes["foci_spell"]; + if ( spellID >= SPELL_NONE && spellID < NUM_SPELLS ) + { + return spellID; + } + } + } + return SPELL_NONE; +} + int getSpellIDFromSpellbook(int spellbookType) { + if ( spellbookType >= 0 && spellbookType < NUMITEMS ) + { + if ( items[spellbookType].hasAttribute("spellbook_spell") ) + { + auto& spellID = items[spellbookType].attributes["spellbook_spell"]; + if ( spellID >= SPELL_NONE && spellID < NUM_SPELLS ) + { + return spellID; + } + } + } + switch (spellbookType ) { case SPELLBOOK_FORCEBOLT: @@ -1212,7 +1715,7 @@ int getSpellIDFromSpellbook(int spellbookType) case SPELLBOOK_HEALING: return spell_healing.ID; case SPELLBOOK_EXTRAHEALING: - return spell_extrahealing.ID; + return SPELL_EXTRAHEALING; case SPELLBOOK_CUREAILMENT: return spell_cureailment.ID; case SPELLBOOK_DIG: @@ -1308,15 +1811,31 @@ bool spellInList(list_t* list, spell_t* spell) return false; } -void spell_changeHealth(Entity* entity, int amount, bool overdrewFromHP) +void spell_changeHealth(Entity* entity, int amount, bool overdrewFromHP, bool doMessage) { if (!entity) { return; } + int baseAmount = amount; + if ( amount > 0 ) + { + real_t healMult = entity->getHealingSpellPotionModifierFromEffects(false); + amount *= healMult; + } + Stat* entitystats = entity->getStats(); + Sint32 prevHP = entitystats ? entity->getStats()->HP : 0; entity->modHP(amount); + if ( entitystats && entitystats->HP > prevHP ) + { + if ( baseAmount < amount ) + { + entity->getHealingSpellPotionModifierFromEffects(true); + } + } + int player = -1; int i = 0; for (i = 0; i < MAXPLAYERS; ++i) @@ -1329,33 +1848,36 @@ void spell_changeHealth(Entity* entity, int amount, bool overdrewFromHP) if (player > -1 && player < MAXPLAYERS) { - if (amount > 0) - { - if ( overdrewFromHP ) - { - Uint32 color = makeColorRGB(255, 255, 255); - messagePlayerColor(player, MESSAGE_STATUS, color, Language::get(3400)); - } - else - { - Uint32 color = makeColorRGB(0, 255, 0); - messagePlayerColor(player, MESSAGE_STATUS, color, Language::get(443)); - } - } - else + if ( doMessage ) { - Uint32 color = makeColorRGB(255, 255, 0); - if (amount == 0) + if (amount > 0) { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(444)); + if ( overdrewFromHP ) + { + Uint32 color = makeColorRGB(255, 255, 255); + messagePlayerColor(player, MESSAGE_STATUS, color, Language::get(3400)); + } + else + { + Uint32 color = makeColorRGB(0, 255, 0); + messagePlayerColor(player, MESSAGE_STATUS, color, Language::get(443)); + } } else { - messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(445)); + Uint32 color = makeColorRGB(255, 255, 0); + if (amount == 0) + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(444)); + } + else + { + messagePlayerColor(player, MESSAGE_COMBAT, color, Language::get(445)); + } } } - if ( multiplayer == SERVER && player > 0 && !players[player]->isLocalPlayer() ) + /*if ( multiplayer == SERVER && player > 0 && !players[player]->isLocalPlayer() ) { strcpy((char*)net_packet->data, "UPHP"); SDLNet_Write32((Uint32)stats[player]->HP, &net_packet->data[4]); @@ -1364,7 +1886,7 @@ void spell_changeHealth(Entity* entity, int amount, bool overdrewFromHP) net_packet->address.port = net_clients[player - 1].port; net_packet->len = 12; sendPacketSafe(net_sock, net_packet->channel, net_packet, player - 1); - } + }*/ } } @@ -1408,9 +1930,9 @@ spell_t* getSpellFromItem(const int player, Item* item, bool usePlayerInventory) { for ( auto spell : allGameSpells ) { - if ( spell->ID == appearance ) + if ( spell.second->ID == appearance ) { - return spell; //Found the spell. + return spell.second; //Found the spell. } } } @@ -1436,7 +1958,7 @@ int canUseShapeshiftSpellInCurrentForm(const int player, Item& item) { return -1; } - if ( !stats[player]->EFFECTS[EFF_SHAPESHIFT] ) + if ( !stats[player]->getEffectActive(EFF_SHAPESHIFT) ) { return 0; } diff --git a/src/main.cpp b/src/main.cpp index b88fdda09..33643ef07 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -298,7 +298,22 @@ SteamStat_t g_SteamStats[NUM_STEAM_STATISTICS] = { 55, STEAM_STAT_INT, "STAT_DUNGEONSEED" }, { 56, STEAM_STAT_INT, "STAT_PITCH_PERFECT" }, { 57, STEAM_STAT_INT, "STAT_RUNG_OUT" }, - { 58, STEAM_STAT_INT, "STAT_SMASH_MELEE" } + { 58, STEAM_STAT_INT, "STAT_SMASH_MELEE" }, + { 59, STEAM_STAT_INT, "STAT_CALL_LOCKSMITH" }, + { 60, STEAM_STAT_INT, "STAT_PREMIUM_LOOTBOX" }, + { 61, STEAM_STAT_INT, "STAT_WITCHES_BREW" }, + { 62, STEAM_STAT_INT, "STAT_HOBBYIST" }, + { 63, STEAM_STAT_INT, "STAT_BLESSED_ADDITION" }, + { 64, STEAM_STAT_INT, "STAT_THATS_A_WRAP" }, + { 65, STEAM_STAT_INT, "STAT_LET_HIM_COOK" }, + { 66, STEAM_STAT_INT, "STAT_TOUCHE" }, + { 67, STEAM_STAT_INT, "STAT_MERCENARY_ARMY" }, + { 68, STEAM_STAT_INT, "STAT_COLONIST" }, + { 69, STEAM_STAT_INT, "STAT_PRICKLY_PERSONALITY" }, + { 70, STEAM_STAT_INT, "STAT_BOOM_DYNAMITE" }, + { 71, STEAM_STAT_INT, "STAT_PAY_TO_WIN" }, + { 72, STEAM_STAT_INT, "STAT_DOESNT_COUNT" }, + { 73, STEAM_STAT_INT, "STAT_SOURCE_ENGINE" } }; #ifdef STEAMWORKS @@ -329,7 +344,6 @@ int fullscreen = 0; bool borderless = false; bool smoothlighting = true; list_t removedEntities; -list_t entitiesToDelete[MAXPLAYERS]; Entity* client_selected[MAXPLAYERS] = {nullptr, nullptr, nullptr, nullptr}; bool inrange[MAXPLAYERS]; Sint32 client_classes[MAXPLAYERS]; @@ -484,6 +498,8 @@ real_t sfxEnvironmentVolume = 1.0; real_t sfxNotificationVolume = 1.0; real_t musvolume = 1.0; bool musicPreload = false; +bool instrument_bg_enabled = true; +bool instrument_fg_enabled = true; // fun stuff SDL_Surface* title_bmp = nullptr; diff --git a/src/main.hpp b/src/main.hpp index df08ab28f..affb85fd3 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -10,6 +10,15 @@ -------------------------------------------------------------------------------*/ #pragma once +#include "Config.hpp" + +#include +//#ifdef WINDOWS +//#ifdef _DEBUG +//#define _CRTDBG_MAP_ALLOC +//#include +//#endif +//#endif #ifdef __arm__ typedef float real_t; @@ -50,12 +59,12 @@ char* stringStr(char* str1, const char* str2, size_t str1_size, size_t str2_size //using namespace std; //For C++ strings //This breaks messages on certain systems, due to template class std::__cxx11::messages using std::string; //Instead of including an entire namespace, please explicitly include only the parts you need, and check for conflicts as reasonably possible. #include +#include #include #include #include #include #include "physfs.h" -#include "Config.hpp" #ifdef NINTENDO #include "nintendo/baronynx.hpp" @@ -91,7 +100,6 @@ extern bool logCheckMainLoopTimers; extern bool autoLimbReload; #include -#include #include #include #include @@ -463,6 +471,14 @@ typedef struct map_t bool* monsterexcludelocations = nullptr; bool* lootexcludelocations = nullptr; std::set liquidSfxPlayedTiles; + std::map tileAttributes; + static const Uint32 TILE_ATTRIBUTE_NODIG = 1 << 0; + static const Uint32 TILE_ATTRIBUTE_SLIPPERY = 1 << 1; + static const Uint32 TILE_ATTRIBUTE_SLOW = 1 << 2; + static const Uint32 TILE_ATTRIBUTE_GREASE = 1 << 3; + static const Uint32 TILE_ATTRIBUTE_TREASURE_ROOM = 1 << 4; + bool tileHasAttribute(int x, int y, int layer, Uint32 attribute); + void setMapHDRSettings(); char filename[256]; ~map_t() { @@ -814,6 +830,8 @@ extern real_t sfxvolume; extern real_t sfxAmbientVolume; extern real_t sfxEnvironmentVolume; extern real_t sfxNotificationVolume; +extern bool instrument_bg_enabled; +extern bool instrument_fg_enabled; extern bool musicPreload; extern bool *animatedtiles, *swimmingtiles, *lavatiles; extern char tempstr[1024]; @@ -890,7 +908,7 @@ extern bool initialized; //So that messagePlayer doesn't explode before the game void GO_SwapBuffers(SDL_Window* screen); -static const int NUM_STEAM_STATISTICS = 58; +static const int NUM_STEAM_STATISTICS = 73; extern SteamStat_t g_SteamStats[NUM_STEAM_STATISTICS]; #ifdef STEAMWORKS diff --git a/src/maps.cpp b/src/maps.cpp index 406369bea..b6c77d4db 100644 --- a/src/maps.cpp +++ b/src/maps.cpp @@ -34,6 +34,206 @@ BaronyRNG map_rng; BaronyRNG map_server_rng; int numChests = 0; int numMimics = 0; +TreasureRoomGenerator treasure_room_generator; +void TreasureRoomGenerator::init() +{ + treasure_floors.clear(); + treasure_secret_floors.clear(); + station_floors.clear(); + station_secret_floors.clear(); + Uint32 seed = uniqueGameKey; + if ( seed < (0xFFFFFFFF - 64) ) + { + seed += 64; + } + else + { + seed -= 64; + } + treasure_rng.seedBytes(&seed, sizeof(seed)); + + std::string previous_station[2] = { "", "" }; + for ( int i = 0; i <= 35; i += 5 ) + { + for ( int j = 0; j < 2; ++j ) + { + { + auto& floors = (j == 0) ? treasure_floors : treasure_secret_floors; + std::vector chances = { 0, 10, 7, 7, 10 }; + if ( i == 0 && j == 0 ) + { + chances[0] = 0; + chances[1] = 0; + } + if ( j == 1 && i == 1 ) + { + chances = { 0, 10, 7, 0, 0 }; // underworld + } + + unsigned int res1 = treasure_rng.discrete(chances.data(), chances.size()); + chances[res1] = 0; + unsigned int res2 = treasure_rng.discrete(chances.data(), chances.size()); + + //if ( treasure_rng.rand() % 3 == 0 ) + { + // do both + floors.insert(i + res1); + floors.insert(i + res2); + } + //else + //{ + // // only 1 + // auto chosen = treasure_rng.rand() % 2 == 0 ? res1 : res2; + // floors.insert(i + chosen); + //} + } + + { + auto& floors_stations = (j == 0) ? station_floors : station_secret_floors; + std::vector chances = { 0, 7, 10, 10, 10 }; + if ( i == 0 && j == 0 ) + { + chances[0] = 0; + chances[1] = 0; + } + if ( j == 1 && i == 5 ) + { + chances = { 0, 7, 10, 0, 0 }; // underworld + } + unsigned int res1 = treasure_rng.discrete(chances.data(), chances.size()); + chances[res1] = 0; + unsigned int res2 = treasure_rng.discrete(chances.data(), chances.size()); + + if ( true /*(treasure_rng.rand() % 4 > 0) || (i >= 5 && i <= 10) || i >= 15*/ ) + { + std::vector chances_level; + chances_level.push_back(1); + chances_level.push_back(1); + auto chosen_level = treasure_rng.discrete(chances_level.data(), chances_level.size()); + chances_level[chosen_level] = 0; + + std::vector strs = { "cauldron", "workbench" }; + + int numStations = 1; + + if ( abs((int)res1) - abs((int)res2) >= 1 ) // x level distance + { + if ( treasure_rng.rand() % 3 == 0 ) + { + numStations = 2; + } + } + + while ( numStations > 0 ) + { + --numStations; + + std::vector chances_strs; + for ( auto& str : strs ) + { + if ( j == 1 && (i == 5 || i == 20) ) // underworld/hell + { + if ( str == "cauldron" ) + { + chances_strs.push_back(1); + } + else + { + chances_strs.push_back(0); + } + } + else if ( str != previous_station[j] ) + { + chances_strs.push_back(1); + } + else + { + chances_strs.push_back(0); + } + } + unsigned int station_name_pick = treasure_rng.discrete(chances_strs.data(), chances_strs.size()); + floors_stations[i + (chosen_level == 0 ? res1 : res2)] = strs[station_name_pick]; + previous_station[j] = strs[station_name_pick]; + + bool anyChances = false; + for ( auto val : chances_level ) + { + if ( val > 0 ) + { + anyChances = true; + break; + } + } + + if ( !anyChances ) + { + break; + } + + chosen_level = treasure_rng.discrete(chances_level.data(), chances_level.size()); + chances_level[chosen_level] = 0; + } + } + } + } + } + + orb_floors.clear(); + orb_floors[8] = "orb_green"; + orb_floors[13] = "orb_red"; + orb_floors[18] = "orb_blue"; +} + +bool TreasureRoomGenerator::bForceStationSpawnForCurrentFloor(int secretlevelexit) +{ + auto& floor = secretlevel ? station_secret_floors : station_floors; + + return floor.find(currentlevel) != floor.end(); +} + +bool TreasureRoomGenerator::bForceSpawnForCurrentFloor(int secretlevelexit, bool minotaur, BaronyRNG& mapRNG) +{ + static ConsoleVariable cvar_treasure_room_test("/treasure_room_test", false); + if ( *cvar_treasure_room_test && (svFlags & SV_FLAG_CHEATS) ) + { + return true; + } + + auto& floor = secretlevel ? treasure_secret_floors : treasure_floors; + bool pushBackSpawn = false; + + if ( secretlevelexit && mapRNG.rand() % 100 < 50 ) + { + pushBackSpawn = true; + } + else if ( minotaur ) + { + pushBackSpawn = mapRNG.rand() % 100 < 75; + } + else + { + return floor.find(currentlevel) != floor.end(); + } + + if ( pushBackSpawn ) + { + // push entry back as it conflicts with secret exit or minotaur + floor.erase(currentlevel); + if ( floor.find(currentlevel + 1) != floor.end() ) + { + floor.insert(currentlevel + 2); + } + else + { + floor.insert(currentlevel + 1); + } + } + else + { + return floor.find(currentlevel) != floor.end(); + } + return false; +} Sint32 doorFrameSprite() { if (stringStr(map.name, "Caves", sizeof(map_t::name), 5)) { @@ -69,6 +269,12 @@ Sint32 doorFrameSprite() { if (stringStr(map.name, "Underworld", sizeof(map_t::name), 10)) { return 1169; } + if ( !strncmp(map.filename, "fortress", 8) ) { + return 1631; + } + if ( !strncmp(map.filename, "keep", 4) ) { + return 1699; + } return 1; // default door frame } @@ -372,6 +578,20 @@ int monsterCurve(int level) return COCKATRICE; } } + else if ( !strncmp(map.filename, "fortress", 8) ) + { + switch ( map_rng.rand() % 4 ) + { + case 0: + return DRYAD; + case 1: + return MYCONID; + case 2: + return GREMLIN; + case 3: + return MOTH_SMALL; + } + } return SKELETON; // basic monster } @@ -633,7 +853,7 @@ struct StartRoomInfo_t if ( exitPoints.empty() ) { printlog("[MAP GENERATOR]: Start map does not have accessibility to any other areas!"); - for ( auto point : goodTunnelPoints ) + for ( auto& point : goodTunnelPoints ) { std::string dir = ""; switch ( point.second ) @@ -655,7 +875,7 @@ struct StartRoomInfo_t } printlog("[MAP GENERATOR]: TunnelPoints1 %s: (x: %d y: %d)", dir.c_str(), point.first.first, point.first.second); } - for ( auto point : badTunnelPoints ) + for ( auto& point : badTunnelPoints ) { std::string dir = ""; switch ( point.second ) @@ -677,7 +897,7 @@ struct StartRoomInfo_t } printlog("[MAP GENERATOR]: TunnelPoints2 %s: (x: %d y: %d)", dir.c_str(), point.first.first, point.first.second); } - for ( auto point : worstTunnelPoints ) + for ( auto& point : worstTunnelPoints ) { std::string dir = ""; switch ( point.second ) @@ -701,7 +921,7 @@ struct StartRoomInfo_t } if ( !goodTunnelPoints.empty() ) { - auto picked = goodTunnelPoints.at(map_rng.rand() % goodTunnelPoints.size()); + auto& picked = goodTunnelPoints.at(map_rng.rand() % goodTunnelPoints.size()); switch ( picked.second ) { case WEST: @@ -724,7 +944,7 @@ struct StartRoomInfo_t } else if ( !badTunnelPoints.empty() ) { - auto picked = badTunnelPoints.at(map_rng.rand() % badTunnelPoints.size()); + auto& picked = badTunnelPoints.at(map_rng.rand() % badTunnelPoints.size()); switch ( picked.second ) { case WEST: @@ -747,7 +967,7 @@ struct StartRoomInfo_t } else if ( !worstTunnelPoints.empty() ) { - auto picked = worstTunnelPoints.at(map_rng.rand() % worstTunnelPoints.size()); + auto& picked = worstTunnelPoints.at(map_rng.rand() % worstTunnelPoints.size()); switch ( picked.second ) { case WEST: @@ -788,6 +1008,10 @@ bool mapSpriteIsDoorway(int sprite) case 114: return true; break; + case 217: + case 218: + return true; + break; default: break; } @@ -825,7 +1049,7 @@ bool mapTileDiggable(const int x, const int y) { return false; } - if ( !strncmp(map.name, "Hell", 4) ) + if ( !strncmp(map.name, "Hell", 4) || (!strncmp(map.filename, "fortress", 8)) ) { if ( x < getMapPossibleLocationX1() || x >= getMapPossibleLocationX2() || y < getMapPossibleLocationY1() || y >= getMapPossibleLocationY2() ) @@ -833,6 +1057,138 @@ bool mapTileDiggable(const int x, const int y) return false; } } + + if ( map.tileHasAttribute(x, y, OBSTACLELAYER, map_t::TILE_ATTRIBUTE_NODIG) ) + { + return false; + } + return true; +} + +bool loadSubRoomData(std::string fullMapPath, list_t* mapList) +{ + // allocate memory for the next subroom and attempt to load it + map_t* subRoomMap = (map_t*)malloc(sizeof(map_t)); + subRoomMap->tiles = nullptr; + subRoomMap->entities = (list_t*)malloc(sizeof(list_t)); + subRoomMap->entities->first = nullptr; + subRoomMap->entities->last = nullptr; + subRoomMap->creatures = new list_t; + subRoomMap->creatures->first = nullptr; + subRoomMap->creatures->last = nullptr; + subRoomMap->worldUI = nullptr; + subRoomMap->trapexcludelocations = nullptr; + subRoomMap->monsterexcludelocations = nullptr; + subRoomMap->lootexcludelocations = nullptr; + int checkMapHash = -1; + if ( fullMapPath.empty() || loadMap(fullMapPath.c_str(), subRoomMap, subRoomMap->entities, subRoomMap->creatures, &checkMapHash) == -1 ) + { + mapDeconstructor((void*)subRoomMap); + return false; // failed to load level + } + if ( !verifyMapHash(fullMapPath.c_str(), checkMapHash) ) + { + conductGameChallenges[CONDUCT_MODDED] = 1; + Mods::disableSteamAchievements = true; + } + + // level is successfully loaded, add it to the pool + list_t* subRoomList = (list_t*)malloc(sizeof(list_t)); + subRoomList->first = nullptr; + subRoomList->last = nullptr; + + node_t* node = list_AddNodeLast(mapList); + node->element = subRoomList; + node->deconstructor = &listDeconstructor; + + node = list_AddNodeLast(subRoomList); + node->element = subRoomMap; + node->deconstructor = &mapDeconstructor; + + // more nodes are created to record the exit points on the sublevel + for ( int y = 0; y < subRoomMap->height; y++ ) + { + for ( int x = 0; x < subRoomMap->width; x++ ) + { + if ( x == 0 || y == 0 || x == subRoomMap->width - 1 || y == subRoomMap->height - 1 ) + { + if ( !subRoomMap->tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * subRoomMap->height] ) + { + door_t* door = (door_t*)malloc(sizeof(door_t)); + door->x = x; + door->y = y; + if ( x == subRoomMap->width - 1 ) + { + door->dir = door_t::DIR_EAST; + if ( y == subRoomMap->height - 1 ) + { + door->edge = door_t::EDGE_SOUTHEAST; + } + else if ( y == 0 ) + { + door->edge = door_t::EDGE_NORTHEAST; + } + else + { + door->edge = door_t::EDGE_EAST; + } + } + else if ( y == subRoomMap->height - 1 ) + { + door->dir = door_t::DIR_SOUTH; + if ( x == subRoomMap->width - 1 ) + { + door->edge = door_t::EDGE_SOUTHEAST; + } + else if ( x == 0 ) + { + door->edge = door_t::EDGE_SOUTHWEST; + } + else + { + door->edge = door_t::EDGE_SOUTH; + } + } + else if ( x == 0 ) + { + door->dir = door_t::DIR_WEST; + if ( y == subRoomMap->height - 1 ) + { + door->edge = door_t::EDGE_SOUTHWEST; + } + else if ( y == 0 ) + { + door->edge = door_t::EDGE_NORTHWEST; + } + else + { + door->edge = door_t::EDGE_WEST; + } + } + else if ( y == 0 ) + { + door->dir = door_t::DIR_NORTH; + if ( x == subRoomMap->width - 1 ) + { + door->edge = door_t::EDGE_NORTHEAST; + } + else if ( x == 0 ) + { + door->edge = door_t::EDGE_NORTHWEST; + } + else + { + door->edge = door_t::EDGE_NORTH; + } + } + node_t* node2 = list_AddNodeLast(subRoomList); + node2->element = door; + node2->deconstructor = &defaultDeconstructor; + } + } + } + } + return true; } @@ -847,25 +1203,19 @@ bool mapTileDiggable(const int x, const int y) int generateDungeon(char* levelset, Uint32 seed, std::tuple mapParameters) { - char* sublevelname, *subRoomName; char sublevelnum[3]; - map_t* tempMap = nullptr; - map_t* subRoomMap = nullptr; - list_t mapList, *newList, *subRoomList, subRoomMapList; - node_t* node, *node2, *node3, *nextnode, *subRoomNode; + list_t mapList, subRoomMapList; + node_t* node, *node2, *node3, *nextnode; Sint32 c, i, j; - Sint32 numlevels, levelnum, levelnum2; - Sint32 x, y, z; - Sint32 x0, y0, x1, y1; + Sint32 numlevels = 0; + //Sint32 x, y, z; door_t* door, *newDoor; - bool* possiblelocations, *possiblelocations2, *possiblerooms; - bool* firstroomtile; + bool* possiblelocations, *possiblelocations2; + bool* firstroomtile, *secretlevelexittile; Sint32 numpossiblelocations, pickedlocation, subroomPickRoom; Entity* entity, *entity2, *childEntity; Uint32 levellimit; list_t doorList; - node_t* doorNode = nullptr; - node_t* subRoomDoorNode = nullptr; bool shoplevel = false; map_t shopmap; map_t secretlevelmap; @@ -925,19 +1275,21 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple Mods::disableSteamAchievements = true; } - std::string fullMapPath; - fullMapPath = physfsFormatMapName(levelset); - int checkMapHash = -1; - if ( fullMapPath.empty() || loadMap(fullMapPath.c_str(), &map, map.entities, map.creatures, &checkMapHash) == -1 ) - { - printlog("error: no level of set '%s' could be found.\n", levelset); - return -1; - } - if ( !verifyMapHash(fullMapPath.c_str(), checkMapHash) ) { - conductGameChallenges[CONDUCT_MODDED] = 1; - Mods::disableSteamAchievements = true; + std::string fullMapPath; + fullMapPath = physfsFormatMapName(levelset); + + if ( fullMapPath.empty() || loadMap(fullMapPath.c_str(), &map, map.entities, map.creatures, &checkMapHash) == -1 ) + { + printlog("error: no level of set '%s' could be found.\n", levelset); + return -1; + } + if ( !verifyMapHash(fullMapPath.c_str(), checkMapHash) ) + { + conductGameChallenges[CONDUCT_MODDED] = 1; + Mods::disableSteamAchievements = true; + } } // store this map's seed @@ -953,7 +1305,9 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple { // function sets shop level for us. } - else if ( map_rng.rand() % 2 && currentlevel > 1 && strncmp(map.name, "Underworld", 10) && strncmp(map.name, "Hell", 4) ) + else if ( map_rng.rand() % 2 && currentlevel > 1 + && strncmp(map.name, "Underworld", 10) && strncmp(map.name, "Hell", 4) + && strncmp(map.filename, "fortress", 8) ) { shoplevel = true; } @@ -979,13 +1333,25 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } } + if ( !strncmp(map.filename, "fortress", 8) ) + { + minotaurlevel = false; + } + // dark level if ( gameplayCustomManager.processedDarkFloor(currentlevel, secretlevel, map.name) ) { // function sets dark level for us. if ( darkmap ) { - messageLocalPlayers(MESSAGE_HINT, Language::get(1108)); + if ( !strncmp(map.filename, "fortress", 8) ) + { + messageLocalPlayers(MESSAGE_HINT, Language::get(6755)); + } + else + { + messageLocalPlayers(MESSAGE_HINT, Language::get(1108)); + } } } else if ( !secretlevel ) @@ -995,7 +1361,14 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple if ( map_rng.rand() % 100 < std::get(mapParameters) ) { darkmap = true; - messageLocalPlayers(MESSAGE_HINT, Language::get(1108)); + if ( !strncmp(map.filename, "fortress", 8) ) + { + messageLocalPlayers(MESSAGE_HINT, Language::get(6755)); + } + else + { + messageLocalPlayers(MESSAGE_HINT, Language::get(1108)); + } } else { @@ -1004,10 +1377,21 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } else if ( currentlevel % LENGTH_OF_LEVEL_REGION >= 2 ) { - if ( map_rng.rand() % 4 == 0 ) + if ( !strncmp(map.filename, "fortress", 8) ) { - darkmap = true; - messageLocalPlayers(MESSAGE_HINT, Language::get(1108)); + if ( map_rng.rand() % 4 == 0 ) + { + darkmap = true; + messageLocalPlayers(MESSAGE_HINT, Language::get(6755)); + } + } + else + { + if ( map_rng.rand() % 4 == 0 ) + { + darkmap = true; + messageLocalPlayers(MESSAGE_HINT, Language::get(1108)); + } } } } @@ -1062,24 +1446,36 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple doorList.first = nullptr; doorList.last = nullptr; - struct ShopSubRooms_t + struct GroupSubRooms_t { - std::string shopFileName = ""; + std::string rootMapFileName = ""; int count = 0; + std::vector possibleRooms; list_t list; - - ShopSubRooms_t() + std::map innerSubRooms; + GroupSubRooms_t() { list.first = nullptr; list.last = nullptr; } }; - ShopSubRooms_t shopSubRooms; + GroupSubRooms_t shopSubRooms; + enum TreasureRoomTypes : int + { + TREASURE_TYPE_BRONZE, + TREASURE_TYPE_IRON, + TREASURE_TYPE_GOLD, + TREASURE_TYPE_SILVER, + TREASURE_ROOM_MAX + }; + GroupSubRooms_t treasureRooms[TREASURE_ROOM_MAX]; + GroupSubRooms_t specialMapRooms; + GroupSubRooms_t* treasureRoomLevel = nullptr; // load shop room if ( shoplevel ) { - sublevelname = (char*) malloc(sizeof(char) * 128); + char sublevelname[128] = ""; std::string shopMapTitle = "shop"; if ( MFLAG_GENADJACENTROOMS ) { @@ -1091,7 +1487,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple snprintf(sublevelnum, 3, "%02d", numlevels); strcat(sublevelname, sublevelnum); - fullMapPath = physfsFormatMapName(sublevelname); + std::string fullMapPath = physfsFormatMapName(sublevelname); if ( fullMapPath.empty() ) { @@ -1104,8 +1500,8 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple strcpy(sublevelname, shopMapTitle.c_str()); snprintf(sublevelnum, 3, "%02d", shopleveltouse); strcat(sublevelname, sublevelnum); - shopSubRooms.shopFileName = sublevelname; - fullMapPath = physfsFormatMapName(sublevelname); + shopSubRooms.rootMapFileName = sublevelname; + std::string fullMapPath = physfsFormatMapName(sublevelname); shopmap.tiles = nullptr; shopmap.entities = (list_t*) malloc(sizeof(list_t)); @@ -1136,459 +1532,294 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple { shoplevel = false; } - free( sublevelname ); } - sublevelname = (char*)malloc(sizeof(char) * 128); - // a maximum of 100 (0-99 inclusive) sublevels can be added to the pool for ( numlevels = 0; numlevels < 100; ++numlevels ) { + char sublevelname[128] = ""; strcpy(sublevelname, levelset); snprintf(sublevelnum, 3, "%02d", numlevels); strcat(sublevelname, sublevelnum); - fullMapPath = physfsFormatMapName(sublevelname); + std::string fullMapPath = physfsFormatMapName(sublevelname); if ( fullMapPath.empty() ) { break; // no more levels to load } - // allocate memory for the next sublevel and attempt to load it - tempMap = (map_t*) malloc(sizeof(map_t)); - tempMap->tiles = nullptr; - tempMap->entities = (list_t*) malloc(sizeof(list_t)); - tempMap->entities->first = nullptr; - tempMap->entities->last = nullptr; - tempMap->creatures = new list_t; - tempMap->creatures->first = nullptr; - tempMap->creatures->last = nullptr; - tempMap->worldUI = nullptr; - tempMap->trapexcludelocations = nullptr; - tempMap->monsterexcludelocations = nullptr; - tempMap->lootexcludelocations = nullptr; - if ( fullMapPath.empty() || loadMap(fullMapPath.c_str(), tempMap, tempMap->entities, tempMap->creatures, &checkMapHash) == -1 ) - { - mapDeconstructor((void*)tempMap); - continue; // failed to load level - } - if (!verifyMapHash(fullMapPath.c_str(), checkMapHash)) - { - conductGameChallenges[CONDUCT_MODDED] = 1; - Mods::disableSteamAchievements = true; - } - - // level is successfully loaded, add it to the pool - newList = (list_t*) malloc(sizeof(list_t)); - newList->first = nullptr; - newList->last = nullptr; - node = list_AddNodeLast(&mapList); - node->element = newList; - node->deconstructor = &listDeconstructor; - - node = list_AddNodeLast(newList); - node->element = tempMap; - node->deconstructor = &mapDeconstructor; + loadSubRoomData(fullMapPath, &mapList); + } - // more nodes are created to record the exit points on the sublevel - for ( y = 0; y < tempMap->height; y++ ) + if ( !secretlevel ) + { + if ( treasure_room_generator.orb_floors.find(currentlevel) != treasure_room_generator.orb_floors.end() ) { - for ( x = 0; x < tempMap->width; x++ ) + std::string specialMapName = treasure_room_generator.orb_floors[currentlevel]; + std::string fullMapPath = physfsFormatMapName(specialMapName.c_str()); + if ( !fullMapPath.empty() ) { - if ( x == 0 || y == 0 || x == tempMap->width - 1 || y == tempMap->height - 1 ) + if ( loadSubRoomData(fullMapPath, &specialMapRooms.list) ) { - if ( !tempMap->tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * tempMap->height] ) + ++specialMapRooms.count; + + // load subrooms if found + for ( char letter = 'a'; letter <= 'z'; letter++ ) { - door = (door_t*) malloc(sizeof(door_t)); - door->x = x; - door->y = y; - if ( x == tempMap->width - 1 ) + char subRoomName[128] = ""; + snprintf(subRoomName, sizeof(subRoomName), "%s%c", specialMapName.c_str(), letter); + + std::string fullMapPath = physfsFormatMapName(subRoomName); + + if ( fullMapPath.empty() ) { - door->dir = door_t::DIR_EAST; - if ( y == tempMap->height - 1 ) - { - door->edge = door_t::EDGE_SOUTHEAST; - } - else if ( y == 0 ) - { - door->edge = door_t::EDGE_NORTHEAST; - } - else - { - door->edge = door_t::EDGE_EAST; - } + break; // no more levels to load } - else if ( y == tempMap->height - 1 ) + + auto& innerSubRooms = specialMapRooms.innerSubRooms[0]; + printlog("[SUBMAP GENERATOR] Found map lv %s, count: %d", subRoomName, innerSubRooms.count); + + if ( loadSubRoomData(fullMapPath, &innerSubRooms.list) ) { - door->dir = door_t::DIR_SOUTH; - if ( x == tempMap->width - 1 ) - { - door->edge = door_t::EDGE_SOUTHEAST; - } - else if ( x == 0 ) - { - door->edge = door_t::EDGE_SOUTHWEST; - } - else - { - door->edge = door_t::EDGE_SOUTH; - } + ++innerSubRooms.count; } - else if ( x == 0 ) - { - door->dir = door_t::DIR_WEST; - if ( y == tempMap->height - 1 ) - { - door->edge = door_t::EDGE_SOUTHWEST; - } - else if ( y == 0 ) - { - door->edge = door_t::EDGE_NORTHWEST; - } - else - { - door->edge = door_t::EDGE_WEST; - } - } - else if ( y == 0 ) - { - door->dir = door_t::DIR_NORTH; - if ( x == tempMap->width - 1 ) - { - door->edge = door_t::EDGE_NORTHEAST; - } - else if ( x == 0 ) - { - door->edge = door_t::EDGE_NORTHWEST; - } - else - { - door->edge = door_t::EDGE_NORTH; - } - } - node2 = list_AddNodeLast(newList); - node2->element = door; - node2->deconstructor = &defaultDeconstructor; } } } + specialMapRooms.possibleRooms.resize(specialMapRooms.count, true); } } - subRoomName = (char*)malloc(sizeof(char) * 128); - subRoomMapList.first = nullptr; - subRoomMapList.last = nullptr; - char letterString[2]; - letterString[1] = '\0'; - int subroomCount[100] = {0}; + static ConsoleVariable cvar_treasure_room_spawn("/treasure_room_spawn", ""); + static ConsoleVariable cvar_treasure_room_spawn_subroom("/treasure_room_spawn_subroom", ""); - // a maximum of 100 (0-99 inclusive) sublevels can be added to the pool - for ( int subRoomNumLevels = 0; subRoomNumLevels <= numlevels; subRoomNumLevels++ ) + // load treasure rooms + for ( int treasureRoomType = TreasureRoomTypes::TREASURE_TYPE_BRONZE; + treasureRoomType < TreasureRoomTypes::TREASURE_ROOM_MAX && numlevels > 1; ++treasureRoomType ) { - for ( char letter = 'a'; letter <= 'z'; letter++ ) - { - // look for mapnames ending in a letter a to z - strcpy(subRoomName, levelset); - snprintf(sublevelnum, 3, "%02d", subRoomNumLevels); - letterString[0] = letter; - strcat(subRoomName, sublevelnum); - strcat(subRoomName, letterString); + char prefix = '\0'; - fullMapPath = physfsFormatMapName(subRoomName); + if ( (svFlags & SV_FLAG_CHEATS) && *cvar_treasure_room_spawn != "" ) + { + if ( (*cvar_treasure_room_spawn).find("lockg") != std::string::npos ) + { + treasureRoomType = TREASURE_TYPE_GOLD; + } + else if ( (*cvar_treasure_room_spawn).find("lockb") != std::string::npos ) + { + treasureRoomType = TREASURE_TYPE_SILVER; + } + else if ( (*cvar_treasure_room_spawn).find("locki") != std::string::npos ) + { + treasureRoomType = TREASURE_TYPE_IRON; + } + else if ( (*cvar_treasure_room_spawn).find("locks") != std::string::npos ) + { + treasureRoomType = TREASURE_TYPE_BRONZE; + } + } + switch ( treasureRoomType ) + { + case TREASURE_TYPE_BRONZE: + prefix = 'b'; + break; + case TREASURE_TYPE_IRON: + prefix = 'i'; + break; + case TREASURE_TYPE_GOLD: + prefix = 'g'; + break; + case TREASURE_TYPE_SILVER: + prefix = 's'; + break; + default: + break; + } + if ( prefix == '\0' ) + { + continue; + } + for ( int treasureLevels = 0; treasureLevels < 100; ++treasureLevels ) + { + char treasureRoomName[128] = ""; + if ( (svFlags & SV_FLAG_CHEATS) && *cvar_treasure_room_spawn != "" ) + { + if ( treasureLevels == 0 ) + { + snprintf(treasureRoomName, sizeof(treasureRoomName), "%s", (*cvar_treasure_room_spawn).c_str()); + } + else + { + break; + } + } + else + { + snprintf(treasureRoomName, sizeof(treasureRoomName), "%s_lock%c%02d", levelset, prefix, treasureLevels); + } + std::string fullMapPath = physfsFormatMapName(treasureRoomName); if ( fullMapPath.empty() ) { break; // no more levels to load } - // check if there is another subroom to load - //if ( !dataPathExists(fullMapPath.c_str()) ) - //{ - // break; // no more levels to load - //} - - printlog("[SUBMAP GENERATOR] Found map lv %s, count: %d", subRoomName, subroomCount[subRoomNumLevels]); - ++subroomCount[subRoomNumLevels]; - - // allocate memory for the next subroom and attempt to load it - subRoomMap = (map_t*)malloc(sizeof(map_t)); - subRoomMap->tiles = nullptr; - subRoomMap->entities = (list_t*)malloc(sizeof(list_t)); - subRoomMap->entities->first = nullptr; - subRoomMap->entities->last = nullptr; - subRoomMap->creatures = new list_t; - subRoomMap->creatures->first = nullptr; - subRoomMap->creatures->last = nullptr; - subRoomMap->worldUI = nullptr; - subRoomMap->trapexcludelocations = nullptr; - subRoomMap->monsterexcludelocations = nullptr; - subRoomMap->lootexcludelocations = nullptr; - if ( fullMapPath.empty() || loadMap(fullMapPath.c_str(), subRoomMap, subRoomMap->entities, subRoomMap->creatures, &checkMapHash) == -1 ) - { - mapDeconstructor((void*)subRoomMap); - continue; // failed to load level - } - if (!verifyMapHash(fullMapPath.c_str(), checkMapHash)) + if ( !loadSubRoomData(fullMapPath, &treasureRooms[treasureRoomType].list) ) { - conductGameChallenges[CONDUCT_MODDED] = 1; - Mods::disableSteamAchievements = true; + continue; } - // level is successfully loaded, add it to the pool - subRoomList = (list_t*)malloc(sizeof(list_t)); - subRoomList->first = nullptr; - subRoomList->last = nullptr; - node = list_AddNodeLast(&subRoomMapList); - node->element = subRoomList; - node->deconstructor = &listDeconstructor; - - node = list_AddNodeLast(subRoomList); - node->element = subRoomMap; - node->deconstructor = &mapDeconstructor; + ++treasureRooms[treasureRoomType].count; - /*if ( subRoomMap->flags[MAP_FLAG_DISABLETRAPS] == 1 ) - { - printlog("%s: no traps", subRoomMap->filename); - } - if ( subRoomMap->flags[MAP_FLAG_DISABLEMONSTERS] == 1 ) + // load subrooms if found + for ( char letter = 'a'; letter <= 'z'; letter++ ) { - printlog("%s: no monsters", subRoomMap->filename); + if ( (svFlags & SV_FLAG_CHEATS) && *cvar_treasure_room_spawn_subroom != "" ) + { + letter = (*cvar_treasure_room_spawn_subroom)[0]; + } + + char treasureSubRoomName[128] = ""; + snprintf(treasureSubRoomName, sizeof(treasureSubRoomName), "%s%c", treasureRoomName, letter); + + std::string fullMapPath = physfsFormatMapName(treasureSubRoomName); + + if ( fullMapPath.empty() ) + { + break; // no more levels to load + } + + auto& innerSubRooms = treasureRooms[treasureRoomType].innerSubRooms[treasureLevels]; + printlog("[SUBMAP GENERATOR] Found map lv %s, count: %d", treasureSubRoomName, innerSubRooms.count); + + if ( loadSubRoomData(fullMapPath, &innerSubRooms.list) ) + { + ++innerSubRooms.count; + } + + if ( (svFlags & SV_FLAG_CHEATS) && *cvar_treasure_room_spawn_subroom != "" ) + { + break; + } } - if ( subRoomMap->flags[MAP_FLAG_DISABLELOOT] == 1 ) - { - printlog("%s: no loot", subRoomMap->filename); - }*/ + } + + treasureRooms[treasureRoomType].possibleRooms.resize(treasureRooms[treasureRoomType].count, true); + + if ( (svFlags & SV_FLAG_CHEATS) && *cvar_treasure_room_spawn != "" ) + { + break; + } + } + + { + bool doTreasureRoom = false; + if ( treasure_room_generator.bForceSpawnForCurrentFloor(secretlevelexit, minotaurlevel, map_rng) ) + { + doTreasureRoom = true; + } + + if ( !secretlevelexit && doTreasureRoom ) + { + std::vector treasureChances; + bool found = false; - // more nodes are created to record the exit points on the sublevel - for ( int y = 0; y < subRoomMap->height; y++ ) + for ( int treasureRoomType = TreasureRoomTypes::TREASURE_TYPE_BRONZE; + treasureRoomType < TreasureRoomTypes::TREASURE_ROOM_MAX; ++treasureRoomType ) { - for ( int x = 0; x < subRoomMap->width; x++ ) + int weight = 0; + switch ( treasureRoomType ) { - if ( x == 0 || y == 0 || x == subRoomMap->width - 1 || y == subRoomMap->height - 1 ) + case TREASURE_TYPE_BRONZE: + weight = 50; + break; + case TREASURE_TYPE_IRON: + weight = 0; + if ( (svFlags & SV_FLAG_CHEATS) && *cvar_treasure_room_spawn != "" ) { - if ( !subRoomMap->tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * subRoomMap->height] ) + if ( (*cvar_treasure_room_spawn).find("locki") != std::string::npos ) { - door = (door_t*)malloc(sizeof(door_t)); - door->x = x; - door->y = y; - if ( x == subRoomMap->width - 1 ) - { - door->dir = door_t::DIR_EAST; - if ( y == subRoomMap->height - 1 ) - { - door->edge = door_t::EDGE_SOUTHEAST; - } - else if ( y == 0 ) - { - door->edge = door_t::EDGE_NORTHEAST; - } - else - { - door->edge = door_t::EDGE_EAST; - } - } - else if ( y == subRoomMap->height - 1 ) - { - door->dir = door_t::DIR_SOUTH; - if ( x == subRoomMap->width - 1 ) - { - door->edge = door_t::EDGE_SOUTHEAST; - } - else if ( x == 0 ) - { - door->edge = door_t::EDGE_SOUTHWEST; - } - else - { - door->edge = door_t::EDGE_SOUTH; - } - } - else if ( x == 0 ) - { - door->dir = door_t::DIR_WEST; - if ( y == subRoomMap->height - 1 ) - { - door->edge = door_t::EDGE_SOUTHWEST; - } - else if ( y == 0 ) - { - door->edge = door_t::EDGE_NORTHWEST; - } - else - { - door->edge = door_t::EDGE_WEST; - } - } - else if ( y == 0 ) - { - door->dir = door_t::DIR_NORTH; - if ( x == subRoomMap->width - 1 ) - { - door->edge = door_t::EDGE_NORTHEAST; - } - else if ( x == 0 ) - { - door->edge = door_t::EDGE_NORTHWEST; - } - else - { - door->edge = door_t::EDGE_NORTH; - } - } - node2 = list_AddNodeLast(subRoomList); - node2->element = door; - node2->deconstructor = &defaultDeconstructor; + weight = 1; } } + break; + case TREASURE_TYPE_GOLD: + weight = 15; + break; + case TREASURE_TYPE_SILVER: + weight = 35; + break; + default: + break; } + + treasureChances.push_back(treasureRooms[treasureRoomType].count > 0 ? weight : 0); + if ( treasureChances.back() > 0 ) + { + found = true; + } + } + + if ( found ) + { + int pickedPos = map_rng.discrete(treasureChances.data(), treasureChances.size()); + treasureRoomLevel = &treasureRooms[pickedPos]; + } + } + } + + subRoomMapList.first = nullptr; + subRoomMapList.last = nullptr; + int subroomCount[100] = {0}; + + // a maximum of 100 (0-99 inclusive) sublevels can be added to the pool + for ( int subRoomNumLevels = 0; subRoomNumLevels <= numlevels; subRoomNumLevels++ ) + { + for ( char letter = 'a'; letter <= 'z'; letter++ ) + { + // look for mapnames ending in a letter a to z + char subRoomName[128] = ""; + snprintf(subRoomName, sizeof(subRoomName), "%s%02d%c", levelset, subRoomNumLevels, letter); + + std::string fullMapPath = physfsFormatMapName(subRoomName); + + if ( fullMapPath.empty() ) + { + break; // no more levels to load + } + + printlog("[SUBMAP GENERATOR] Found map lv %s, count: %d", subRoomName, subroomCount[subRoomNumLevels]); + + if ( loadSubRoomData(fullMapPath, &subRoomMapList) ) + { + ++subroomCount[subRoomNumLevels]; } } } - for ( char letter = 'a'; letter <= 'z' && shoplevel && shopSubRooms.shopFileName.size() > 0; letter++ ) + for ( char letter = 'a'; letter <= 'z' && shoplevel && shopSubRooms.rootMapFileName.size() > 0; letter++ ) { // look for mapnames ending in a letter a to z - char shopSubRoomName[64]; - snprintf(shopSubRoomName, sizeof(shopSubRoomName), "%s%c", shopSubRooms.shopFileName.c_str(), letter); - fullMapPath = physfsFormatMapName(shopSubRoomName); + char shopSubRoomName[128]; + snprintf(shopSubRoomName, sizeof(shopSubRoomName), "%s%c", shopSubRooms.rootMapFileName.c_str(), letter); + std::string fullMapPath = physfsFormatMapName(shopSubRoomName); if ( fullMapPath.empty() ) { break; // no more levels to load } - // check if there is another subroom to load - //if ( !dataPathExists(fullMapPath.c_str()) ) - //{ - // break; // no more levels to load - //} - printlog("[SUBMAP GENERATOR] Found map lv %s, count: %d", shopSubRoomName, shopSubRooms.count); - ++shopSubRooms.count; - - // allocate memory for the next subroom and attempt to load it - map_t* subRoomMap = (map_t*)malloc(sizeof(map_t)); - subRoomMap->tiles = nullptr; - subRoomMap->entities = (list_t*)malloc(sizeof(list_t)); - subRoomMap->entities->first = nullptr; - subRoomMap->entities->last = nullptr; - subRoomMap->creatures = new list_t; - subRoomMap->creatures->first = nullptr; - subRoomMap->creatures->last = nullptr; - subRoomMap->worldUI = nullptr; - subRoomMap->trapexcludelocations = nullptr; - subRoomMap->monsterexcludelocations = nullptr; - subRoomMap->lootexcludelocations = nullptr; - if ( fullMapPath.empty() || loadMap(fullMapPath.c_str(), subRoomMap, subRoomMap->entities, subRoomMap->creatures, &checkMapHash) == -1 ) - { - mapDeconstructor((void*)subRoomMap); - continue; // failed to load level - } - if (!verifyMapHash(fullMapPath.c_str(), checkMapHash)) - { - conductGameChallenges[CONDUCT_MODDED] = 1; - Mods::disableSteamAchievements = true; - } - - // level is successfully loaded, add it to the pool - subRoomList = (list_t*)malloc(sizeof(list_t)); - subRoomList->first = nullptr; - subRoomList->last = nullptr; - - node = list_AddNodeLast(&shopSubRooms.list); - node->element = subRoomList; - node->deconstructor = &listDeconstructor; - - node = list_AddNodeLast(subRoomList); - node->element = subRoomMap; - node->deconstructor = &mapDeconstructor; - // more nodes are created to record the exit points on the sublevel - for ( y = 0; y < subRoomMap->height; y++ ) + if ( loadSubRoomData(fullMapPath, &shopSubRooms.list) ) { - for ( x = 0; x < subRoomMap->width; x++ ) - { - if ( x == 0 || y == 0 || x == subRoomMap->width - 1 || y == subRoomMap->height - 1 ) - { - if ( !subRoomMap->tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * subRoomMap->height] ) - { - door = (door_t*)malloc(sizeof(door_t)); - door->x = x; - door->y = y; - if ( x == subRoomMap->width - 1 ) - { - door->dir = door_t::DIR_EAST; - if ( y == subRoomMap->height - 1 ) - { - door->edge = door_t::EDGE_SOUTHEAST; - } - else if ( y == 0 ) - { - door->edge = door_t::EDGE_NORTHEAST; - } - else - { - door->edge = door_t::EDGE_EAST; - } - } - else if ( y == subRoomMap->height - 1 ) - { - door->dir = door_t::DIR_SOUTH; - if ( x == subRoomMap->width - 1 ) - { - door->edge = door_t::EDGE_SOUTHEAST; - } - else if ( x == 0 ) - { - door->edge = door_t::EDGE_SOUTHWEST; - } - else - { - door->edge = door_t::EDGE_SOUTH; - } - } - else if ( x == 0 ) - { - door->dir = door_t::DIR_WEST; - if ( y == subRoomMap->height - 1 ) - { - door->edge = door_t::EDGE_SOUTHWEST; - } - else if ( y == 0 ) - { - door->edge = door_t::EDGE_NORTHWEST; - } - else - { - door->edge = door_t::EDGE_WEST; - } - } - else if ( y == 0 ) - { - door->dir = door_t::DIR_NORTH; - if ( x == subRoomMap->width - 1 ) - { - door->edge = door_t::EDGE_NORTHEAST; - } - else if ( x == 0 ) - { - door->edge = door_t::EDGE_NORTHWEST; - } - else - { - door->edge = door_t::EDGE_NORTH; - } - } - node2 = list_AddNodeLast(subRoomList); - node2->element = door; - node2->deconstructor = &defaultDeconstructor; - } - } - } + ++shopSubRooms.count; } } StartRoomInfo_t startRoomInfo; + std::vector treasureRoomLocations(map.width * map.height, false); + std::vector decorationexcludelocations(map.width * map.height, false); // generate dungeon level... int roomcount = 0; @@ -1598,9 +1829,9 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple map.trapexcludelocations = (bool*)malloc(sizeof(bool) * map.width * map.height); map.monsterexcludelocations = (bool*)malloc(sizeof(bool) * map.width * map.height); map.lootexcludelocations = (bool*)malloc(sizeof(bool) * map.width * map.height); - for ( y = 0; y < map.height; y++ ) + for ( int y = 0; y < map.height; y++ ) { - for ( x = 0; x < map.width; x++ ) + for ( int x = 0; x < map.width; x++ ) { if ( x < (std::max(2, getMapPossibleLocationX1())) || y < (std::max(2, getMapPossibleLocationY1())) @@ -1636,7 +1867,8 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } possiblelocations2 = (bool*) malloc(sizeof(bool) * map.width * map.height); firstroomtile = (bool*) malloc(sizeof(bool) * map.width * map.height); - possiblerooms = (bool*) malloc(sizeof(bool) * numlevels); + secretlevelexittile = (bool*)malloc(sizeof(bool) * map.width * map.height); + bool* possiblerooms = (bool*) malloc(sizeof(bool) * numlevels); for ( c = 0; c < numlevels; c++ ) { possiblerooms[c] = true; @@ -1645,14 +1877,19 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple for ( c = 0; c < levellimit; c++ ) { // reset array of possible locations for the current room - for ( y = 0; y < map.height; y++ ) + for ( int y = 0; y < map.height; y++ ) { - for ( x = 0; x < map.width; x++ ) + for ( int x = 0; x < map.width; x++ ) { possiblelocations2[x + y * map.width] = true; } } - doorNode = nullptr; + + node_t* doorNode = nullptr; + map_t* tempMap = nullptr; + + int levelnum = 0; + int levelnum2 = 0; // pick the room to be used if ( c == 0 ) @@ -1707,7 +1944,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple default: break; } - fullMapPath = physfsFormatMapName(secretmapname); + std::string fullMapPath = physfsFormatMapName(secretmapname); if ( fullMapPath.empty() || loadMap(fullMapPath.c_str(), &secretlevelmap, secretlevelmap.entities, secretlevelmap.creatures, &checkMapHash) == -1 ) { list_FreeAll(secretlevelmap.entities); @@ -1729,31 +1966,24 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple levelnum2 = -1; tempMap = &secretlevelmap; } - else if ( c == 2 && shoplevel ) + else if ( ((c == 1 && !secretlevelexit) || (c == 3 && secretlevelexit)) && treasureRoomLevel ) { - // generate a shop + // generate a treasure room levelnum = 0; levelnum2 = -1; - tempMap = &shopmap; - } - else - { - if ( !numlevels ) - { - break; - } - levelnum = map_rng.rand() % (numlevels); // draw randomly from the pool + + levelnum = map_rng.rand() % (treasureRoomLevel->count); // draw randomly from the pool // traverse the map list to the picked level - node = mapList.first; + node = treasureRoomLevel->list.first; i = 0; j = -1; - while (1) + while ( 1 ) { - if (possiblerooms[i]) + if ( treasureRoomLevel->possibleRooms[i] ) { ++j; - if (j == levelnum) + if ( j == levelnum ) { break; } @@ -1766,21 +1996,88 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple doorNode = node->next; tempMap = (map_t*)node->element; } + else if ( c == 2 && shoplevel ) + { + // generate a shop + levelnum = 0; + levelnum2 = -1; + tempMap = &shopmap; + } + else if ( c == 4 && specialMapRooms.count > 0 ) + { + // generate a special room + levelnum = 0; + levelnum2 = -1; - // find locations where the selected room can be added to the level - numpossiblelocations = map.width * map.height; + levelnum = map_rng.rand() % (specialMapRooms.count); // draw randomly from the pool + + // traverse the map list to the picked level + node = specialMapRooms.list.first; + i = 0; + j = -1; + while ( 1 ) + { + if ( specialMapRooms.possibleRooms[i] ) + { + ++j; + if ( j == levelnum ) + { + break; + } + } + node = node->next; + ++i; + } + levelnum2 = i; + node = ((list_t*)node->element)->first; + doorNode = node->next; + tempMap = (map_t*)node->element; + } + else + { + if ( !numlevels ) + { + break; + } + levelnum = map_rng.rand() % (numlevels); // draw randomly from the pool + + // traverse the map list to the picked level + node = mapList.first; + i = 0; + j = -1; + while (1) + { + if (possiblerooms[i]) + { + ++j; + if (j == levelnum) + { + break; + } + } + node = node->next; + ++i; + } + levelnum2 = i; + node = ((list_t*)node->element)->first; + doorNode = node->next; + tempMap = (map_t*)node->element; + } + + // find locations where the selected room can be added to the level + numpossiblelocations = map.width * map.height; bool hellGenerationFix = !strncmp(map.name, "Hell", 4) && !MFLAG_GENADJACENTROOMS; - for ( y0 = 0; y0 < map.height; y0++ ) + for ( int y0 = 0; y0 < map.height; y0++ ) { - for ( x0 = 0; x0 < map.width; x0++ ) + for ( int x0 = 0; x0 < map.width; x0++ ) { - for ( y1 = y0; y1 < std::min(y0 + tempMap->height, map.height); y1++ ) + for ( int y1 = y0; y1 < std::min(y0 + tempMap->height, map.height); y1++ ) { // don't generate start room in hell along the rightmost wall, causes pathing to fail. Check 2 tiles to the right extra // to try fit start room. - for ( x1 = x0; x1 < std::min(x0 + tempMap->width + ((hellGenerationFix && c == 0) ? 2 : 0), map.width); x1++ ) + for ( int x1 = x0; x1 < std::min(x0 + tempMap->width + ((hellGenerationFix && c == 0) ? 2 : 0), map.width); x1++ ) { if ( possiblelocations[x1 + y1 * map.width] == false && possiblelocations2[x0 + y0 * map.width] == true ) { @@ -1793,7 +2090,37 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } // in case no locations are available, remove this room from the selection - if ( numpossiblelocations <= 0 ) + if ( ((c == 1 && !secretlevelexit) || (c == 3 && secretlevelexit)) && treasureRoomLevel) + { + if ( numpossiblelocations <= 0 ) + { + if ( levelnum2 >= 0 && levelnum2 < treasureRoomLevel->count ) + { + treasureRoomLevel->possibleRooms[levelnum2] = false; + } + --treasureRoomLevel->count; + if ( treasureRoomLevel->count <= 0 ) + { + treasureRoomLevel = nullptr; + } + --c; + continue; + } + } + else if ( c == 4 && specialMapRooms.count > 0 ) + { + if ( numpossiblelocations <= 0 ) + { + if ( levelnum2 >= 0 && levelnum2 < specialMapRooms.count ) + { + specialMapRooms.possibleRooms[levelnum2] = false; + } + --specialMapRooms.count; + --c; + continue; + } + } + else if ( numpossiblelocations <= 0 ) { if ( levelnum2 >= 0 && levelnum2 < numlevels ) { @@ -1822,10 +2149,22 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple map.lootexcludelocations = nullptr; } free(firstroomtile); - free(sublevelname); - free(subRoomName); + free(secretlevelexittile); list_FreeAll(&subRoomMapList); list_FreeAll(&mapList); + for ( int i = 0; i < TreasureRoomTypes::TREASURE_ROOM_MAX; ++i ) + { + list_FreeAll(&treasureRooms[i].list); + for ( auto& r : treasureRooms[i].innerSubRooms ) + { + list_FreeAll(&r.second.list); + } + } + list_FreeAll(&specialMapRooms.list); + for ( auto& r : specialMapRooms.innerSubRooms ) + { + list_FreeAll(&r.second.list); + } if ( shoplevel && c == 2 ) { list_FreeAll(shopmap.entities); @@ -1859,6 +2198,9 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple continue; } + int x = 0; + int y = 0; + // otherwise, choose a location from those available (to be stored in x/y) if ( MFLAG_GENADJACENTROOMS ) { @@ -2000,14 +2342,15 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple // now copy all the geometry from the sublevel to the chosen location if ( c == 0 ) { - for ( z = 0; z < map.width * map.height; ++z ) + for ( int z = 0; z < map.width * map.height; ++z ) { firstroomtile[z] = false; + secretlevelexittile[z] = false; } } - x1 = x + tempMap->width; - y1 = y + tempMap->height; + int x1 = x + tempMap->width; + int y1 = y + tempMap->height; //**********pick subroom if available int pickSubRoom = 0; @@ -2016,7 +2359,64 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple int subRoom_tileStartx = -1; int subRoom_tileStarty = -1; bool foundSubRoom = false; - if ( c == 2 && shoplevel && tempMap == &shopmap && shopSubRooms.count > 0 ) + map_t* subRoomMap = nullptr; + node_t* subRoomDoorNode = nullptr; + node_t* subRoomNode = nullptr; + int subroomLogCount = 0; + static char submapLogMsg[256] = ""; + if ( (((c == 1 && !secretlevelexit) || (c == 3 && secretlevelexit)) && treasureRoomLevel) + && treasureRoomLevel->innerSubRooms.find(levelnum2) != treasureRoomLevel->innerSubRooms.end() + && treasureRoomLevel->innerSubRooms[levelnum2].count > 0 ) + { + auto& innerSubRooms = treasureRoomLevel->innerSubRooms[levelnum2]; + pickSubRoom = map_rng.rand() % innerSubRooms.count; + subRoomNode = innerSubRooms.list.first; + int k = 0; + while ( 1 ) + { + if ( k == pickSubRoom ) + { + break; + } + subRoomNode = subRoomNode->next; + k++; + } + subRoomNode = ((list_t*)subRoomNode->element)->first; + subRoomMap = (map_t*)subRoomNode->element; + subRoomDoorNode = subRoomNode->next; + + subroomLogCount = innerSubRooms.count; + + snprintf(submapLogMsg, sizeof(submapLogMsg), + "Picked level: %d from %d possible rooms in submap %s at x:%d y:%d", pickSubRoom + 1, subroomLogCount, tempMap->filename, x, y); + } + else if ( c == 4 && specialMapRooms.count > 0 + && specialMapRooms.innerSubRooms.find(levelnum2) != specialMapRooms.innerSubRooms.end() + && specialMapRooms.innerSubRooms[levelnum2].count > 0 ) + { + auto& innerSubRooms = specialMapRooms.innerSubRooms[levelnum2]; + pickSubRoom = map_rng.rand() % innerSubRooms.count; + subRoomNode = innerSubRooms.list.first; + int k = 0; + while ( 1 ) + { + if ( k == pickSubRoom ) + { + break; + } + subRoomNode = subRoomNode->next; + k++; + } + subRoomNode = ((list_t*)subRoomNode->element)->first; + subRoomMap = (map_t*)subRoomNode->element; + subRoomDoorNode = subRoomNode->next; + + subroomLogCount = innerSubRooms.count; + + snprintf(submapLogMsg, sizeof(submapLogMsg), + "Picked level: %d from %d possible rooms in submap %s at x:%d y:%d", pickSubRoom + 1, subroomLogCount, tempMap->filename, x, y); + } + else if ( c == 2 && shoplevel && tempMap == &shopmap && shopSubRooms.count > 0 ) { pickSubRoom = map_rng.rand() % shopSubRooms.count; subRoomNode = shopSubRooms.list.first; @@ -2033,6 +2433,11 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple subRoomNode = ((list_t*)subRoomNode->element)->first; subRoomMap = (map_t*)subRoomNode->element; subRoomDoorNode = subRoomNode->next; + + subroomLogCount = shopSubRooms.count; + + snprintf(submapLogMsg, sizeof(submapLogMsg), + "Picked level: %d from %d possible rooms in submap %s at x:%d y:%d", pickSubRoom + 1, subroomLogCount, shopSubRooms.rootMapFileName.c_str(), x, y); } else { @@ -2077,22 +2482,18 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple subRoomMap = (map_t*)subRoomNode->element; subRoomDoorNode = subRoomNode->next; } - } - int subroomLogCount = 0; - if ( shopSubRooms.count > 0 ) - { - subroomLogCount = shopSubRooms.count; - } - else - { subroomLogCount = subroomCount[levelnum + 1]; + + snprintf(submapLogMsg, sizeof(submapLogMsg), + "Picked level: %d from %d possible rooms in submap %s at x:%d y:%d", pickSubRoom + 1, subroomLogCount, tempMap->filename, x, y); } - for ( z = 0; z < MAPLAYERS; z++ ) + + for ( int z = 0; z < MAPLAYERS; z++ ) { - for ( y0 = y; y0 < y1; y0++ ) + for ( int y0 = y; y0 < y1; y0++ ) { - for ( x0 = x; x0 < x1; x0++ ) + for ( int x0 = x; x0 < x1; x0++ ) { if ( (subroomLogCount > 0) && tempMap->tiles[z + (y0 - y) * MAPLAYERS + (x0 - x) * MAPLAYERS * tempMap->height] == 201 ) { @@ -2101,14 +2502,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple subRoom_tileStartx = x0; subRoom_tileStarty = y0; foundSubRoom = true; - if ( shoplevel && c == 2 ) - { - printlog("Picked level: %d from %d possible rooms in submap %s at x:%d y:%d", pickSubRoom + 1, subroomLogCount, shopSubRooms.shopFileName.c_str(), x, y); - } - else - { - printlog("Picked level: %d from %d possible rooms in submap %d at x:%d y:%d", pickSubRoom + 1, subroomLogCount, levelnum + 1, x, y); - } + printlog(submapLogMsg); } map.tiles[z + y0 * MAPLAYERS + x0 * MAPLAYERS * map.height] = subRoomMap->tiles[z + (subRoom_tiley)* MAPLAYERS + (subRoom_tilex)* MAPLAYERS * subRoomMap->height]; @@ -2180,6 +2574,23 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple shoparea[y0 + x0 * map.height] = true; } } + + if ( c == 1 && secretlevelexit ) + { + secretlevelexittile[y0 + x0 * map.height] = true; + } + if ( ((c == 1 && !secretlevelexit) || (c == 3 && secretlevelexit)) && treasureRoomLevel ) + { + decorationexcludelocations[x0 + y0 * map.width] = true; + treasureRoomLocations[x0 + y0 * map.width] = true; + map.tileAttributes[(y0)*MAPLAYERS + (x0)*MAPLAYERS * map.height] |= map_t::TILE_ATTRIBUTE_TREASURE_ROOM; + } + if ( c == 4 && specialMapRooms.count > 0 ) + { + decorationexcludelocations[x0 + y0 * map.width] = true; + treasureRoomLocations[x0 + y0 * map.width] = true; + map.tileAttributes[(y0)*MAPLAYERS + (x0)*MAPLAYERS * map.height] |= map_t::TILE_ATTRIBUTE_TREASURE_ROOM; + } } // remove any existing entities in this region too @@ -2310,14 +2721,38 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple ++roomcount; } list_FreeAll(&shopSubRooms.list); + for ( int i = 0; i < TreasureRoomTypes::TREASURE_ROOM_MAX; ++i ) + { + list_FreeAll(&treasureRooms[i].list); + for ( auto& r : treasureRooms[i].innerSubRooms ) + { + list_FreeAll(&r.second.list); + } + } + list_FreeAll(&specialMapRooms.list); + for ( auto& r : specialMapRooms.innerSubRooms ) + { + list_FreeAll(&r.second.list); + } free(possiblerooms); free(possiblelocations2); } else { + for ( int i = 0; i < TreasureRoomTypes::TREASURE_ROOM_MAX; ++i ) + { + list_FreeAll(&treasureRooms[i].list); + for ( auto& r : treasureRooms[i].innerSubRooms ) + { + list_FreeAll(&r.second.list); + } + } + list_FreeAll(&specialMapRooms.list); + for ( auto& r : specialMapRooms.innerSubRooms ) + { + list_FreeAll(&r.second.list); + } list_FreeAll(&shopSubRooms.list); - free(subRoomName); - free(sublevelname); list_FreeAll(&subRoomMapList); list_FreeAll(&mapList); list_FreeAll(&doorList); @@ -2336,14 +2771,15 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple entity = (Entity*)node2->element; if ( entity->x / 16 == door->x && entity->y / 16 == door->y && (/*entity->sprite == 2 || entity->sprite == 3 ||*/ entity->sprite == 19 || entity->sprite == 20 - || entity->sprite == 113 || entity->sprite == 114) ) + || entity->sprite == 113 || entity->sprite == 114 + || entity->sprite == 217 || entity->sprite == 218) ) { int doordir = door->dir; // if door is on a corner, then determine the proper facing based on the entity dir if ( doordir == door_t::DIR_EAST || doordir == door_t::DIR_WEST ) { - if ( (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113) ) // north/south sprites + if ( (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113 || entity->sprite == 217) ) // north/south sprites { switch ( door->edge ) { @@ -2365,7 +2801,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } else if ( doordir == door_t::DIR_SOUTH || doordir == door_t::DIR_NORTH ) { - if ( (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114) ) // east/west sprites + if ( (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114 || entity->sprite == 218) ) // east/west sprites { switch ( door->edge ) { @@ -2389,113 +2825,141 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple switch ( doordir ) { case door_t::DIR_EAST: // east - map.tiles[OBSTACLELAYER + door->y * MAPLAYERS + (door->x + 1)*MAPLAYERS * map.height] = 0; - for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) + if ( treasureRoomLocations[(door->x + 1) + (door->y) * map.width] ) + { + // don't defile this room + } + else { - entity = (Entity*)node3->element; - nextnode = node3->next; - if ( mapSpriteIsDoorway(entity->sprite) ) + map.tiles[OBSTACLELAYER + door->y * MAPLAYERS + (door->x + 1)*MAPLAYERS * map.height] = 0; + for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) { - if ( (int)(entity->x / 16) == door->x + 2 && (int)(entity->y / 16) == door->y - && (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113) ) // north/south doors 2 tiles away - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y + 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y - 1 ) + entity = (Entity*)node3->element; + nextnode = node3->next; + if ( mapSpriteIsDoorway(entity->sprite) ) { - list_RemoveNode(entity->mynode); + if ( (int)(entity->x / 16) == door->x + 2 && (int)(entity->y / 16) == door->y + && (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113 || entity->sprite == 217) ) // north/south doors 2 tiles away + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } } } } break; case door_t::DIR_SOUTH: // south - map.tiles[OBSTACLELAYER + (door->y + 1)*MAPLAYERS + door->x * MAPLAYERS * map.height] = 0; - for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) + if ( treasureRoomLocations[(door->x) + (door->y + 1) * map.width] ) + { + // don't defile this room + } + else { - entity = (Entity*)node3->element; - nextnode = node3->next; - if ( mapSpriteIsDoorway(entity->sprite) ) + map.tiles[OBSTACLELAYER + (door->y + 1)*MAPLAYERS + door->x * MAPLAYERS * map.height] = 0; + for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) { - if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y + 2 - && (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114) ) // east/west doors 2 tiles away - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y + 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y + 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y + 1 ) + entity = (Entity*)node3->element; + nextnode = node3->next; + if ( mapSpriteIsDoorway(entity->sprite) ) { - list_RemoveNode(entity->mynode); + if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y + 2 + && (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114 || entity->sprite == 218) ) // east/west doors 2 tiles away + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } } } } break; case door_t::DIR_WEST: // west - map.tiles[OBSTACLELAYER + door->y * MAPLAYERS + (door->x - 1)*MAPLAYERS * map.height] = 0; - for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) + if ( treasureRoomLocations[(door->x - 1) + (door->y) * map.width] ) { - entity = (Entity*)node3->element; - nextnode = node3->next; - if ( mapSpriteIsDoorway(entity->sprite) ) + // don't defile this room + } + else + { + map.tiles[OBSTACLELAYER + door->y * MAPLAYERS + (door->x - 1) * MAPLAYERS * map.height] = 0; + for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) { - if ( (int)(entity->x / 16) == door->x - 2 && (int)(entity->y / 16) == door->y - && (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113) ) // north/south doors 2 tiles away - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y + 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y - 1 ) + entity = (Entity*)node3->element; + nextnode = node3->next; + if ( mapSpriteIsDoorway(entity->sprite) ) { - list_RemoveNode(entity->mynode); + if ( (int)(entity->x / 16) == door->x - 2 && (int)(entity->y / 16) == door->y + && (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113 || entity->sprite == 217) ) // north/south doors 2 tiles away + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } } } } break; case door_t::DIR_NORTH: // north - map.tiles[OBSTACLELAYER + (door->y - 1)*MAPLAYERS + door->x * MAPLAYERS * map.height] = 0; - for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) + if ( treasureRoomLocations[(door->x) + (door->y - 1) * map.width] ) + { + // don't defile this room + } + else { - entity = (Entity*)node3->element; - nextnode = node3->next; - if ( mapSpriteIsDoorway(entity->sprite) ) + map.tiles[OBSTACLELAYER + (door->y - 1)*MAPLAYERS + door->x * MAPLAYERS * map.height] = 0; + for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) { - if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y - 2 - && (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114) ) // east/west doors 2 tiles away - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y - 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y - 1 ) + entity = (Entity*)node3->element; + nextnode = node3->next; + if ( mapSpriteIsDoorway(entity->sprite) ) { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y - 1 ) - { - list_RemoveNode(entity->mynode); + if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y - 2 + && (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114 || entity->sprite == 218) ) // east/west doors 2 tiles away + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } } } } @@ -2521,7 +2985,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple // if door is on a corner, then determine the proper facing based on the entity dir if ( doordir == door_t::DIR_EAST || doordir == door_t::DIR_WEST ) { - if ( (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113) ) // north/south sprites + if ( (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113 || entity->sprite == 217) ) // north/south sprites { switch ( door->edge ) { @@ -2544,7 +3008,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } else if ( doordir == door_t::DIR_SOUTH || doordir == door_t::DIR_NORTH ) { - if ( (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114) ) // east/west sprites + if ( (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114 || entity->sprite == 218) ) // east/west sprites { switch ( door->edge ) { @@ -2569,113 +3033,142 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple switch ( doordir ) { case door_t::DIR_EAST: // east - map.tiles[OBSTACLELAYER + door->y * MAPLAYERS + (door->x + 1)*MAPLAYERS * map.height] = 0; - for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) + + if ( treasureRoomLocations[(door->x + 1) + (door->y) * map.width] ) + { + // don't defile this room + } + else { - entity = (Entity*)node3->element; - nextnode = node3->next; - if ( mapSpriteIsDoorway(entity->sprite) ) + map.tiles[OBSTACLELAYER + door->y * MAPLAYERS + (door->x + 1) * MAPLAYERS * map.height] = 0; + for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) { - if ( (int)(entity->x / 16) == door->x + 2 && (int)(entity->y / 16) == door->y - && (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113) ) // north/south doors 2 tiles away - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y ) + entity = (Entity*)node3->element; + nextnode = node3->next; + if ( mapSpriteIsDoorway(entity->sprite) ) { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y + 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y - 1 ) - { - list_RemoveNode(entity->mynode); + if ( (int)(entity->x / 16) == door->x + 2 && (int)(entity->y / 16) == door->y + && (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113 || entity->sprite == 217) ) // north/south doors 2 tiles away + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } } } } break; case door_t::DIR_SOUTH: // south - map.tiles[OBSTACLELAYER + (door->y + 1)*MAPLAYERS + door->x * MAPLAYERS * map.height] = 0; - for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) + if ( treasureRoomLocations[(door->x) + (door->y + 1) * map.width] ) { - entity = (Entity*)node3->element; - nextnode = node3->next; - if ( mapSpriteIsDoorway(entity->sprite) ) + // don't defile this room + } + else + { + map.tiles[OBSTACLELAYER + (door->y + 1) * MAPLAYERS + door->x * MAPLAYERS * map.height] = 0; + for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) { - if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y + 2 - && (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114) ) // east/west doors 2 tiles away - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y + 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y + 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y + 1 ) + entity = (Entity*)node3->element; + nextnode = node3->next; + if ( mapSpriteIsDoorway(entity->sprite) ) { - list_RemoveNode(entity->mynode); + if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y + 2 + && (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114 || entity->sprite == 218) ) // east/west doors 2 tiles away + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } } } } break; case door_t::DIR_WEST: // west - map.tiles[OBSTACLELAYER + door->y * MAPLAYERS + (door->x - 1)*MAPLAYERS * map.height] = 0; - for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) + if ( treasureRoomLocations[(door->x - 1) + (door->y) * map.width] ) + { + // don't defile this room + } + else { - entity = (Entity*)node3->element; - nextnode = node3->next; - if ( mapSpriteIsDoorway(entity->sprite) ) + map.tiles[OBSTACLELAYER + door->y * MAPLAYERS + (door->x - 1) * MAPLAYERS * map.height] = 0; + for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) { - if ( (int)(entity->x / 16) == door->x - 2 && (int)(entity->y / 16) == door->y - && (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113) ) // north/south doors 2 tiles away + entity = (Entity*)node3->element; + nextnode = node3->next; + if ( mapSpriteIsDoorway(entity->sprite) ) { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y + 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y - 1 ) - { - list_RemoveNode(entity->mynode); + if ( (int)(entity->x / 16) == door->x - 2 && (int)(entity->y / 16) == door->y + && (entity->sprite == 3 || entity->sprite == 19 || entity->sprite == 113 || entity->sprite == 217) ) // north/south doors 2 tiles away + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y + 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } } } } break; case door_t::DIR_NORTH: // north - map.tiles[OBSTACLELAYER + (door->y - 1)*MAPLAYERS + door->x * MAPLAYERS * map.height] = 0; - for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) + if ( treasureRoomLocations[(door->x) + (door->y - 1) * map.width] ) + { + // don't defile this room + } + else { - entity = (Entity*)node3->element; - nextnode = node3->next; - if ( mapSpriteIsDoorway(entity->sprite) ) + map.tiles[OBSTACLELAYER + (door->y - 1) * MAPLAYERS + door->x * MAPLAYERS * map.height] = 0; + for ( node3 = map.entities->first; node3 != nullptr; node3 = nextnode ) { - if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y - 2 - && (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114) ) // east/west doors 2 tiles away - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y - 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y - 1 ) - { - list_RemoveNode(entity->mynode); - } - else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y - 1 ) + entity = (Entity*)node3->element; + nextnode = node3->next; + if ( mapSpriteIsDoorway(entity->sprite) ) { - list_RemoveNode(entity->mynode); + if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y - 2 + && (entity->sprite == 2 || entity->sprite == 20 || entity->sprite == 114 || entity->sprite == 218) ) // east/west doors 2 tiles away + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x + 1 && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } + else if ( (int)(entity->x / 16) == door->x - 1 && (int)(entity->y / 16) == door->y - 1 ) + { + list_RemoveNode(entity->mynode); + } } } } @@ -2686,11 +3179,11 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } bool foundsubmaptile = false; // if for whatever reason some submap 201 tiles didn't get filled in, let's get rid of those. - for ( z = 0; z < MAPLAYERS; ++z ) + for ( int z = 0; z < MAPLAYERS; ++z ) { - for ( y = 1; y < map.height; ++y ) + for ( int y = 1; y < map.height; ++y ) { - for ( x = 1; x < map.height; ++x ) + for ( int x = 1; x < map.width; ++x ) { if ( map.tiles[z + y * MAPLAYERS + x * MAPLAYERS * map.height] == 201 ) { @@ -2705,17 +3198,17 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple printlog("[SUBMAP GENERATOR] Found some junk tiles!"); } - for ( node = map.entities->first; node != nullptr; node = node->next ) + // fix gate air-gap borders on citadel map next to perimeter gates. + if ( !strncmp(map.name, "Citadel", 7) ) { - // fix gate air-gap borders on citadel map next to perimeter gates. - if ( !strncmp(map.name, "Citadel", 7) ) + for ( node = map.entities->first; node != nullptr; node = node->next ) { Entity* gateEntity = (Entity*)node->element; if ( gateEntity->sprite == 19 || gateEntity->sprite == 20 ) // N/S E/W gates take these sprite numbers in the editor. { int gatex = static_cast(gateEntity->x) / 16; int gatey = static_cast(gateEntity->y) / 16; - for ( z = OBSTACLELAYER; z < MAPLAYERS; ++z ) + for ( int z = OBSTACLELAYER; z < MAPLAYERS; ++z ) { if ( gateEntity->x / 16 == 1 ) // along leftmost edge { @@ -2802,9 +3295,9 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple possiblelocations[c] = false; } std::unordered_map trapLocationAndSide; - for ( y = 1; y < map.height - 1; ++y ) + for ( int y = 1; y < map.height - 1; ++y ) { - for ( x = 1; x < map.width - 1; ++x ) + for ( int x = 1; x < map.width - 1; ++x ) { int sides = 0; if ( firstroomtile[y + x * map.height] ) @@ -2857,8 +3350,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } // don't spawn traps in doors - node_t* doorNode; - for ( doorNode = doorList.first; doorNode != nullptr; doorNode = doorNode->next ) + for ( node_t* doorNode = doorList.first; doorNode != nullptr; doorNode = doorNode->next ) { door_t* door = (door_t*)doorNode->element; int x = std::min(std::max(0, door->x), map.width - 1); //TODO: Why are const int and unsigned int being compared? @@ -2977,8 +3469,8 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple i = -1; //printlog("pickedlocation: %d\n",pickedlocation); //printlog("numpossiblelocations: %d\n",numpossiblelocations); - x = 0; - y = 0; + int x = 0; + int y = 0; while ( 1 ) { if ( possiblelocations[y + x * map.height] == true ) @@ -3255,9 +3747,9 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple // monsters, decorations, and items numpossiblelocations = map.width * map.height; - for ( y = 0; y < map.height; y++ ) + for ( int y = 0; y < map.height; y++ ) { - for ( x = 0; x < map.width; x++ ) + for ( int x = 0; x < map.width; x++ ) { if ( firstroomtile[y + x * map.height] ) { @@ -3320,11 +3812,26 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } } } + + std::vector> generateKeyItems; + bool ceilingTilesAllowed = !strncmp(map.filename, "fortress", 8); for ( node = map.entities->first; node != nullptr; node = node->next ) { entity = (Entity*)node->element; - x = entity->x / 16; - y = entity->y / 16; + int x = entity->x / 16; + int y = entity->y / 16; + + if ( checkSpriteType(entity->sprite) == 30 ) // wall locks + { + if ( entity->wallLockAutoGenKey != 0 ) + { + generateKeyItems.push_back(std::make_pair(static_cast((int)KEY_STONE + entity->wallLockMaterial), x + y * 10000)); + } + } + if ( entity->sprite == 119 && ceilingTilesAllowed ) // ceiling tile no block stuff + { + continue; + } if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) { if ( possiblelocations[y + x * map.height] ) @@ -3404,6 +3911,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple int exit_x = -1; int exit_y = -1; + int secretExitLadderTries = 200; //printlog("j: %d\n",j); //printlog("numpossiblelocations: %d\n",numpossiblelocations); for ( c = 0; c < std::min(j, numpossiblelocations); ++c ) @@ -3413,8 +3921,8 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple i = -1; //printlog("pickedlocation: %d\n",pickedlocation); //printlog("numpossiblelocations: %d\n",numpossiblelocations); - x = 0; - y = 0; + int x = 0; + int y = 0; bool skipPossibleLocationsDecrement = false; while ( 1 ) { @@ -3595,7 +4103,8 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple if ( (entity2 = (Entity*)node->element) ) { if ( entity2->sprite == 19 || entity2->sprite == 20 - || entity2->sprite == 113 || entity2->sprite == 114 ) + || entity2->sprite == 113 || entity2->sprite == 114 + /*|| entity2->sprite == 217 || entity2->sprite == 218*/ ) { int entx = entity2->x / 16; int enty = entity2->y / 16; @@ -3670,9 +4179,26 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple } else { - // normal exits - if ( strcmp(map.name, "Hell") ) + // determine if ladder inside treasure room + if ( treasureRoomLocations[x + y * map.width] ) + { + // try again, treasure room area + c--; + entity = NULL; + continue; + } + + if ( secretlevelexittile[y + x * map.height] && secretExitLadderTries > 0 ) + { + // try again, no exits in secret level exits + c--; + entity = NULL; + --secretExitLadderTries; + continue; + } + else if ( strcmp(map.name, "Hell") ) { + // normal exits entity = newEntity(11, 1, map.entities, nullptr); // ladder entity->behavior = &actLadder; } @@ -3683,9 +4209,38 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple entity->skill[3] = 1; // not secret portals though } - // determine if the ladder generated in a viable location - if ( strncmp(map.name, "Underworld", 10) ) + if ( entity && !strncmp(map.name, "Underworld", 10) ) + { + // determine if ladder is blocked off by walls + int obstacles = 0; + if ( checkObstacle((x + 1) * 16, (y) * 16, entity, NULL, false) ) + { + obstacles++; + } + if ( checkObstacle((x - 1) * 16, (y) * 16, NULL, NULL, false) ) + { + obstacles++; + } + if ( checkObstacle((x) * 16, (y + 1) * 16, entity, NULL, false) ) + { + obstacles++; + } + if ( checkObstacle((x) * 16, (y - 1) * 16, NULL, NULL, false) ) + { + obstacles++; + } + if ( obstacles >= 4 ) + { + // try again, enclosed area + c--; + list_RemoveNode(entity->mynode); + entity = NULL; + continue; + } + } + else if ( entity && strncmp(map.name, "Underworld", 10) ) { + // determine if the ladder generated in a viable location bool nopath = false; bool hellLadderFix = !strncmp(map.name, "Hell", 4); std::vector tempPassableEntities; @@ -3696,7 +4251,8 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple if ( (entity2 = (Entity*)node->element) ) { if ( entity2->sprite == 19 || entity2->sprite == 20 - || entity2->sprite == 113 || entity2->sprite == 114 ) + || entity2->sprite == 113 || entity2->sprite == 114 + /*|| entity2->sprite == 217 || entity2->sprite == 218*/ ) { int entx = entity2->x / 16; int enty = entity2->y / 16; @@ -3763,7 +4319,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple && ((c == 1 && !(secretlevel && currentlevel == 7)) || (c == 2 && secretlevel && currentlevel == 7)) ) { std::set walkableTiles; - for ( int isley = 1; isley < map.width - 1; ++isley ) + for ( int isley = 1; isley < map.height - 1; ++isley ) { for ( int islex = 1; islex < map.width - 1; ++islex ) { @@ -3972,6 +4528,10 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple { nodecoration = true; } + if ( decorationexcludelocations[x + y * map.width] == true ) + { + nodecoration = true; + } if ( forcedMonsterSpawns > 0 || forcedLootSpawns > 0 || (forcedDecorationSpawns > 0 && !nodecoration) ) { // force monsters, then loot, then decorations. @@ -4043,8 +4603,10 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple // decorations if ( (map_rng.rand() % 4 == 0 || (currentlevel <= 10 && !customTrapsForMapInUse)) && strcmp(map.name, "Hell") ) { - switch ( map_rng.rand() % 7 ) + if ( !strncmp(map.filename, "fortress", 8) ) { + switch ( map_rng.rand() % 4 ) + { case 0: entity = newEntity(12, 1, map.entities, nullptr); //Firecamp. break; //Firecamp @@ -4059,17 +4621,38 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple setSpriteAttributes(entity, nullptr, nullptr); entity->chestLocked = -1; break; //Chest - case 4: - entity = newEntity(39, 1, map.entities, nullptr); //Tomb. - break; //Tomb - case 5: - entity = newEntity(59, 1, map.entities, nullptr); //Table. - setSpriteAttributes(entity, nullptr, nullptr); - break; //Table - case 6: - entity = newEntity(60, 1, map.entities, nullptr); //Chair. - setSpriteAttributes(entity, nullptr, nullptr); - break; //Chair + } + } + else + { + switch ( map_rng.rand() % 7 ) + { + case 0: + entity = newEntity(12, 1, map.entities, nullptr); //Firecamp. + break; //Firecamp + case 1: + entity = newEntity(14, 1, map.entities, nullptr); //Fountain. + break; //Fountain + case 2: + entity = newEntity(15, 1, map.entities, nullptr); //Sink. + break; //Sink + case 3: + entity = newEntity(21, 1, map.entities, nullptr); //Chest. + setSpriteAttributes(entity, nullptr, nullptr); + entity->chestLocked = -1; + break; //Chest + case 4: + entity = newEntity(39, 1, map.entities, nullptr); //Tomb. + break; //Tomb + case 5: + entity = newEntity(59, 1, map.entities, nullptr); //Table. + setSpriteAttributes(entity, nullptr, nullptr); + break; //Table + case 6: + entity = newEntity(60, 1, map.entities, nullptr); //Chair. + setSpriteAttributes(entity, nullptr, nullptr); + break; //Chair + } } } else @@ -4220,33 +4803,56 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple // decorations if ( (map_rng.rand() % 4 == 0 || (currentlevel <= 10 && !customTrapsForMapInUse)) && strcmp(map.name, "Hell") ) { - switch ( map_rng.rand() % 7 ) + if ( !strncmp(map.filename, "fortress", 8) ) { + switch ( map_rng.rand() % 4 ) + { case 0: - entity = newEntity(12, 1, map.entities, nullptr); //Firecamp entity. + entity = newEntity(12, 1, map.entities, nullptr); //Firecamp. break; //Firecamp case 1: - entity = newEntity(14, 1, map.entities, nullptr); //Fountain entity. + entity = newEntity(14, 1, map.entities, nullptr); //Fountain. break; //Fountain case 2: - entity = newEntity(15, 1, map.entities, nullptr); //Sink entity. + entity = newEntity(15, 1, map.entities, nullptr); //Sink. break; //Sink case 3: - entity = newEntity(21, 1, map.entities, nullptr); //Chest entity. + entity = newEntity(21, 1, map.entities, nullptr); //Chest. setSpriteAttributes(entity, nullptr, nullptr); entity->chestLocked = -1; break; //Chest - case 4: - entity = newEntity(39, 1, map.entities, nullptr); //Tomb entity. - break; //Tomb - case 5: - entity = newEntity(59, 1, map.entities, nullptr); //Table entity. - setSpriteAttributes(entity, nullptr, nullptr); - break; //Table - case 6: - entity = newEntity(60, 1, map.entities, nullptr); //Chair entity. - setSpriteAttributes(entity, nullptr, nullptr); - break; //Chair + } + } + else + { + switch ( map_rng.rand() % 7 ) + { + case 0: + entity = newEntity(12, 1, map.entities, nullptr); //Firecamp entity. + break; //Firecamp + case 1: + entity = newEntity(14, 1, map.entities, nullptr); //Fountain entity. + break; //Fountain + case 2: + entity = newEntity(15, 1, map.entities, nullptr); //Sink entity. + break; //Sink + case 3: + entity = newEntity(21, 1, map.entities, nullptr); //Chest entity. + setSpriteAttributes(entity, nullptr, nullptr); + entity->chestLocked = -1; + break; //Chest + case 4: + entity = newEntity(39, 1, map.entities, nullptr); //Tomb entity. + break; //Tomb + case 5: + entity = newEntity(59, 1, map.entities, nullptr); //Table entity. + setSpriteAttributes(entity, nullptr, nullptr); + break; //Table + case 6: + entity = newEntity(60, 1, map.entities, nullptr); //Chair entity. + setSpriteAttributes(entity, nullptr, nullptr); + break; //Chair + } } } else @@ -4482,114 +5088,76 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple int dir; int id = -1; }; - auto findBreakables = EditorEntityData_t::colliderRandomGenPool.find(map.name); - if ( findBreakables == EditorEntityData_t::colliderRandomGenPool.end() ) - { - numBreakables = 0; - } - int numOpenAreaBreakables = 0; - std::vector breakableLocations; - if ( findBreakables->first == "Underworld" ) - { - numOpenAreaBreakables = 10; - - std::vector picked; - while ( numOpenAreaBreakables > 0 && underworldEmptyTiles.size() > 0 ) - { - int pick = map_rng.rand() % underworldEmptyTiles.size(); - picked.push_back(underworldEmptyTiles[pick]); - underworldEmptyTiles.erase(underworldEmptyTiles.begin() + pick); - } + static ConsoleVariable cvar_debug_station_spawn("/debug_station_spawn", false); + if ( treasure_room_generator.bForceStationSpawnForCurrentFloor(secretlevelexit) ) + { + bool* possibleLocationsStations = (bool*)malloc(sizeof(bool) * map.width * map.height); + memcpy(possibleLocationsStations, possiblelocations, map.width * map.height * sizeof(bool)); + int numpossibleStationLocations = numpossiblelocations; - for ( auto& coord : picked ) + std::vector stationLocations; + std::set obstacleLocations; + for ( int c = 0; c < std::min(30, numpossibleStationLocations); ++c ) { - int x = (coord) % 1000; - int y = (coord) / 1000; - - if ( numOpenAreaBreakables > 0 ) + // choose a random location from those available + pickedlocation = map_rng.rand() % numpossibleStationLocations; + i = -1; + int x = 0; + int y = 0; + while ( 1 ) { - int obstacles = 0; - // add some hanging cages - for ( int x2 = -1; x2 <= 1; x2++ ) + if ( possibleLocationsStations[y + x * map.height] == true ) { - for ( int y2 = -1; y2 <= 1; y2++ ) + ++i; + if ( i == pickedlocation ) { - if ( x2 == 0 && y2 == 0 ) { continue; } - int checkx = x + x2; - int checky = y + y2; - if ( checkx >= 0 && checkx < map.width ) - { - if ( checky >= 0 && checky < map.height ) - { - int index = (checky)*MAPLAYERS + (checkx)*MAPLAYERS * map.height; - if ( map.tiles[index] ) - { - ++obstacles; - break; - } - if ( checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, false, true, false) ) - { - ++obstacles; - break; - } - } - } + break; } } - - if ( obstacles == 0 ) + ++x; + if ( x >= map.width ) { - breakableLocations.push_back(BreakableNode_t(1, x, y, map_rng.rand() % 4, - map_rng.rand() % 2 ? 14 : 40)); // random dir, hanging cage ids - --numOpenAreaBreakables; - - if ( possiblelocations[y + x * map.height] ) + x = 0; + ++y; + if ( y >= map.height ) { - possiblelocations[y + x * map.height] = false; - --numpossiblelocations; + y = 0; } } } - } - } - for ( c = 0; c < std::min(numBreakables, numpossiblelocations); ++c ) - { - // choose a random location from those available - pickedlocation = map_rng.rand() % numpossiblelocations; - i = -1; - int x = 0; - int y = 0; - bool skipPossibleLocationsDecrement = false; - while ( 1 ) - { - if ( possiblelocations[y + x * map.height] == true ) + if ( decorationexcludelocations[x + y * map.width] + || treasureRoomLocations[x + y * map.width] + || secretlevelexittile[y + x * map.height] ) { - ++i; - if ( i == pickedlocation ) - { - break; - } + --c; + possibleLocationsStations[y + x * map.height] = false; + numpossibleStationLocations--; + continue; } - ++x; - if ( x >= map.width ) + + std::set walls; + std::set corners; + int maxContinuousTiles = 0; + + std::vector> coordsToCheck = // spiral { - x = 0; - ++y; - if ( y >= map.height ) - { - y = 0; - } - } - } + { -1, -1 }, + { 0, -1 }, + { 1, -1 }, + { 1, 0 }, + { 1, 1 }, + { 0, 1 }, + {-1, 1 }, + {-1, 0 } + }; - std::set walls; - std::set corners; - for ( int x2 = -1; x2 <= 1; x2++ ) - { - for ( int y2 = -1; y2 <= 1; y2++ ) + int numContinuousTiles = 0; + for ( auto& pair : coordsToCheck ) { + int x2 = pair.first; + int y2 = pair.second; if ( x2 == 0 && y2 == 0 ) { continue; @@ -4601,7 +5169,7 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple { if ( checky >= 0 && checky < map.height ) { - int index = (checky) * MAPLAYERS + (checkx) * MAPLAYERS * map.height; + int index = (checky)*MAPLAYERS + (checkx)*MAPLAYERS * map.height; if ( map.tiles[OBSTACLELAYER + index] ) { if ( (x2 == -1 && y2 == -1) || (x2 == 1 && y2 == 1) @@ -4613,970 +5181,2078 @@ int generateDungeon(char* levelset, Uint32 seed, std::tuple { walls.insert(checkx + checky * 1000); } + numContinuousTiles = 0; + } + else + { + ++numContinuousTiles; + maxContinuousTiles = std::max(maxContinuousTiles, numContinuousTiles); } } + else + { + numContinuousTiles = 0; + } + } + else + { + numContinuousTiles = 0; } } - } - - possiblelocations[y + x * map.height] = false; - numpossiblelocations--; - //if ( walls.size() == 0 && findBreakables->first == "Underworld" && numOpenAreaBreakables > 0 ) - //{ - // int obstacles = 0; - // // add some hanging cages - // for ( int x2 = -1; x2 <= 1; x2++ ) - // { - // for ( int y2 = -1; y2 <= 1; y2++ ) - // { - // if ( x2 == 0 && y2 == 0 ) { continue; } - // int checkx = x + x2; - // int checky = y + y2; - // if ( checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, false, true, false) ) - // { - // ++obstacles; - // break; - // } - // } - // } + possibleLocationsStations[y + x * map.height] = false; + numpossibleStationLocations--; - // if ( obstacles == 0 ) - // { - // breakableLocations.push(BreakableNode_t(1, x, y, map_rng.rand() % 4, - // map_rng.rand() % 2 ? 14 : 40)); // random dir, low prio, hanging cage ids - // --numOpenAreaBreakables; - // } - // --c; - // continue; - //} - - if ( walls.size() == 0 || walls.size() >= 4 ) - { - // try again - --c; - continue; - } + if ( walls.size() == 2 ) + { + int tmpx1 = 0; + int tmpy1 = 0; + int tmpx2 = 0; + int tmpy2 = 0; + int index = -1; + for ( auto val : walls ) + { + ++index; + if ( index == 0 ) + { + tmpx1 = val % 1000; + tmpy1 = val / 1000; + } + else if ( index == 1 ) + { + tmpx2 = val % 1000; + tmpy2 = val / 1000; + } + } + if ( tmpx1 == tmpx2 || tmpy1 == tmpy2 ) + { + // try again, can't be on same axis, needs to be corner config + --c; + continue; + } + } + if ( !((walls.size() == 1 && corners.size() == 2 && maxContinuousTiles >= 5) + || (walls.size() == 1 && corners.size() == 0 && maxContinuousTiles >= 7) + || (walls.size() == 2 && maxContinuousTiles >= 3) + || (walls.size() == 3)) ) + { + // try again + --c; + continue; + } - std::set freespaces; - for ( int x2 = -1; x2 <= 1; x2++ ) - { - for ( int y2 = -1; y2 <= 1; y2++ ) + std::set freespaces; + numContinuousTiles = 0; + maxContinuousTiles = 0; + int freeAxisSpaces = 0; + for ( auto& pair : coordsToCheck ) { + int x2 = pair.first; + int y2 = pair.second; if ( x2 == 0 && y2 == 0 ) { continue; } + if ( walls.size() == 3 ) + { + // ignore corners + if ( x2 != 0 && y2 != 0 ) + { + numContinuousTiles = 0; + continue; + } + } + int checkx = x + x2; int checky = y + y2; - if ( walls.find(checkx + checky * 1000) != walls.end() + if ( walls.find(checkx + checky * 1000) != walls.end() || corners.find(checkx + checky * 1000) != corners.end() ) { + numContinuousTiles = 0; continue; } + if ( checkx >= 0 && checkx < map.width ) { if ( checky >= 0 && checky < map.height ) { - int index = (checky) * MAPLAYERS + (checkx) * MAPLAYERS * map.height; + int index = (checky)*MAPLAYERS + (checkx)*MAPLAYERS * map.height; if ( swimmingtiles[map.tiles[index]] || lavatiles[map.tiles[index]] ) { + numContinuousTiles = 0; continue; } - if ( !checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, false, false) ) + if ( obstacleLocations.find(checkx + checky * 1000) != obstacleLocations.end() ) + { + numContinuousTiles = 0; + continue; + } + else if ( !checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, false, false) ) { + obstacleLocations.insert(checkx + checky * 1000); freespaces.insert(checkx + checky * 1000); + if ( x2 == 0 || y2 == 0 ) + { + ++freeAxisSpaces; + } + ++numContinuousTiles; + maxContinuousTiles = std::max(maxContinuousTiles, numContinuousTiles); + } + else + { + numContinuousTiles = 0; } } + else + { + numContinuousTiles = 0; + } + } + else + { + numContinuousTiles = 0; } } - } - bool foundSpace = false; - if ( (walls.size() == 1 && freespaces.size() >= 5) - || (walls.size() == 2 && freespaces.size() >= 3) - || (walls.size() == 3 && freespaces.size() >= 1) ) - { - int numIslands = 0; - std::set reachedTiles; - std::map> islands; - for ( auto it = freespaces.begin(); it != freespaces.end(); ++it ) + if ( walls.size() == 2 && freeAxisSpaces != 2 ) // need 2 axis aligned spaces { - if ( reachedTiles.find(*it) == reachedTiles.end() ) + freespaces.clear(); + } + + bool foundSpace = false; + if ( (walls.size() == 1 && corners.size() == 2 && maxContinuousTiles >= 5 && freespaces.size() >= 5) + || (walls.size() == 1 && corners.size() == 0 && maxContinuousTiles >= 7 && freespaces.size() >= 7) + || (walls.size() == 2 && maxContinuousTiles >= 3 && freespaces.size() >= 3) + || (walls.size() == 3 && freespaces.size() > 0) ) + { + std::vector dirs; + if ( walls.size() == 3 ) { - // new island - std::queue frontier; - frontier.push(*it); - reachedTiles.insert(*it); - while ( !frontier.empty() ) + if ( walls.find((x + 1) + (y + 0) * 1000) == walls.end() ) { - auto currentKey = frontier.front(); - frontier.pop(); - - const int ix = (currentKey) % 1000; - const int iy = (currentKey) / 1000; - - islands[numIslands].insert(currentKey); - - int checkKey = (ix + 1) + ((iy) * 1000); - if ( freespaces.find(checkKey) != freespaces.end() - && reachedTiles.find(checkKey) == reachedTiles.end() ) - { - frontier.push(checkKey); - reachedTiles.insert(checkKey); - } - checkKey = (ix - 1) + ((iy) * 1000); - if ( freespaces.find(checkKey) != freespaces.end() - && reachedTiles.find(checkKey) == reachedTiles.end() ) - { - frontier.push(checkKey); - reachedTiles.insert(checkKey); - } - checkKey = (ix) + ((iy + 1) * 1000); - if ( freespaces.find(checkKey) != freespaces.end() - && reachedTiles.find(checkKey) == reachedTiles.end() ) - { - frontier.push(checkKey); - reachedTiles.insert(checkKey); - } - checkKey = (ix) + ((iy - 1) * 1000); - if ( freespaces.find(checkKey) != freespaces.end() - && reachedTiles.find(checkKey) == reachedTiles.end() ) - { - frontier.push(checkKey); - reachedTiles.insert(checkKey); - } + dirs.push_back(0); } - if ( !islands[numIslands].empty() ) + else if ( walls.find((x - 1) + (y + 0) * 1000) == walls.end() ) { - ++numIslands; + dirs.push_back(4); + } + else if ( walls.find((x + 0) + (y + 1) * 1000) == walls.end() ) + { + dirs.push_back(2); + } + else if ( walls.find((x + 0) + (y - 1) * 1000) == walls.end() ) + { + dirs.push_back(6); } } - } - - for ( auto& island : islands ) - { - if ( (walls.size() == 1 && island.second.size() >= 5) - || (walls.size() == 2 && island.second.size() >= 3) - || (walls.size() == 3 && island.second.size() >= 1) ) + else { - std::vector dirs; - if ( walls.size() == 3 ) + if ( walls.find((x + 1) + (y + 0) * 1000) != walls.end() ) { - if ( walls.find((x + 1) + (y + 0) * 1000) == walls.end() ) - { - dirs.push_back(0); - } - else if ( walls.find((x - 1) + (y + 0) * 1000) == walls.end() ) - { - dirs.push_back(4); - } - else if ( walls.find((x + 0) + (y + 1) * 1000) == walls.end() ) - { - dirs.push_back(2); - } - else if ( walls.find((x + 0) + (y - 1) * 1000) == walls.end() ) - { - dirs.push_back(6); - } + dirs.push_back(4); } - else + else if ( walls.find((x - 1) + (y + 0) * 1000) != walls.end() ) { - if ( walls.find((x + 1) + (y + 0) * 1000) != walls.end() ) - { - dirs.push_back(4); - } - else if ( walls.find((x - 1) + (y + 0) * 1000) != walls.end() ) - { - dirs.push_back(0); - } - else if ( walls.find((x + 0) + (y + 1) * 1000) != walls.end() ) - { - dirs.push_back(6); - } - else if ( walls.find((x + 0) + (y - 1) * 1000) != walls.end() ) - { - dirs.push_back(2); - } + dirs.push_back(0); + } + else if ( walls.find((x + 0) + (y + 1) * 1000) != walls.end() ) + { + dirs.push_back(6); + } + else if ( walls.find((x + 0) + (y - 1) * 1000) != walls.end() ) + { + dirs.push_back(2); } + } + if ( dirs.size() ) + { int picked = dirs[map_rng.rand() % dirs.size()]; - breakableLocations.push_back(BreakableNode_t(walls.size(), x, y, picked)); + + int priority = 10; + if ( walls.size() == 1 && corners.size() == 2 ) + { + priority = 15; + } + else if ( walls.size() == 3 ) + { + priority = 15; + } + stationLocations.push_back(BreakableNode_t(priority, x, y, picked)); foundSpace = true; - break; } } - } - if ( !foundSpace ) - { - --c; + if ( !foundSpace ) + { + --c; + continue; + } } - } - int breakableGoodies = breakableLocations.size() * 80 / 100; - int breakableMonsters = 0; - int breakableMonsterLimit = 2 + (currentlevel / LENGTH_OF_LEVEL_REGION) * (1 + map_rng.rand() % 2); - static ConsoleVariable cvar_breakableMonsterLimit("/breakable_monster_limit", 0); - if ( svFlags & SV_FLAG_CHEATS ) - { - breakableMonsterLimit = std::max(*cvar_breakableMonsterLimit, breakableMonsterLimit); - } - if ( findBreakables != EditorEntityData_t::colliderRandomGenPool.end() && findBreakables->second.size() > 0 && breakableGoodies > 0 ) - { - int breakableItemsFromGround = 0; - std::vector chances; - std::vector ids; - for ( auto& pair : findBreakables->second ) - { - ids.push_back(pair.first); - chances.push_back(pair.second); - } - Monster lastMonsterEvent = NOTHING; - while ( !breakableLocations.empty() ) + free(possibleLocationsStations); + possibleLocationsStations = nullptr; + + while ( !stationLocations.empty() ) { - int maxNumWalls = 0; - for ( auto& b : breakableLocations ) - { - maxNumWalls = std::max(b.walls, maxNumWalls); - } std::vector posChances; int pickedPos = 0; - for ( auto& b : breakableLocations ) + for ( auto& b : stationLocations ) { - posChances.push_back(b.walls == maxNumWalls ? 1 : 0); + posChances.push_back(b.walls); } pickedPos = map_rng.discrete(posChances.data(), posChances.size()); - auto& top = breakableLocations.at(pickedPos); + auto& top = stationLocations.at(pickedPos); int x = top.x; int y = top.y; - Entity* breakable = newEntity(179, 1, map.entities, nullptr); - breakable->x = x * 16.0; - breakable->y = y * 16.0; - breakable->colliderDecorationRotation = top.dir; - - if ( top.id >= 0 ) - { - breakable->colliderDamageTypes = top.id; - } - else + auto& station = secretlevel ? treasure_room_generator.station_secret_floors : treasure_room_generator.station_floors; + if ( station[currentlevel] == "cauldron" ) { - int picked = map_rng.discrete(chances.data(), chances.size()); - breakable->colliderDamageTypes = ids[picked]; - } + Entity* stationEntity = newEntity(300, 1, map.entities, nullptr); // cauldron + stationEntity->x = x * 16.0; + stationEntity->y = y * 16.0; + stationEntity->yaw = top.dir / 2; - bool monsterEventExists = false; - auto findData = EditorEntityData_t::colliderData.find(breakable->colliderDamageTypes); - if ( findData != EditorEntityData_t::colliderData.end() ) - { - auto findMap = findData->second.hideMonsters.find(map.name); - if ( findMap != findData->second.hideMonsters.end() ) + stationLocations.erase(stationLocations.begin() + pickedPos); + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; + + if ( *cvar_debug_station_spawn ) { - if ( findMap->second.size() > 0 ) + if ( (svFlags & SV_FLAG_CHEATS) ) { - for ( auto m : findMap->second ) - { - if ( m > NOTHING && m < NUMMONSTERS ) - { - monsterEventExists = true; - } - } + messagePlayer(clientnum, MESSAGE_DEBUG, "[STATIONS]: %s generated at x:%d y:%d", station[currentlevel].c_str(), x, y); } } } - - if ( breakableGoodies > 0 ) + else if ( station[currentlevel] == "workbench" ) { - --breakableGoodies; - - int index = (y) * MAPLAYERS + (x) * MAPLAYERS * map.height; + Entity* stationEntity = newEntity(301, 1, map.entities, nullptr); // workbench + stationEntity->x = x * 16.0; + stationEntity->y = y * 16.0; + stationEntity->yaw = top.dir / 2; - static ConsoleVariable cvar_breakableMonsterChance("/breakable_monster_chance", 10); + stationLocations.erase(stationLocations.begin() + pickedPos); + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; - if ( !map.tiles[index] && map_rng.rand() % 2 == 1 ) + if ( *cvar_debug_station_spawn ) { - // nothing over pits 50% + if ( (svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_DEBUG, "[STATIONS]: %s generated at x:%d y:%d", station[currentlevel].c_str(), x, y); + } } - else if ( (breakableMonsters < breakableMonsterLimit && monsterEventExists - && map_rng.rand() % ((svFlags & SV_FLAG_CHEATS) ? std::min(10, *cvar_breakableMonsterChance) : 10) == 0) - && map.monsterexcludelocations[x + y * map.width] == false ) // 10% monster inside + } + else + { + if ( *cvar_debug_station_spawn ) { - Monster monsterEvent = NOTHING; - auto findMap = findData->second.hideMonsters.find(map.name); - if ( findMap != findData->second.hideMonsters.end() ) + if ( (svFlags & SV_FLAG_CHEATS) ) { - if ( findMap->second.size() > 0 ) + messagePlayer(clientnum, MESSAGE_DEBUG, "[STATIONS]: nothing generated at x:%d y:%d", x, y); + } + } + } + break; + } + } + + auto findBreakables = EditorEntityData_t::colliderRandomGenPool.find(map.name); + if ( findBreakables == EditorEntityData_t::colliderRandomGenPool.end() ) + { + numBreakables = 0; + } + int numOpenAreaBreakables = 0; + std::vector breakableLocations; + if ( findBreakables != EditorEntityData_t::colliderRandomGenPool.end() + && findBreakables->first == "The Fortress" ) + { + numOpenAreaBreakables = 10; + + int numLeaves = 10; + int numShrubs = 10; + int numMushrooms = 5; + int numClosedAreaBreakables = 0; + std::vector goodSpots; + for ( int x = 0; x < map.width; ++x ) + { + for ( int y = 0; y < map.height; ++y ) + { + if ( possiblelocations[y + x * map.height] == true ) + { + goodSpots.push_back(x + 10000 * y); + } + } + } + + std::set allTrees; + std::set allMushrooms; + for ( auto node = map.entities->first; node; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity->sprite == 179 && (entity->colliderDecorationModel == 1607 || entity->colliderDecorationModel == 1610) ) + { + int coord = ((int)(entity->x / 16)) + ((int)(entity->y / 16)) * 10000; + allTrees.insert(coord); + } + else if ( entity->sprite == 179 && (entity->colliderDecorationModel == 1611 || entity->colliderDecorationModel == 1612) ) + { + int coord = ((int)(entity->x / 16)) + ((int)(entity->y / 16)) * 10000; + allMushrooms.insert(coord); + } + } + } + + for ( int c = 0; c < (int)goodSpots.size() + && (numOpenAreaBreakables > 0 || numLeaves > 0 || numShrubs > 0 || numMushrooms > 0 || numClosedAreaBreakables > 0); ++c ) + { + // choose a random location from those available + int pick = map_rng.rand() % goodSpots.size(); + int x = goodSpots[pick] % 10000; + int y = goodSpots[pick] / 10000; + + goodSpots.erase(goodSpots.begin() + pick); + + int treesNearby = 0; + int mushroomsNearby = 0; + int obstacles = 0; + int numWalls = 0; + for ( int x2 = -1; x2 <= 1; x2++ ) + { + for ( int y2 = -1; y2 <= 1; y2++ ) + { + if ( x2 == 0 && y2 == 0 ) { continue; } + int checkx = x + x2; + int checky = y + y2; + if ( checkx >= 0 && checkx < map.width ) + { + if ( checky >= 0 && checky < map.height ) { - std::vector chances; - bool avoidLastMonster = false; - for ( auto m : findMap->second ) + int index = (checky)*MAPLAYERS + (checkx)*MAPLAYERS * map.height; + if ( !map.tiles[index] || swimmingtiles[map.tiles[index]] || lavatiles[map.tiles[index]] ) { - chances.push_back(1); - if ( lastMonsterEvent != NOTHING && m != lastMonsterEvent ) - { - avoidLastMonster = true; - } + ++obstacles; } - if ( avoidLastMonster ) + if ( map.tiles[OBSTACLELAYER + index] ) { - for ( size_t i = 0; i < chances.size(); ++i ) - { - if ( findMap->second[i] == lastMonsterEvent ) - { - chances[i] = 0; - } - } + ++numWalls; } - int pickIndex = map_rng.discrete(chances.data(), chances.size()); - int picked = findMap->second[pickIndex]; - if ( picked > NOTHING && picked < NUMMONSTERS ) + if ( checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, false, true, false) ) { - monsterEvent = (Monster)picked; - lastMonsterEvent = monsterEvent; + if ( allTrees.find(checkx + checky * 10000) != allTrees.end() ) + { + ++treesNearby; + } + if ( allMushrooms.find(checkx + checky * 10000) != allMushrooms.end() ) + { + ++mushroomsNearby; + } + ++obstacles; } } } + } + } - if ( (svFlags & SV_FLAG_TRAPS) ) + if ( obstacles == 0 ) + { + if ( numOpenAreaBreakables > 0 ) + { + // add some mushrooms + int id = EditorEntityData_t::colliderNameIndexes["mushroom_spell_common"]; + if ( map_rng.rand() % 5 == 0 ) { - if ( map_rng.rand() % 2 == 0 ) - { - breakable->colliderHideMonster = monsterEvent; - } - else - { - breakable->colliderHideMonster = 1000 + monsterEvent; - } + id = EditorEntityData_t::colliderNameIndexes["mushroom_spell_fragile"]; + } + else if ( map_rng.rand() % 5 == 0 ) + { + id = EditorEntityData_t::colliderNameIndexes["mushroom_nospell"]; + } + + breakableLocations.push_back(BreakableNode_t(1, x, y, map_rng.rand() % 4, + id)); // random dir + --numOpenAreaBreakables; + if ( possiblelocations[y + x * map.height] ) + { + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; } - ++breakableMonsters; } - else if ( !map.tiles[index] || map_rng.rand() % 2 == 1 ) // 50% chance (or floating over a pit is just gold) + else if ( numLeaves > 0 ) { - std::vector genGold; - int numGold = 3 + map_rng.rand() % 3; - while ( numGold > 0 ) + --numLeaves; + Entity* leaf = newEntity(254, 1, map.entities, nullptr); + leaf->x = x * 16.0; + leaf->y = y * 16.0; + if ( possiblelocations[y + x * map.height] ) { - --numGold; - Entity* entity = newEntity(9, 1, map.entities, nullptr); // gold - genGold.push_back(entity); - entity->x = breakable->x; - entity->y = breakable->y; - entity->goldAmount = 2 + map_rng.rand() % 3; - entity->flags[INVISIBLE] = true; - entity->yaw = breakable->yaw; - entity->goldInContainer = breakable->getUID(); - breakable->colliderContainedEntity = entity->getUID(); - numGenGold++; + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; } - int index = -1; - for ( auto gold : genGold ) + } + } + else if ( ((treesNearby > 0 && numShrubs > 0) || (mushroomsNearby > 0 && numMushrooms > 0)) && obstacles <= 3 ) + { + if ( treesNearby > 0 ) + { + breakableLocations.push_back(BreakableNode_t(1, x, y, map_rng.rand() % 4, + EditorEntityData_t::colliderNameIndexes["shrub"])); + --numShrubs; + if ( possiblelocations[y + x * map.height] ) { - ++index; - gold->yaw += (index * PI) / genGold.size(); + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; } } - else + else if ( mushroomsNearby > 0 ) { - if ( itemsGeneratedList.size() > 10 && breakableItemsFromGround < 6 ) + int id = EditorEntityData_t::colliderNameIndexes["mushroom_spell_common"]; + if ( map_rng.rand() % 5 == 0 ) { - // steal an item from the ground - size_t index = map_rng.rand() % itemsGeneratedList.size(); - Uint32 uid = itemsGeneratedList.at(index); - itemsGeneratedList.erase(itemsGeneratedList.begin() + index); - if ( Entity* entity = uidToEntity(uid) ) - { - entity->x = breakable->x; - entity->y = breakable->y; - entity->flags[INVISIBLE] = true; - entity->itemContainer = breakable->getUID(); - entity->yaw = breakable->yaw; - breakable->colliderContainedEntity = entity->getUID(); - ++breakableItemsFromGround; - } + id = EditorEntityData_t::colliderNameIndexes["mushroom_spell_fragile"]; } - else + else if ( map_rng.rand() % 5 == 0 ) { - Entity* entity = newEntity(8, 1, map.entities, nullptr); // item - setSpriteAttributes(entity, nullptr, nullptr); - entity->x = breakable->x; - entity->y = breakable->y; - entity->flags[INVISIBLE] = true; - entity->itemContainer = breakable->getUID(); - entity->yaw = breakable->yaw; - breakable->colliderContainedEntity = entity->getUID(); - numGenItems++; + id = EditorEntityData_t::colliderNameIndexes["mushroom_nospell"]; + } + breakableLocations.push_back(BreakableNode_t(1, x, y, map_rng.rand() % 4, + id)); + --numMushrooms; + if ( possiblelocations[y + x * map.height] ) + { + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; } } } - - if ( false ) + else if ( obstacles == 1 && numLeaves > 0 ) { - //messagePlayer(0, MESSAGE_DEBUG, "pick: %d | x: %d y: %d", picked, x, y); - Entity* ent = newEntity(245, 0, map.entities, nullptr); - //ent->behavior = &actBoulder; - ent->x = x * 16.0 + 8; - ent->y = y * 16.0 + 8; - ent->z = 24.0; - ent->flags[PASSABLE] = true; + --numLeaves; + Entity* leaf = newEntity(254, 1, map.entities, nullptr); + leaf->x = x * 16.0; + leaf->y = y * 16.0; + if ( possiblelocations[y + x * map.height] ) + { + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; + } } - breakableLocations.erase(breakableLocations.begin() + pickedPos); + /*else if ( numClosedAreaBreakables > 0 && obstacles > 3 && (numWalls >= 2) ) + { + breakableLocations.push_back(BreakableNode_t(1, x, y, map_rng.rand() % 4, + EditorEntityData_t::colliderNameIndexes["stump"])); + --numClosedAreaBreakables; + if ( possiblelocations[y + x * map.height] ) + { + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; + } + }*/ } } - - if ( darkmap && map.skybox == 0 ) + if ( findBreakables != EditorEntityData_t::colliderRandomGenPool.end() + && findBreakables->first == "Underworld" ) { - std::vector>> batAreasGood; - std::vector>> batAreasOk; - for ( int x = 1; x < map.width - 1; ++x ) + numOpenAreaBreakables = 10; + + std::vector picked; + while ( numOpenAreaBreakables > 0 && underworldEmptyTiles.size() > 0 ) { - for ( int y = 1; y < map.height - 1; ++y ) + int pick = map_rng.rand() % underworldEmptyTiles.size(); + picked.push_back(underworldEmptyTiles[pick]); + + underworldEmptyTiles.erase(underworldEmptyTiles.begin() + pick); + } + + for ( auto& coord : picked ) + { + int x = (coord) % 1000; + int y = (coord) / 1000; + + if ( numOpenAreaBreakables > 0 ) { - if ( possiblelocations[y + x * map.height] ) + int obstacles = 0; + // add some hanging cages + for ( int x2 = -1; x2 <= 1; x2++ ) { - std::vector testAreas = { - (x - 1) + 1000 * (y + 0), - (x + 1) + 1000 * (y + 0), - (x + 0) + 1000 * (y + 1), - (x + 0) + 1000 * (y - 1), - (x + 0) + 1000 * (y + 0), - (x + 1) + 1000 * (y + 1), - (x - 1) + 1000 * (y + 1), - (x + 1) + 1000 * (y - 1), - (x - 1) + 1000 * (y - 1) - }; - std::map> goodSpots; - int openCeilings = 0; - for ( auto coord : testAreas ) + for ( int y2 = -1; y2 <= 1; y2++ ) { - int tx = coord % 1000; - int ty = coord / 1000; - if ( tx >= 1 && tx < map.width - 1 && ty >= 1 && ty < map.height - 1 ) + if ( x2 == 0 && y2 == 0 ) { continue; } + int checkx = x + x2; + int checky = y + y2; + if ( checkx >= 0 && checkx < map.width ) { - if ( possiblelocations[ty + tx * map.height] ) + if ( checky >= 0 && checky < map.height ) { - int mapIndex = (ty)*MAPLAYERS + (tx)*MAPLAYERS * map.height; - if ( !map.tiles[OBSTACLELAYER + mapIndex] ) + int index = (checky)*MAPLAYERS + (checkx)*MAPLAYERS * map.height; + if ( map.tiles[index] ) { - if ( !map.tiles[(MAPLAYERS - 1) + mapIndex] ) - { - ++openCeilings; - goodSpots[0].push_back(coord); - } - else - { - goodSpots[1].push_back(coord); - } + ++obstacles; + break; + } + if ( checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, false, true, false) ) + { + ++obstacles; + break; } } } } - if ( openCeilings >= 5 ) - { - batAreasGood.push_back(goodSpots); - } - else if ( (goodSpots[0].size() + goodSpots[1].size()) >= 5 ) - { - batAreasOk.push_back(goodSpots); - } } - } - } - std::unordered_set visited; - std::vector previousAreas; - int numBatAreas = std::max(2, std::min(5, 1 + (currentlevel / LENGTH_OF_LEVEL_REGION))); - while ( numBatAreas > 0 ) - { - if ( batAreasGood.size() == 0 && batAreasOk.size() == 0 ) - { - break; - } - - auto& areas = batAreasGood.size() > 0 ? batAreasGood : batAreasOk; - if ( areas.size() > 0 ) - { - size_t picked = map_rng.rand() % areas.size(); - auto& coords = areas[picked]; - - bool skip = false; - for ( auto coord : coords[0] ) - { - if ( visited.find(coord) != visited.end() ) - { - // no good - skip = true; - } - else - { - visited.insert(coord); - } - } - for ( auto coord : coords[1] ) - { - if ( visited.find(coord) != visited.end() ) - { - // no good - skip = true; - } - else - { - visited.insert(coord); - } - } - - int currentCoord = 0; - if ( coords[0].size() > 0 ) - { - currentCoord = coords[0][0]; - } - else if ( coords[1].size() > 0 ) - { - currentCoord = coords[1][0]; - } - - int checkx = currentCoord % 1000; - int checky = currentCoord / 1000; - for ( auto previousCoord : previousAreas ) - { - int ox = previousCoord % 1000; - int oy = previousCoord / 1000; - - real_t dx, dy; - dx = checkx - ox; - dy = checky - oy; - if ( sqrt(dx * dx + dy * dy) < 8.0 ) // too close to other regions, within 8 tiles - { - skip = true; - break; - } - } - - if ( skip ) - { - areas.erase(areas.begin() + picked); - continue; - } - - if ( coords[0].size() > 0 ) - { - previousAreas.push_back(coords[0][0]); - } - else if ( coords[1].size() > 0 ) - { - previousAreas.push_back(coords[1][0]); - } - - int numSpawns = std::max(2, std::min(4, 1 + (currentlevel / LENGTH_OF_LEVEL_REGION))); - for ( size_t i = 0; i < (coords[0].size() + coords[1].size()) && numSpawns > 0; ++i ) + if ( obstacles == 0 ) { - auto coord = (i < coords[0].size()) ? coords[0][i] : coords[1][i - coords[0].size()]; - int tx = coord % 1000; - int ty = coord / 1000; + breakableLocations.push_back(BreakableNode_t(1, x, y, map_rng.rand() % 4, + map_rng.rand() % 2 ? 14 : 40)); // random dir, hanging cage ids + --numOpenAreaBreakables; + if ( possiblelocations[y + x * map.height] ) { - Entity* ent = newEntity(188, 0, map.entities, nullptr); - ent->x = tx * 16.0; - ent->y = ty * 16.0; + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; } - - //Entity* ent = newEntity(245, 0, map.entities, nullptr); - ////ent->behavior = &actBoulder; - //ent->x = tx * 16.0 + 8; - //ent->y = ty * 16.0 + 8; - //ent->z = 24.0; - //ent->flags[PASSABLE] = true; - visited.insert(coord); - --numSpawns; - - possiblelocations[ty + tx * map.height] = false; - --numpossiblelocations; } - - --numBatAreas; - - areas.erase(areas.begin() + picked); - continue; } } } - // on hell levels, lava doesn't bubble. helps performance - /*if( !strcmp(map.name,"Hell") ) { - for( node=map.entities->first; node!=NULL; node=node->next ) { - Entity *entity = (Entity *)node->element; - if( entity->sprite == 41 ) { // lava.png - entity->skill[4] = 1; // LIQUID_LAVANOBUBBLE = - } - } - }*/ - - free(possiblelocations); - free(firstroomtile); - free(subRoomName); - free(sublevelname); - list_FreeAll(&subRoomMapList); - list_FreeAll(&mapList); - list_FreeAll(&doorList); - - printlog("successfully generated a dungeon with %d rooms, %d monsters, %d gold, %d items, %d decorations.\n", roomcount, nummonsters, numGenGold, numGenItems, numGenDecorations); - //messagePlayer(0, "successfully generated a dungeon with %d rooms, %d monsters, %d gold, %d items, %d decorations.", roomcount, nummonsters, numGenGold, numGenItems, numGenDecorations); - return secretlevelexit; -} - -bool allowedGenerateMimicOnChest(int x, int y, map_t& map) -{ - if ( gameModeManager.getMode() == gameModeManager.GAME_MODE_TUTORIAL - || gameModeManager.getMode() == gameModeManager.GAME_MODE_TUTORIAL_INIT ) - { - return false; - } - if ( !(svFlags & SV_FLAG_TRAPS) ) + bool* possibleLocationsBreakables = (bool*)malloc(sizeof(bool) * map.width * map.height); + memcpy(possibleLocationsBreakables, possiblelocations, map.width * map.height * sizeof(bool)); + int numpossibleBreakableLocations = numpossiblelocations; + for ( c = 0; c < std::min(numBreakables, numpossibleBreakableLocations); ++c ) { - return false; - } - /*if ( map.trapexcludelocations ) - { - if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + // choose a random location from those available + pickedlocation = map_rng.rand() % numpossibleBreakableLocations; + i = -1; + int x = 0; + int y = 0; + while ( 1 ) { - if ( map.trapexcludelocations[x + y * map.width] ) + if ( possibleLocationsBreakables[y + x * map.height] == true ) { - return false; + ++i; + if ( i == pickedlocation ) + { + break; + } } - } - }*/ - return true; -} - -void debugMap(map_t* map) -{ - return; - if ( !map ) - { - return; - } - - std::set takenSlots; - for ( auto node = map->entities->first; node != nullptr; ) - { - Entity* postProcessEntity = (Entity*)node->element; - node = node->next; - if ( postProcessEntity ) - { - if ( postProcessEntity->behavior == &actItem && postProcessEntity->z > 4 ) + ++x; + if ( x >= map.width ) { - int x = (int)postProcessEntity->x >> 4; - int y = (int)postProcessEntity->y >> 4; - takenSlots.insert(x + y * 10000); + x = 0; + ++y; + if ( y >= map.height ) + { + y = 0; + } } } - } - for ( int x = 0; x < map->width; ++x ) - { - for ( int y = 0; y < map->height; ++y ) + std::set walls; + std::set corners; + for ( int x2 = -1; x2 <= 1; x2++ ) { - if ( takenSlots.find(x + y * 10000) != takenSlots.end() ) + for ( int y2 = -1; y2 <= 1; y2++ ) { - int numWalls = 0; - std::vector> coords = { - {x + 1, y}, - {x - 1, y}, - {x, y + 1}, - {x, y - 1} - }; - for ( auto& pair : coords ) + if ( x2 == 0 && y2 == 0 ) { - if ( pair.first >= 0 && pair.first < map->width ) + continue; + } + + int checkx = x + x2; + int checky = y + y2; + if ( checkx >= 0 && checkx < map.width ) + { + if ( checky >= 0 && checky < map.height ) { - if ( pair.second >= 0 && pair.second < map->height ) + int index = (checky) * MAPLAYERS + (checkx) * MAPLAYERS * map.height; + if ( map.tiles[OBSTACLELAYER + index] ) { - if ( map->tiles[pair.second * MAPLAYERS + pair.first * MAPLAYERS * map->height] ) // floor + if ( (x2 == -1 && y2 == -1) || (x2 == 1 && y2 == 1) + || (x2 == -1 && y2 == 1) || (x2 == 1 && y2 == -1) ) { - numWalls += map->tiles[OBSTACLELAYER + pair.second * MAPLAYERS + pair.first * MAPLAYERS * map->height] != 0 ? 1 : 0; + corners.insert(checkx + checky * 1000); + } + else + { + walls.insert(checkx + checky * 1000); } } } } - if ( numWalls > 0 ) - { - //Entity* ent = newEntity(245, 0, map->entities, nullptr); - ////ent->behavior = &actBoulder; - //ent->x = x * 16.0 + 8; - //ent->y = y * 16.0 + 8; - //ent->z = 24.0; - } - //int numObstacles = checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, true); } } - } - mapLevel2(0); - /*int num5x5s = 0; - // open area debugging tool - for ( int x = 0; x < map->width; ++x ) - { - for ( int y = 0; y < map->height; ++y ) - { - if ( takenSlots.find(x + y * 10000) == takenSlots.end() ) - { - if ( !map->tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map->height] - && !map->tiles[2 + y * MAPLAYERS + x * MAPLAYERS * map->height] ) - { - int numTiles = 0; - for ( int x1 = x; x1 < map->width && x1 < x + 5; ++x1 ) + possibleLocationsBreakables[y + x * map.height] = false; + numpossibleBreakableLocations--; + + //if ( walls.size() == 0 && findBreakables->first == "Underworld" && numOpenAreaBreakables > 0 ) + //{ + // int obstacles = 0; + // // add some hanging cages + // for ( int x2 = -1; x2 <= 1; x2++ ) + // { + // for ( int y2 = -1; y2 <= 1; y2++ ) + // { + // if ( x2 == 0 && y2 == 0 ) { continue; } + // int checkx = x + x2; + // int checky = y + y2; + // if ( checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, false, true, false) ) + // { + // ++obstacles; + // break; + // } + // } + // } + + // if ( obstacles == 0 ) + // { + // breakableLocations.push(BreakableNode_t(1, x, y, map_rng.rand() % 4, + // map_rng.rand() % 2 ? 14 : 40)); // random dir, low prio, hanging cage ids + // --numOpenAreaBreakables; + // } + // --c; + // continue; + //} + + if ( walls.size() == 0 || walls.size() >= 4 ) + { + // try again + --c; + continue; + } + + std::set freespaces; + for ( int x2 = -1; x2 <= 1; x2++ ) + { + for ( int y2 = -1; y2 <= 1; y2++ ) + { + if ( x2 == 0 && y2 == 0 ) { continue; } + int checkx = x + x2; + int checky = y + y2; + if ( walls.find(checkx + checky * 1000) != walls.end() + || corners.find(checkx + checky * 1000) != corners.end() ) + { + continue; + } + if ( checkx >= 0 && checkx < map.width ) + { + if ( checky >= 0 && checky < map.height ) { - for ( int y1 = y; y1 < map->height && y1 < y + 5; ++y1 ) + int index = (checky) * MAPLAYERS + (checkx) * MAPLAYERS * map.height; + if ( swimmingtiles[map.tiles[index]] || lavatiles[map.tiles[index]] ) { - if ( takenSlots.find(x1 + y1 * 10000) == takenSlots.end() ) - { - if ( !map->tiles[OBSTACLELAYER + y1 * MAPLAYERS + x1 * MAPLAYERS * map->height] - && !map->tiles[2 + y1 * MAPLAYERS + x1 * MAPLAYERS * map->height] ) - { - ++numTiles; - } - } + continue; + } + if ( !checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, false, false) ) + { + freespaces.insert(checkx + checky * 1000); } - } - if ( numTiles == 25 ) - { - ++num5x5s; - Entity* ent = newEntity(245, 0, map->entities, nullptr); - ent->behavior == &actBoulder; - ent->x = x * 16.0 + 8; - ent->y = y * 16.0 + 8; } } } } - } - messagePlayer(0, MESSAGE_DEBUG, "%d 5x5s", num5x5s);*/ -} -/*------------------------------------------------------------------------------- - - assignActions - - configures a map to be playable from a default state - --------------------------------------------------------------------------------*/ + bool foundSpace = false; + if ( (walls.size() == 1 && freespaces.size() >= 5) + || (walls.size() == 2 && freespaces.size() >= 3) + || (walls.size() == 3 && freespaces.size() >= 1) ) + { + int numIslands = 0; + std::set reachedTiles; + std::map> islands; + for ( auto it = freespaces.begin(); it != freespaces.end(); ++it ) + { + if ( reachedTiles.find(*it) == reachedTiles.end() ) + { + // new island + std::queue frontier; + frontier.push(*it); + reachedTiles.insert(*it); + while ( !frontier.empty() ) + { + auto currentKey = frontier.front(); + frontier.pop(); -void assignActions(map_t* map) -{ - bool itemsdonebefore = false; - Entity* vampireQuestChest = nullptr; + const int ix = (currentKey) % 1000; + const int iy = (currentKey) / 1000; - if ( map == nullptr ) - { - return; - } + islands[numIslands].insert(currentKey); - // update arachnophobia filter - arachnophobia_filter = GameplayPreferences_t::getGameConfigValue(GameplayPreferences_t::GOPT_ARACHNOPHOBIA); - colorblind_lobby = GameplayPreferences_t::getGameConfigValue(GameplayPreferences_t::GOPT_COLORBLIND); + int checkKey = (ix + 1) + ((iy) * 1000); + if ( freespaces.find(checkKey) != freespaces.end() + && reachedTiles.find(checkKey) == reachedTiles.end() ) + { + frontier.push(checkKey); + reachedTiles.insert(checkKey); + } + checkKey = (ix - 1) + ((iy) * 1000); + if ( freespaces.find(checkKey) != freespaces.end() + && reachedTiles.find(checkKey) == reachedTiles.end() ) + { + frontier.push(checkKey); + reachedTiles.insert(checkKey); + } + checkKey = (ix) + ((iy + 1) * 1000); + if ( freespaces.find(checkKey) != freespaces.end() + && reachedTiles.find(checkKey) == reachedTiles.end() ) + { + frontier.push(checkKey); + reachedTiles.insert(checkKey); + } + checkKey = (ix) + ((iy - 1) * 1000); + if ( freespaces.find(checkKey) != freespaces.end() + && reachedTiles.find(checkKey) == reachedTiles.end() ) + { + frontier.push(checkKey); + reachedTiles.insert(checkKey); + } + } + if ( !islands[numIslands].empty() ) + { + ++numIslands; + } + } + } - // add lava lights - for ( int y = 0; y < map->height; ++y ) - { - for ( int x = 0; x < map->width; ++x ) - { - if ( lavatiles[map->tiles[y * MAPLAYERS + x * MAPLAYERS * map->height]] ) + for ( auto& island : islands ) { - addLight(x, y, "lava"); + if ( (walls.size() == 1 && island.second.size() >= 5) + || (walls.size() == 2 && island.second.size() >= 3) + || (walls.size() == 3 && island.second.size() >= 1) ) + { + std::vector dirs; + if ( walls.size() == 3 ) + { + if ( walls.find((x + 1) + (y + 0) * 1000) == walls.end() ) + { + dirs.push_back(0); + } + else if ( walls.find((x - 1) + (y + 0) * 1000) == walls.end() ) + { + dirs.push_back(4); + } + else if ( walls.find((x + 0) + (y + 1) * 1000) == walls.end() ) + { + dirs.push_back(2); + } + else if ( walls.find((x + 0) + (y - 1) * 1000) == walls.end() ) + { + dirs.push_back(6); + } + } + else + { + if ( walls.find((x + 1) + (y + 0) * 1000) != walls.end() ) + { + dirs.push_back(4); + } + else if ( walls.find((x - 1) + (y + 0) * 1000) != walls.end() ) + { + dirs.push_back(0); + } + else if ( walls.find((x + 0) + (y + 1) * 1000) != walls.end() ) + { + dirs.push_back(6); + } + else if ( walls.find((x + 0) + (y - 1) * 1000) != walls.end() ) + { + dirs.push_back(2); + } + } + int picked = dirs[map_rng.rand() % dirs.size()]; + breakableLocations.push_back(BreakableNode_t(walls.size(), x, y, picked)); + foundSpace = true; + break; + } } } - } - - // seed the random generator - - map_rng.seedBytes(&mapseed, sizeof(mapseed)); - map_server_rng.seedBytes(&mapseed, sizeof(mapseed)); - int balance = 0; - for ( int i = 0; i < MAXPLAYERS; i++ ) - { - if ( !client_disconnected[i] ) + if ( !foundSpace ) { - balance++; + --c; + continue; } - } - bool customMonsterCurveExists = false; - monsterCurveCustomManager.followersToGenerateForLeaders.clear(); - if ( !monsterCurveCustomManager.inUse() ) - { - monsterCurveCustomManager.readFromFile(mapseed); - } - if ( monsterCurveCustomManager.curveExistsForCurrentMapName(map->name) ) - { - customMonsterCurveExists = true; - conductGameChallenges[CONDUCT_MODDED] = 1; - Mods::disableSteamAchievements = true; + possiblelocations[y + x * map.height] = false; + --numpossiblelocations; } - if ( gameplayCustomManager.inUse() ) + + free(possibleLocationsBreakables); + possibleLocationsBreakables = nullptr; + + int breakableGoodies = breakableLocations.size() * 80 / 100; + int breakableMonsters = 0; + int breakableMonsterLimit = 2 + (currentlevel / LENGTH_OF_LEVEL_REGION) * (1 + map_rng.rand() % 2); + static ConsoleVariable cvar_breakableMonsterLimit("/breakable_monster_limit", 0); + std::set generatedBreakables; + if ( svFlags & SV_FLAG_CHEATS ) { - conductGameChallenges[CONDUCT_MODDED] = 1; - Mods::disableSteamAchievements = true; + breakableMonsterLimit = std::max(*cvar_breakableMonsterLimit, breakableMonsterLimit); } - - // assign entity behaviors - node_t* nextnode; - for ( auto node = map->entities->first; node != nullptr; node = nextnode ) + if ( findBreakables != EditorEntityData_t::colliderRandomGenPool.end() && findBreakables->second.size() > 0 && breakableGoodies > 0 ) { - auto entity = (Entity*)node->element; - nextnode = node->next; - if ( !entity ) + int breakableItemsFromGround = 0; + std::vector chances; + std::vector ids; + for ( auto& pair : findBreakables->second ) { - continue; + ids.push_back(pair.first); + chances.push_back(pair.second); } - switch ( entity->sprite ) + Monster lastMonsterEvent = NOTHING; + int lastSpellEvent = 0; + while ( !breakableLocations.empty() ) { - // null: - case 0: + int maxNumWalls = 0; + for ( auto& b : breakableLocations ) { - list_RemoveNode(entity->mynode); - entity = nullptr; - break; - // player: + maxNumWalls = std::max(b.walls, maxNumWalls); } - case 1: + std::vector posChances; + int pickedPos = 0; + for ( auto& b : breakableLocations ) { - if ( numplayers >= 0 && numplayers < MAXPLAYERS ) + posChances.push_back(b.walls == maxNumWalls ? 1 : 0); + } + + pickedPos = map_rng.discrete(posChances.data(), posChances.size()); + + auto& top = breakableLocations.at(pickedPos); + int x = top.x; + int y = top.y; + + Entity* breakable = newEntity(179, 1, map.entities, nullptr); + breakable->x = x * 16.0; + breakable->y = y * 16.0; + breakable->colliderDecorationRotation = top.dir; + breakable->colliderIsMapGenerated = 1; + generatedBreakables.insert(breakable->getUID()); + + if ( top.id >= 0 ) + { + breakable->colliderDamageTypes = top.id; + } + else + { + int picked = map_rng.discrete(chances.data(), chances.size()); + breakable->colliderDamageTypes = ids[picked]; + } + + bool monsterEventExists = false; + bool spellEventExists = false; + auto findData = EditorEntityData_t::colliderData.find(breakable->colliderDamageTypes); + if ( findData != EditorEntityData_t::colliderData.end() ) + { + auto findMap = findData->second.hideMonsters.find(map.name); + if ( findMap != findData->second.hideMonsters.end() ) { - if ( client_disconnected[numplayers] && !intro ) + if ( findMap->second.size() > 0 ) { - // don't spawn missing players - ++numplayers; - list_RemoveNode(entity->mynode); - entity = nullptr; - break; - } - if ( stats[numplayers]->HP <= 0 ) - { - if (!keepInventoryGlobal) - { - Item** items[] = { - &stats[numplayers]->helmet, - &stats[numplayers]->breastplate, - &stats[numplayers]->gloves, - &stats[numplayers]->shoes, - &stats[numplayers]->shield, - &stats[numplayers]->weapon, - &stats[numplayers]->cloak, - &stats[numplayers]->amulet, - &stats[numplayers]->ring, - &stats[numplayers]->mask, - }; - constexpr int num_slots = sizeof(items) / sizeof(items[0]); - for (int c = 0; c < num_slots; ++c) { - if (*(items[c])) { - if ((*(items[c]))->node) { - list_RemoveNode((*(items[c]))->node); - } else { - free((*(items[c]))); - } - } - *(items[c]) = nullptr; - } - node_t *node, *nextnode; - for ( node = stats[numplayers]->inventory.first; node != nullptr; node = nextnode ) - { - nextnode = node->next; - Item* item = (Item*)node->element; - if ( itemCategory(item) == SPELL_CAT ) - { - continue; // don't drop spells on death, stupid! - } - list_RemoveNode(node); - } - } - if ( multiplayer != CLIENT ) - { - messagePlayer(numplayers, MESSAGE_STATUS, Language::get(1109)); - stats[numplayers]->HP = stats[numplayers]->MAXHP / 2; - stats[numplayers]->MP = stats[numplayers]->MAXMP / 2; - stats[numplayers]->HUNGER = 500; - for ( int c = 0; c < NUMEFFECTS; ++c ) + for ( auto m : findMap->second ) + { + if ( m > NOTHING && m < NUMMONSTERS ) { - if ( !(c == EFF_VAMPIRICAURA && stats[numplayers]->EFFECTS_TIMERS[c] == -2) - && c != EFF_WITHDRAWAL && c != EFF_SHAPESHIFT ) - { - stats[numplayers]->EFFECTS[c] = false; - stats[numplayers]->EFFECTS_TIMERS[c] = 0; - } + monsterEventExists = true; } } } - entity->behavior = &actPlayer; - entity->addToCreatureList(map->creatures); + } - players[numplayers]->ghost.initStartRoomLocation(entity->x / 16, entity->y / 16); + if ( findData->second.spellTriggers.size() > 0 ) + { + spellEventExists = true; + } + } - entity->x += 8; - entity->y += 8; - entity->z = -1; - entity->focalx = limbs[HUMAN][0][0]; // 0 - entity->focaly = limbs[HUMAN][0][1]; // 0 - entity->focalz = limbs[HUMAN][0][2]; // -1.5 - entity->sprite = 113; // head model - entity->sizex = 4; - entity->sizey = 4; - entity->flags[GENIUS] = true; - if ( numplayers == clientnum && multiplayer == CLIENT ) + if ( breakableGoodies > 0 ) + { + --breakableGoodies; + + int index = (y) * MAPLAYERS + (x) * MAPLAYERS * map.height; + + static ConsoleVariable cvar_breakableMonsterChance("/breakable_monster_chance", 10); + + if ( spellEventExists ) + { + std::vector chances; + bool avoidLastSpell = false; + for ( auto s : findData->second.spellTriggers ) { - entity->flags[UPDATENEEDED] = false; + chances.push_back(1); + if ( lastSpellEvent != 0 && s != lastSpellEvent ) + { + avoidLastSpell = true; + } } - else + + if ( avoidLastSpell ) { - entity->flags[UPDATENEEDED] = true; + int unusedChances = 0; + for ( size_t i = 0; i < chances.size(); ++i ) + { + if ( chances[i] != lastSpellEvent ) + { + ++unusedChances; + } + } + if ( unusedChances > 0 ) + { + for ( size_t i = 0; i < chances.size(); ++i ) + { + if ( findData->second.spellTriggers[i] == lastSpellEvent ) + { + chances[i] = 0; + } + } + } } - entity->flags[BLOCKSIGHT] = true; - entity->skill[2] = numplayers; // skill[2] == PLAYER_NUM - players[numplayers]->entity = entity; - if ( entity->playerStartDir == -1 ) + + int pickIndex = map_rng.discrete(chances.data(), chances.size()); + int picked = findData->second.spellTriggers[pickIndex]; + if ( picked > 0 ) { - entity->yaw = (map_rng.rand() % 8) * 45 * (PI / 180.f); + if ( map_rng.rand() % 5 > 0 ) + { + picked += 1000; + } + breakable->colliderSpellEvent = picked; + lastSpellEvent = picked % 1000; } - else + } + + if ( !map.tiles[index] && map_rng.rand() % 2 == 1 ) + { + // nothing over pits 50% + } + else if ( (breakableMonsters < breakableMonsterLimit && monsterEventExists + && map_rng.rand() % ((svFlags & SV_FLAG_CHEATS) ? std::min(10, *cvar_breakableMonsterChance) : 10) == 0) + && map.monsterexcludelocations[x + y * map.width] == false ) // 10% monster inside + { + Monster monsterEvent = NOTHING; + auto findMap = findData->second.hideMonsters.find(map.name); + if ( findMap != findData->second.hideMonsters.end() ) { - entity->yaw = entity->playerStartDir * 45 * (PI / 180.f); + if ( findMap->second.size() > 0 ) + { + std::vector chances; + bool avoidLastMonster = false; + for ( auto m : findMap->second ) + { + chances.push_back(1); + if ( lastMonsterEvent != NOTHING && m != lastMonsterEvent ) + { + avoidLastMonster = true; + } + } + if ( avoidLastMonster ) + { + for ( size_t i = 0; i < chances.size(); ++i ) + { + if ( findMap->second[i] == lastMonsterEvent ) + { + chances[i] = 0; + } + } + } + int pickIndex = map_rng.discrete(chances.data(), chances.size()); + int picked = findMap->second[pickIndex]; + if ( picked > NOTHING && picked < NUMMONSTERS ) + { + monsterEvent = (Monster)picked; + lastMonsterEvent = monsterEvent; + } + } } - entity->playerStartDir = 0; - if ( multiplayer != CLIENT ) + + if ( (svFlags & SV_FLAG_TRAPS) ) { - if ( numplayers == 0 && minotaurlevel ) + if ( map_rng.rand() % 2 == 0 ) { - createMinotaurTimer(entity, map, map_server_rng.getU32()); + breakable->colliderHideMonster = monsterEvent; + } + else + { + breakable->colliderHideMonster = 1000 + monsterEvent; } } - ++numplayers; - } - if ( balance > 4 ) - { - // if MAXPLAYERS > 4, then add some new player markers - --balance; - Entity* extraPlayer = newEntity(1, 1, map->entities, nullptr); - extraPlayer->x = entity->x - 8; - extraPlayer->y = entity->y - 8; + ++breakableMonsters; } - if ( numplayers > MAXPLAYERS ) + else if ( !map.tiles[index] || map_rng.rand() % 2 == 1 ) // 50% chance (or floating over a pit is just gold) { - printlog("warning: too many player objects in level!\n"); - } - break; - } - // east/west door: - case 2: - { - entity->x += 8; - entity->y += 8; - entity->sprite = doorFrameSprite(); - entity->flags[PASSABLE] = true; - entity->behavior = &actDoorFrame; - auto childEntity = newEntity(2, 0, map->entities, nullptr); //Door frame entity. - childEntity->x = entity->x; - childEntity->y = entity->y; - TileEntityList.addEntity(*childEntity); - //printlog("16 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); - childEntity->sizex = 1; + std::vector genGold; + int numGold = 3 + map_rng.rand() % 3; + while ( numGold > 0 ) + { + --numGold; + Entity* entity = newEntity(9, 1, map.entities, nullptr); // gold + genGold.push_back(entity); + entity->x = breakable->x; + entity->y = breakable->y; + entity->goldAmount = 2 + map_rng.rand() % 3; + entity->flags[INVISIBLE] = true; + entity->yaw = breakable->yaw; + entity->goldInContainer = breakable->getUID(); + breakable->colliderContainedEntity = entity->getUID(); + numGenGold++; + } + int index = -1; + for ( auto gold : genGold ) + { + ++index; + gold->yaw += (index * PI) / genGold.size(); + } + } + else + { + if ( itemsGeneratedList.size() > 10 && breakableItemsFromGround < 6 ) + { + // steal an item from the ground + size_t index = map_rng.rand() % itemsGeneratedList.size(); + Uint32 uid = itemsGeneratedList.at(index); + itemsGeneratedList.erase(itemsGeneratedList.begin() + index); + if ( Entity* entity = uidToEntity(uid) ) + { + entity->x = breakable->x; + entity->y = breakable->y; + entity->flags[INVISIBLE] = true; + entity->itemContainer = breakable->getUID(); + entity->yaw = breakable->yaw; + breakable->colliderContainedEntity = entity->getUID(); + ++breakableItemsFromGround; + } + } + else + { + Entity* entity = newEntity(8, 1, map.entities, nullptr); // item + setSpriteAttributes(entity, nullptr, nullptr); + entity->x = breakable->x; + entity->y = breakable->y; + entity->flags[INVISIBLE] = true; + entity->itemContainer = breakable->getUID(); + entity->yaw = breakable->yaw; + breakable->colliderContainedEntity = entity->getUID(); + numGenItems++; + } + } + } + + if ( false ) + { + //messagePlayer(0, MESSAGE_DEBUG, "pick: %d | x: %d y: %d", picked, x, y); + Entity* ent = newEntity(245, 0, map.entities, nullptr); + //ent->behavior = &actBoulder; + ent->x = x * 16.0 + 8; + ent->y = y * 16.0 + 8; + ent->z = 24.0; + ent->flags[PASSABLE] = true; + } + breakableLocations.erase(breakableLocations.begin() + pickedPos); + } + } + + if ( darkmap && map.skybox == 0 ) + { + std::vector>> batAreasGood; + std::vector>> batAreasOk; + for ( int x = 1; x < map.width - 1; ++x ) + { + for ( int y = 1; y < map.height - 1; ++y ) + { + if ( possiblelocations[y + x * map.height] ) + { + std::vector testAreas = { + (x - 1) + 1000 * (y + 0), + (x + 1) + 1000 * (y + 0), + (x + 0) + 1000 * (y + 1), + (x + 0) + 1000 * (y - 1), + (x + 0) + 1000 * (y + 0), + (x + 1) + 1000 * (y + 1), + (x - 1) + 1000 * (y + 1), + (x + 1) + 1000 * (y - 1), + (x - 1) + 1000 * (y - 1) + }; + std::map> goodSpots; + int openCeilings = 0; + for ( auto coord : testAreas ) + { + int tx = coord % 1000; + int ty = coord / 1000; + if ( tx >= 1 && tx < map.width - 1 && ty >= 1 && ty < map.height - 1 ) + { + if ( possiblelocations[ty + tx * map.height] ) + { + int mapIndex = (ty)*MAPLAYERS + (tx)*MAPLAYERS * map.height; + if ( !map.tiles[OBSTACLELAYER + mapIndex] ) + { + if ( !map.tiles[(MAPLAYERS - 1) + mapIndex] ) + { + ++openCeilings; + goodSpots[0].push_back(coord); + } + else + { + goodSpots[1].push_back(coord); + } + } + } + } + } + if ( openCeilings >= 5 ) + { + batAreasGood.push_back(goodSpots); + } + else if ( (goodSpots[0].size() + goodSpots[1].size()) >= 5 ) + { + batAreasOk.push_back(goodSpots); + } + } + } + } + + std::unordered_set visited; + std::vector previousAreas; + int numBatAreas = std::max(2, std::min(5, 1 + (currentlevel / LENGTH_OF_LEVEL_REGION))); + while ( numBatAreas > 0 ) + { + if ( batAreasGood.size() == 0 && batAreasOk.size() == 0 ) + { + break; + } + + auto& areas = batAreasGood.size() > 0 ? batAreasGood : batAreasOk; + if ( areas.size() > 0 ) + { + size_t picked = map_rng.rand() % areas.size(); + auto& coords = areas[picked]; + + bool skip = false; + for ( auto coord : coords[0] ) + { + if ( visited.find(coord) != visited.end() ) + { + // no good + skip = true; + } + else + { + visited.insert(coord); + } + } + for ( auto coord : coords[1] ) + { + if ( visited.find(coord) != visited.end() ) + { + // no good + skip = true; + } + else + { + visited.insert(coord); + } + } + + int currentCoord = 0; + if ( coords[0].size() > 0 ) + { + currentCoord = coords[0][0]; + } + else if ( coords[1].size() > 0 ) + { + currentCoord = coords[1][0]; + } + + int checkx = currentCoord % 1000; + int checky = currentCoord / 1000; + for ( auto previousCoord : previousAreas ) + { + int ox = previousCoord % 1000; + int oy = previousCoord / 1000; + + real_t dx, dy; + dx = checkx - ox; + dy = checky - oy; + if ( sqrt(dx * dx + dy * dy) < 8.0 ) // too close to other regions, within 8 tiles + { + skip = true; + break; + } + } + + if ( skip ) + { + areas.erase(areas.begin() + picked); + continue; + } + + if ( coords[0].size() > 0 ) + { + previousAreas.push_back(coords[0][0]); + } + else if ( coords[1].size() > 0 ) + { + previousAreas.push_back(coords[1][0]); + } + + int numSpawns = std::max(2, std::min(4, 1 + (currentlevel / LENGTH_OF_LEVEL_REGION))); + for ( size_t i = 0; i < (coords[0].size() + coords[1].size()) && numSpawns > 0; ++i ) + { + auto coord = (i < coords[0].size()) ? coords[0][i] : coords[1][i - coords[0].size()]; + int tx = coord % 1000; + int ty = coord / 1000; + + { + Entity* ent = newEntity(188, 0, map.entities, nullptr); + ent->x = tx * 16.0; + ent->y = ty * 16.0; + } + + //Entity* ent = newEntity(245, 0, map.entities, nullptr); + ////ent->behavior = &actBoulder; + //ent->x = tx * 16.0 + 8; + //ent->y = ty * 16.0 + 8; + //ent->z = 24.0; + //ent->flags[PASSABLE] = true; + visited.insert(coord); + --numSpawns; + + possiblelocations[ty + tx * map.height] = false; + --numpossiblelocations; + } + + --numBatAreas; + + areas.erase(areas.begin() + picked); + continue; + } + } + } + + static ConsoleVariable cvar_treasure_chances("/treasure_chances", Vector4{ 0.f, 0.f, 0.f, 0.f }); + static ConsoleVariable cvar_treasure_key_force("/treasure_key_force", 0); + if ( *cvar_treasure_key_force > 0 && (svFlags & SV_FLAG_CHEATS) ) + { + for ( int i = 0; i < *cvar_treasure_key_force; ++i ) + { + generateKeyItems.push_back(std::make_pair(static_cast((int)KEY_STONE + map_rng.rand() % 7), 0)); + } + } + if ( generateKeyItems.size() > 0 ) + { + int numKeysGenerated = 0; + + enum GenerateKeyPlaces + { + KEY_GEN_CHEST, + KEY_GEN_BREAKABLE, + KEY_GEN_TABLE_PODIUM, + KEY_GEN_ENUM_END + }; + + std::vector goodEntities[KEY_GEN_ENUM_END]; + std::set itemEntityCoords; + std::vector chances = { 4, 3, 6 }; + if ( svFlags & SV_FLAG_CHEATS ) + { + if ( cvar_treasure_chances->w > 0.01f ) + { + chances[0] = cvar_treasure_chances->x; + chances[1] = cvar_treasure_chances->y; + chances[2] = cvar_treasure_chances->z; + } + } + + Entity* playerStart = nullptr; + for ( node = map.entities->first; node != NULL; node = node->next ) + { + entity2 = (Entity*)node->element; + + if ( !playerStart ) + { + if ( entity2->sprite == 1 ) + { + playerStart = entity2; + } + } + + GenerateKeyPlaces entityType = KEY_GEN_ENUM_END; + switch ( entity2->sprite ) + { + case 8: // items + itemEntityCoords.insert(static_cast(entity2->x / 16) + 10000 * static_cast(entity2->y / 16)); + break; + case 21: + if ( entity2->chestLocked < 100 && entity2->chestMimicChance < 100 ) + { + entityType = KEY_GEN_CHEST; // not 100% locked, and not 100% mimic chance + } + break; + case 125: + case 59: + if ( entity2->sprite == 59 ) // table + { + if ( entity2->furnitureTableRandomItemChance < 100 ) + { + entityType = KEY_GEN_TABLE_PODIUM; // not 100% for random table item + } + } + else + { + entityType = KEY_GEN_TABLE_PODIUM; + } + break; + case 179: + if ( generatedBreakables.find(entity2->getUID()) != generatedBreakables.end() ) + { + if ( entity2->colliderHideMonster == 0 && entity2->colliderContainedEntity == 0 ) + { + entityType = KEY_GEN_BREAKABLE; + } + } + break; + default: + continue; + } + + if ( entityType != KEY_GEN_ENUM_END ) + { + int x = static_cast(entity2->x / 16); + int y = static_cast(entity2->y / 16); + int index = (y)*MAPLAYERS + (x)*MAPLAYERS * map.height; + if ( map.tiles[index] && !shoparea[y + x * map.height] + && !treasureRoomLocations[x + y * map.width] + && !(x >= startRoomInfo.x1 && x <= startRoomInfo.x2 + && y >= startRoomInfo.y1 && y <= startRoomInfo.y2) ) // check floor, dont spawn in treasure room, shop, or start area + { + goodEntities[entityType].push_back(entity2); + } + } + } + + for ( auto it = goodEntities[KEY_GEN_TABLE_PODIUM].begin(); it != goodEntities[KEY_GEN_TABLE_PODIUM].end(); ) + { + int x = static_cast((*it)->x / 16); + int y = static_cast((*it)->y / 16); + + if ( itemEntityCoords.find(x + 10000 * y) != itemEntityCoords.end() ) // item already exists on this tile + { + it = goodEntities[KEY_GEN_TABLE_PODIUM].erase(it); + } + else + { + ++it; + } + } + + for ( int c = 0; c < KEY_GEN_ENUM_END; ++c ) + { + if ( goodEntities[c].empty() ) + { + chances[c] = 0; + } + } + + if ( playerStart ) + { + bool hellLadderFix = !strncmp(map.name, "Hell", 4); + std::vector tempPassableEntities; + if ( hellLadderFix ) + { + for ( node = map.entities->first; node != NULL; node = node->next ) + { + if ( (entity2 = (Entity*)node->element) ) + { + if ( entity2->sprite == 19 || entity2->sprite == 20 + || entity2->sprite == 113 || entity2->sprite == 114 + /*|| entity2->sprite == 217 || entity2->sprite == 218*/ ) + { + int entx = entity2->x / 16; + int enty = entity2->y / 16; + if ( !entity2->flags[PASSABLE] ) + { + if ( entx >= startRoomInfo.x1 && entx <= startRoomInfo.x2 + && enty >= startRoomInfo.y1 && enty <= startRoomInfo.y2 ) + { + tempPassableEntities.push_back(entity2); + entity2->flags[PASSABLE] = true; + } + } + } + } + } + } + + while ( generateKeyItems.size() > 0 ) + { + bool anychances = false; + for ( int c = 0; c < KEY_GEN_ENUM_END; ++c ) + { + if ( chances[c] > 0 ) + { + anychances = true; + break; + } + } + + if ( !anychances ) + { + break; + } + + GenerateKeyPlaces pickedGenType = static_cast(map_rng.discrete(chances.data(), chances.size())); + auto& entities = goodEntities[pickedGenType]; + + int pick = map_rng.rand() % entities.size(); + Entity* ent = entities[pick]; + entities.erase(entities.begin() + pick); + if ( entities.empty() ) + { + chances[static_cast(pickedGenType)] = 0; + } + int x = static_cast(ent->x / 16); + int y = static_cast(ent->y / 16); + + if ( strncmp(map.name, "Underworld", 10) ) // underworld no check paths + { + list_t* path = generatePath(x, y, playerStart->x / 16, playerStart->y / 16, + ent, playerStart, GeneratePathTypes::GENERATE_PATH_CHECK_EXIT, true); + if ( path == NULL ) + { + continue; // no path + } + list_FreeAll(path); + free(path); + } + else + { + if ( !map.tiles[(y)*MAPLAYERS + (x)*MAPLAYERS * map.height] ) + { + // underworld don't spawn over pit + continue; + } + } + + Entity* keyItem = newEntity(8, 1, map.entities, nullptr); // item + keyItem->x = x * 16.0; + keyItem->y = y * 16.0; + setSpriteAttributes(keyItem, nullptr, nullptr); + keyItem->skill[10] = generateKeyItems.front().first + 2; + keyItem->skill[11] = 3; + keyItem->skill[12] = 0; + keyItem->skill[13] = 1; + itemsGeneratedList.push_back(keyItem->getUID()); + numGenItems++; + + generateKeyItems.erase(generateKeyItems.begin()); + ++numKeysGenerated; + if ( pickedGenType == KEY_GEN_CHEST ) + { + ent->chestLocked = 0; + ent->chestMimicChance = 0; + + char buf[256] = ""; + snprintf(buf, sizeof(buf), "@script @attachto=items @attached.addtochest=%d,%d @triggerif=always", x, y); + textSourceScript.createScriptEntityInMapGen(x, y, buf); + + if ( *cvar_treasure_key_force > 0 && (svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_HINT, "Key generated at x:%d, y:%d, type: chest", x, y); + } + } + else if ( pickedGenType == KEY_GEN_BREAKABLE ) + { + keyItem->itemContainer = ent->getUID(); + keyItem->flags[INVISIBLE] = true; + keyItem->yaw = ent->yaw; + ent->colliderContainedEntity = keyItem->getUID(); + + if ( *cvar_treasure_key_force > 0 && (svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_HINT, "Key generated at x:%d, y:%d, type: breakable", x, y); + } + } + else if ( pickedGenType == KEY_GEN_TABLE_PODIUM ) + { + //keyItem->z = 0.0; + //keyItem->itemNotMoving = 1; + //keyItem->itemNotMovingClient = 1; + if ( ent->sprite == 59 ) + { + // table + ent->furnitureTableRandomItemChance = 0; + } + ent->parent = keyItem->getUID(); + + if ( *cvar_treasure_key_force > 0 && (svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_HINT, "Key generated at x:%d, y:%d, type: table", x, y); + } + } + + if ( possiblelocations[y + x * map.height] ) + { + possiblelocations[y + x * map.height] = false; + numpossiblelocations--; + } + } + + if ( generateKeyItems.size() > 0 ) + { + // failsafe if no objects available + std::vector goodSpots; + std::vector goodDeadEnds; + for ( int x = 0; x < map.width; ++x ) + { + for ( int y = 0; y < map.height; ++y ) + { + if ( possiblelocations[y + x * map.height] == true && treasureRoomLocations[x + y * map.width] == false + && map.tiles[(y)*MAPLAYERS + (x)*MAPLAYERS * map.height] && !shoparea[y + x * map.height] ) + { + goodSpots.push_back(x + 10000 * y); + + int walls = 0; + for ( int x2 = -1; x2 <= 1; x2++ ) + { + for ( int y2 = -1; y2 <= 1; y2++ ) + { + if ( x2 == 0 && y2 == 0 ) + { + continue; + } + + int checkx = x + x2; + int checky = y + y2; + if ( checkx >= 0 && checkx < map.width ) + { + if ( checky >= 0 && checky < map.height ) + { + int index = (checky)*MAPLAYERS + (checkx)*MAPLAYERS * map.height; + if ( map.tiles[OBSTACLELAYER + index] ) + { + if ( (x2 == -1 && y2 == -1) || (x2 == 1 && y2 == 1) + || (x2 == -1 && y2 == 1) || (x2 == 1 && y2 == -1) ) + { + // corners + } + else + { + // walls + ++walls; + } + } + } + } + } + } + + if ( walls == 3 ) + { + goodDeadEnds.push_back(x + 10000 * y); + } + } + } + } + while ( generateKeyItems.size() > 0 ) + { + auto& floorTiles = goodDeadEnds.size() > 0 ? goodDeadEnds : goodSpots; + if ( floorTiles.size() == 0 ) + { + break; // no available floor tiles + } + + int pick = map_rng.rand() % floorTiles.size(); + int x = floorTiles[pick] % 10000; + int y = floorTiles[pick] / 10000; + + floorTiles.erase(floorTiles.begin() + pick); + + Entity* keyItem = newEntity(8, 1, map.entities, nullptr); // item + keyItem->x = x * 16.0; + keyItem->y = y * 16.0; + + if ( strncmp(map.name, "Underworld", 10) ) // underworld no check paths + { + list_t* path = generatePath(x, y, playerStart->x / 16, playerStart->y / 16, + keyItem, playerStart, GeneratePathTypes::GENERATE_PATH_CHECK_EXIT, true); + if ( path == NULL ) + { + list_RemoveNode(keyItem->mynode); + keyItem = nullptr; + continue; // no path + } + list_FreeAll(path); + free(path); + } + else + { + if ( !map.tiles[(y)*MAPLAYERS + (x)*MAPLAYERS * map.height] ) + { + // underworld don't spawn over pit + continue; + } + } + + if ( *cvar_treasure_key_force > 0 && (svFlags & SV_FLAG_CHEATS) ) + { + if ( &floorTiles == &goodSpots ) + { + messagePlayer(clientnum, MESSAGE_HINT, "Key generated at x:%d, y:%d, type: floor", x, y); + } + else if ( &floorTiles == &goodDeadEnds ) + { + messagePlayer(clientnum, MESSAGE_HINT, "Key generated at x:%d, y:%d, type: deadend", x, y); + } + } + + setSpriteAttributes(keyItem, nullptr, nullptr); + keyItem->skill[10] = generateKeyItems.front().first + 2; + keyItem->skill[11] = 3; + keyItem->skill[12] = 0; + keyItem->skill[13] = 1; + itemsGeneratedList.push_back(keyItem->getUID()); + numGenItems++; + + generateKeyItems.erase(generateKeyItems.begin()); + ++numKeysGenerated; + + if ( possiblelocations[y + x * map.height] ) + { + possiblelocations[y + x * map.height] = false; + numpossiblelocations--; + } + } + } + + for ( auto ent : tempPassableEntities ) + { + ent->flags[PASSABLE] = false; + } + } + + if ( *cvar_treasure_key_force > 0 && (svFlags & SV_FLAG_CHEATS) ) + { + messagePlayer(clientnum, MESSAGE_HINT, "Keys generated: %d", numKeysGenerated); + } + } + + // on hell levels, lava doesn't bubble. helps performance + /*if( !strcmp(map.name,"Hell") ) { + for( node=map.entities->first; node!=NULL; node=node->next ) { + Entity *entity = (Entity *)node->element; + if( entity->sprite == 41 ) { // lava.png + entity->skill[4] = 1; // LIQUID_LAVANOBUBBLE = + } + } + }*/ + + free(possiblelocations); + free(firstroomtile); + free(secretlevelexittile); + list_FreeAll(&subRoomMapList); + list_FreeAll(&mapList); + list_FreeAll(&doorList); + + printlog("successfully generated a dungeon with %d rooms, %d monsters, %d gold, %d items, %d decorations.\n", roomcount, nummonsters, numGenGold, numGenItems, numGenDecorations); + //messagePlayer(0, "successfully generated a dungeon with %d rooms, %d monsters, %d gold, %d items, %d decorations.", roomcount, nummonsters, numGenGold, numGenItems, numGenDecorations); + return secretlevelexit; +} + +bool allowedGenerateMimicOnChest(int x, int y, map_t& map) +{ + if ( gameModeManager.getMode() == gameModeManager.GAME_MODE_TUTORIAL + || gameModeManager.getMode() == gameModeManager.GAME_MODE_TUTORIAL_INIT ) + { + return false; + } + if ( !(svFlags & SV_FLAG_TRAPS) ) + { + return false; + } + /*if ( map.trapexcludelocations ) + { + if ( x >= 0 && x < map.width && y >= 0 && y < map.height ) + { + if ( map.trapexcludelocations[x + y * map.width] ) + { + return false; + } + } + }*/ + return true; +} + +void debugMap(map_t* map) +{ + return; + if ( !map ) + { + return; + } + + std::set takenSlots; + for ( auto node = map->entities->first; node != nullptr; ) + { + Entity* postProcessEntity = (Entity*)node->element; + node = node->next; + if ( postProcessEntity ) + { + if ( postProcessEntity->behavior == &actItem && postProcessEntity->z > 4 ) + { + int x = (int)postProcessEntity->x >> 4; + int y = (int)postProcessEntity->y >> 4; + takenSlots.insert(x + y * 10000); + } + } + } + + for ( int x = 0; x < map->width; ++x ) + { + for ( int y = 0; y < map->height; ++y ) + { + if ( takenSlots.find(x + y * 10000) != takenSlots.end() ) + { + int numWalls = 0; + std::vector> coords = { + {x + 1, y}, + {x - 1, y}, + {x, y + 1}, + {x, y - 1} + }; + for ( auto& pair : coords ) + { + if ( pair.first >= 0 && pair.first < map->width ) + { + if ( pair.second >= 0 && pair.second < map->height ) + { + if ( map->tiles[pair.second * MAPLAYERS + pair.first * MAPLAYERS * map->height] ) // floor + { + numWalls += map->tiles[OBSTACLELAYER + pair.second * MAPLAYERS + pair.first * MAPLAYERS * map->height] != 0 ? 1 : 0; + } + } + } + } + if ( numWalls > 0 ) + { + //Entity* ent = newEntity(245, 0, map->entities, nullptr); + ////ent->behavior = &actBoulder; + //ent->x = x * 16.0 + 8; + //ent->y = y * 16.0 + 8; + //ent->z = 24.0; + } + //int numObstacles = checkObstacle((checkx) * 16, (checky) * 16, NULL, NULL, true); + } + } + } + mapLevel2(0); + + /*int num5x5s = 0; + // open area debugging tool + for ( int x = 0; x < map->width; ++x ) + { + for ( int y = 0; y < map->height; ++y ) + { + if ( takenSlots.find(x + y * 10000) == takenSlots.end() ) + { + if ( !map->tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map->height] + && !map->tiles[2 + y * MAPLAYERS + x * MAPLAYERS * map->height] ) + { + int numTiles = 0; + for ( int x1 = x; x1 < map->width && x1 < x + 5; ++x1 ) + { + for ( int y1 = y; y1 < map->height && y1 < y + 5; ++y1 ) + { + if ( takenSlots.find(x1 + y1 * 10000) == takenSlots.end() ) + { + if ( !map->tiles[OBSTACLELAYER + y1 * MAPLAYERS + x1 * MAPLAYERS * map->height] + && !map->tiles[2 + y1 * MAPLAYERS + x1 * MAPLAYERS * map->height] ) + { + ++numTiles; + } + } + } + } + if ( numTiles == 25 ) + { + ++num5x5s; + Entity* ent = newEntity(245, 0, map->entities, nullptr); + ent->behavior == &actBoulder; + ent->x = x * 16.0 + 8; + ent->y = y * 16.0 + 8; + } + } + } + } + } + messagePlayer(0, MESSAGE_DEBUG, "%d 5x5s", num5x5s);*/ +} + +/*------------------------------------------------------------------------------- + + assignActions + + configures a map to be playable from a default state + +-------------------------------------------------------------------------------*/ +std::map generatedSpellbooks; +void assignActions(map_t* map) +{ + bool itemsdonebefore = false; + Entity* vampireQuestChest = nullptr; + + if ( map == nullptr ) + { + return; + } + + // update arachnophobia filter + arachnophobia_filter = GameplayPreferences_t::getGameConfigValue(GameplayPreferences_t::GOPT_ARACHNOPHOBIA); + colorblind_lobby = GameplayPreferences_t::getGameConfigValue(GameplayPreferences_t::GOPT_COLORBLIND); + + // add lava lights + for ( int y = 0; y < map->height; ++y ) + { + for ( int x = 0; x < map->width; ++x ) + { + if ( lavatiles[map->tiles[y * MAPLAYERS + x * MAPLAYERS * map->height]] ) + { + addLight(x, y, "lava"); + } + } + } + + // seed the random generator + + map_rng.seedBytes(&mapseed, sizeof(mapseed)); + map_server_rng.seedBytes(&mapseed, sizeof(mapseed)); + + int balance = 0; + for ( int i = 0; i < MAXPLAYERS; i++ ) + { + if ( !client_disconnected[i] ) + { + balance++; + } + } + + bool customMonsterCurveExists = false; + monsterCurveCustomManager.followersToGenerateForLeaders.clear(); + if ( !monsterCurveCustomManager.inUse() ) + { + monsterCurveCustomManager.readFromFile(mapseed); + } + if ( monsterCurveCustomManager.curveExistsForCurrentMapName(map->name) ) + { + customMonsterCurveExists = true; + conductGameChallenges[CONDUCT_MODDED] = 1; + Mods::disableSteamAchievements = true; + } + if ( gameplayCustomManager.inUse() ) + { + conductGameChallenges[CONDUCT_MODDED] = 1; + Mods::disableSteamAchievements = true; + } + + // assign entity behaviors + node_t* nextnode; + for ( auto node = map->entities->first; node != nullptr; node = nextnode ) + { + auto entity = (Entity*)node->element; + nextnode = node->next; + if ( !entity ) + { + continue; + } + switch ( entity->sprite ) + { + // null: + case 0: + { + list_RemoveNode(entity->mynode); + entity = nullptr; + break; + // player: + } + case 1: + { + if ( numplayers >= 0 && numplayers < MAXPLAYERS ) + { + if ( client_disconnected[numplayers] && !intro ) + { + // don't spawn missing players + ++numplayers; + list_RemoveNode(entity->mynode); + entity = nullptr; + break; + } + + bool revived = false; + if ( stats[numplayers]->HP <= 0 ) + { + revived = true; + if (!keepInventoryGlobal) + { + Item** items[] = { + &stats[numplayers]->helmet, + &stats[numplayers]->breastplate, + &stats[numplayers]->gloves, + &stats[numplayers]->shoes, + &stats[numplayers]->shield, + &stats[numplayers]->weapon, + &stats[numplayers]->cloak, + &stats[numplayers]->amulet, + &stats[numplayers]->ring, + &stats[numplayers]->mask, + }; + constexpr int num_slots = sizeof(items) / sizeof(items[0]); + for (int c = 0; c < num_slots; ++c) { + if (*(items[c])) { + if ((*(items[c]))->node) { + list_RemoveNode((*(items[c]))->node); + } else { + free((*(items[c]))); + } + } + *(items[c]) = nullptr; + } + node_t *node, *nextnode; + for ( node = stats[numplayers]->inventory.first; node != nullptr; node = nextnode ) + { + nextnode = node->next; + Item* item = (Item*)node->element; + if ( itemCategory(item) == SPELL_CAT ) + { + continue; // don't drop spells on death, stupid! + } + list_RemoveNode(node); + } + } + if ( multiplayer != CLIENT ) + { + messagePlayer(numplayers, MESSAGE_STATUS, Language::get(1109)); + stats[numplayers]->HP = stats[numplayers]->MAXHP / 2; + stats[numplayers]->MP = stats[numplayers]->MAXMP / 2; + stats[numplayers]->HUNGER = 500; + for ( int c = 0; c < NUMEFFECTS; ++c ) + { + if ( !(c == EFF_VAMPIRICAURA && stats[numplayers]->EFFECTS_TIMERS[c] == -2) + && c != EFF_WITHDRAWAL && c != EFF_SHAPESHIFT ) + { + stats[numplayers]->clearEffect(c); + stats[numplayers]->EFFECTS_TIMERS[c] = 0; + stats[numplayers]->EFFECTS_ACCRETION_TIME[c] = 0; + } + } + } + } + entity->behavior = &actPlayer; + entity->addToCreatureList(map->creatures); + + players[numplayers]->ghost.initStartRoomLocation(entity->x / 16, entity->y / 16); + + entity->x += 8; + entity->y += 8; + entity->z = -1; + entity->focalx = limbs[HUMAN][0][0]; // 0 + entity->focaly = limbs[HUMAN][0][1]; // 0 + entity->focalz = limbs[HUMAN][0][2]; // -1.5 + //entity->sprite = 113; // head model + if ( multiplayer == CLIENT ) + { + entity->sprite = playerHeadSprite(getMonsterFromPlayerRace(stats[numplayers]->playerRace), + stats[numplayers]->sex, stats[numplayers]->stat_appearance); + } + else + { + entity->sprite = playerHeadSprite(getMonsterFromPlayerRace(stats[numplayers]->playerRace), + stats[numplayers]->sex, stats[numplayers]->stat_appearance); + } + entity->sizex = 4; + entity->sizey = 4; + entity->flags[GENIUS] = true; + if ( numplayers == clientnum && multiplayer == CLIENT ) + { + entity->flags[UPDATENEEDED] = false; + } + else + { + entity->flags[UPDATENEEDED] = true; + } + entity->flags[BLOCKSIGHT] = true; + entity->skill[2] = numplayers; // skill[2] == PLAYER_NUM + players[numplayers]->entity = entity; + if ( entity->playerStartDir == -1 ) + { + entity->yaw = (map_rng.rand() % 8) * 45 * (PI / 180.f); + } + else + { + entity->yaw = entity->playerStartDir * 45 * (PI / 180.f); + } + entity->playerStartDir = 0; + if ( multiplayer != CLIENT ) + { + if ( numplayers == 0 && minotaurlevel ) + { + createMinotaurTimer(entity, map, map_server_rng.getU32()); + } + } + + if ( !revived ) + { + int hpMod = Entity::getHPRestoreOnLevelUp(entity, stats[numplayers], 0, true); + int mpMod = Entity::getMPRestoreOnLevelUp(entity, stats[numplayers], 0, true); + int maxHpMod = stats[numplayers]->MAXHP / 2 - stats[numplayers]->HP; + int maxMpMod = stats[numplayers]->MAXMP / 2 - stats[numplayers]->MP; + if ( maxHpMod > 0 ) + { + hpMod = std::min(maxHpMod, hpMod); + if ( hpMod > 0 ) + { + entity->modHP(hpMod); + } + } + if ( maxMpMod > 0 ) + { + mpMod = std::min(maxMpMod, mpMod); + if ( mpMod > 0 ) + { + int mpAmount = entity->modMP(mpMod); + entity->playerInsectoidIncrementHungerToMP(mpAmount); + } + } + } + + ++numplayers; + } + if ( balance > 4 ) + { + // if MAXPLAYERS > 4, then add some new player markers + --balance; + Entity* extraPlayer = newEntity(1, 1, map->entities, nullptr); + extraPlayer->x = entity->x - 8; + extraPlayer->y = entity->y - 8; + } + if ( numplayers > MAXPLAYERS ) + { + printlog("warning: too many player objects in level!\n"); + } + break; + } + // east/west door: + case 2: + { + entity->x += 8; + entity->y += 8; + entity->sprite = doorFrameSprite(); + entity->flags[PASSABLE] = true; + entity->behavior = &actDoorFrame; + auto childEntity = newEntity(2, 0, map->entities, nullptr); //Door frame entity. + childEntity->x = entity->x; + childEntity->y = entity->y; + TileEntityList.addEntity(*childEntity); + //printlog("16 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); + childEntity->sizex = 1; childEntity->sizey = 8; childEntity->behavior = &actDoor; childEntity->flags[BLOCKSIGHT] = true; @@ -5752,6 +7428,7 @@ void assignActions(map_t* map) } entity->flags[PASSABLE] = true; entity->behavior = &actItem; + bool rolledLevelCurveItem = false; if ( entity->sprite == 68 ) // magic_bow.png { entity->skill[10] = ARTIFACT_BOW; @@ -5803,35 +7480,38 @@ void assignActions(map_t* map) if ( map_rng.rand() % 2 == 0 ) { // possible magicstaff - int randType = map_rng.rand() % (NUMCATEGORIES - 1); + int randType = map_rng.rand() % (Category::CATEGORY_MAX - 2); if ( randType == THROWN && map_rng.rand() % 3 ) // THROWN items 66% to be re-roll. { - randType = map_rng.rand() % (NUMCATEGORIES - 1); + randType = map_rng.rand() % (Category::CATEGORY_MAX - 2); } entity->skill[10] = itemLevelCurve(static_cast(randType), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } else { // impossible magicstaff - int randType = map_rng.rand() % (NUMCATEGORIES - 2); + int randType = map_rng.rand() % (Category::CATEGORY_MAX - 3); if ( randType >= MAGICSTAFF ) { randType++; } if ( randType == THROWN && map_rng.rand() % 3 ) // THROWN items 66% to be re-roll. { - randType = map_rng.rand() % (NUMCATEGORIES - 2); + randType = map_rng.rand() % (Category::CATEGORY_MAX - 3); if ( randType >= MAGICSTAFF ) { randType++; } } entity->skill[10] = itemLevelCurve(static_cast(randType), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } } else { entity->skill[10] = itemLevelCurve(FOOD, 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } } } @@ -5841,6 +7521,7 @@ void assignActions(map_t* map) if ( entity->skill[16] > 0 && entity->skill[16] <= 13 ) { entity->skill[10] = itemLevelCurve(static_cast(entity->skill[16] - 1), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } else { @@ -5852,10 +7533,12 @@ void assignActions(map_t* map) if ( randType == 0 ) { entity->skill[10] = itemLevelCurve(static_cast(WEAPON), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } else if ( randType == 1 ) { entity->skill[10] = itemLevelCurve(static_cast(ARMOR), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } } else if ( entity->skill[16] == 15 ) @@ -5865,10 +7548,12 @@ void assignActions(map_t* map) if ( randType == 0 ) { entity->skill[10] = itemLevelCurve(static_cast(AMULET), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } else { entity->skill[10] = itemLevelCurve(static_cast(RING), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } } else if ( entity->skill[16] == 16 ) @@ -5878,14 +7563,17 @@ void assignActions(map_t* map) if ( randType == 0 ) { entity->skill[10] = itemLevelCurve(static_cast(SCROLL), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } else if ( randType == 1 ) { entity->skill[10] = itemLevelCurve(static_cast(MAGICSTAFF), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } else { entity->skill[10] = itemLevelCurve(static_cast(SPELLBOOK), 0, currentlevel, map_rng); + rolledLevelCurveItem = true; } } } @@ -6004,10 +7692,10 @@ void assignActions(map_t* map) { entity->skill[14] = 75 + 25 * (map_rng.rand() % 2); // appearance } - else if ( entity->skill[10] >= BRONZE_TOMAHAWK && entity->skill[10] <= CRYSTAL_SHURIKEN ) + + if ( rolledLevelCurveItem ) { - // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) - entity->skill[11] = std::min(DECREPIT + (entity->skill[10] - BRONZE_TOMAHAWK), static_cast(EXCELLENT)); + itemLevelCurvePostProcess(entity, nullptr, map_rng); } auto item = newItemFromEntity(entity); @@ -6129,6 +7817,12 @@ void assignActions(map_t* map) case 166: case 188: case 189: + case 204: + case 205: + case 206: + case 207: + case 246: + case 247: { entity->sizex = 4; entity->sizey = 4; @@ -6247,6 +7941,11 @@ void assignActions(map_t* map) monsterCurveCustomManager.createMonsterFromFile(entity, myStats, variantName, monsterType); } } + else if ( checkName.find("$npc=") != std::string::npos ) + { + myStats->setAttribute("special_npc", checkName.substr(strlen("$npc="))); + strcpy(myStats->name, MonsterData_t::getSpecialNPCName(*myStats).c_str()); + } } if ( multiplayer != CLIENT ) { @@ -6634,7 +8333,7 @@ void assignActions(map_t* map) entity->flags[SPRITE] = true; entity->flags[INVISIBLE] = true; entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; + //entity->flags[NOUPDATE] = true; entity->skill[28] = 1; // is a mechanism entity->skill[1] = QUIVER_SILVER + map_rng.rand() % 7; // random arrow type. if ( currentlevel <= 15 ) @@ -6784,6 +8483,7 @@ void assignActions(map_t* map) { childEntity->z = -10.99; } + childEntity->parent = entity->getUID(); childEntity->behavior = &actBoulderTrapHole; TileEntityList.addEntity(*childEntity); entity->boulderTrapRocksToSpawn |= (1 << c); // add this location to spawn a boulder below the trapdoor model. @@ -6952,9 +8652,9 @@ void assignActions(map_t* map) childEntity->y = entity->y - 8; TileEntityList.addEntity(*childEntity); //printlog("31 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); - childEntity->z = 0; - childEntity->itemNotMoving = 1; // so the item retains its position - childEntity->itemNotMovingClient = 1; // so the item retains its position for clients + //childEntity->z = 0; + //childEntity->itemNotMoving = 1; // so the item retains its position + //childEntity->itemNotMovingClient = 1; // so the item retains its position for clients entity->parent = childEntity->getUID(); } @@ -7254,6 +8954,7 @@ void assignActions(map_t* map) { childEntity->z = -10.99; } + childEntity->parent = entity->getUID(); childEntity->behavior = &actBoulderTrapHole; TileEntityList.addEntity(*childEntity); } @@ -7295,6 +8996,7 @@ void assignActions(map_t* map) { childEntity->z = -10.99; } + childEntity->parent = entity->getUID(); childEntity->behavior = &actBoulderTrapHole; TileEntityList.addEntity(*childEntity); } @@ -7336,6 +9038,7 @@ void assignActions(map_t* map) { childEntity->z = -10.99; } + childEntity->parent = entity->getUID(); childEntity->behavior = &actBoulderTrapHole; TileEntityList.addEntity(*childEntity); } @@ -7377,6 +9080,7 @@ void assignActions(map_t* map) { childEntity->z = -10.99; } + childEntity->parent = entity->getUID(); childEntity->behavior = &actBoulderTrapHole; TileEntityList.addEntity(*childEntity); } @@ -7814,7 +9518,7 @@ void assignActions(map_t* map) entity->flags[SPRITE] = true; entity->flags[INVISIBLE] = true; entity->flags[PASSABLE] = true; - entity->flags[NOUPDATE] = true; + //entity->flags[NOUPDATE] = true; entity->skill[28] = 1; // is a mechanism entity->spellTrapRefireRate = entity->spellTrapRefireRate * TICKS_PER_SECOND; // convert seconds to ticks from editor entity->seedEntityRNG(map_server_rng.getU32()); @@ -8053,13 +9757,71 @@ void assignActions(map_t* map) entity->z = 7.5 - entity->floorDecorationHeightOffset * 0.25; entity->x += entity->floorDecorationXOffset * 0.25; entity->y += entity->floorDecorationYOffset * 0.25; + int rotation = entity->floorDecorationRotation; if ( entity->floorDecorationRotation == -1 ) { - entity->yaw = (map_rng.rand() % 8) * (PI / 4); + rotation = map_rng.rand() % 8; + entity->yaw = (rotation) * (PI / 4); } else { - entity->yaw = entity->floorDecorationRotation * (PI / 4); + entity->yaw = entity->floorDecorationRotation * (PI / 4); + } + if ( entity->floorDecorationDestroyIfNoWall == 8 ) + { + // match rotation + if ( rotation >= 4 ) + { + entity->floorDecorationDestroyIfNoWall = rotation - 4; + } + else + { + entity->floorDecorationDestroyIfNoWall = rotation + 4; + } + } + bool modifiedFocal = false; + if ( entity->x < 0.0 ) + { + while ( entity->x < 0.0 ) + { + entity->x += 16.0; + entity->focalx -= 16.0; + } + modifiedFocal = true; + } + if ( entity->y < 0 ) + { + while ( entity->y < 0.0 ) + { + entity->y += 16.0; + entity->focaly -= 16.0; + } + modifiedFocal = true; + } + if ( static_cast(entity->x) >= map->width * 16 ) + { + while ( static_cast(entity->x) >= map->width * 16 ) + { + entity->x -= 16.0; + entity->focalx += 16.0; + } + modifiedFocal = true; + } + if ( static_cast(entity->y) >= map->height * 16 ) + { + while ( static_cast(entity->y) >= map->height * 16 ) + { + entity->y -= 16.0; + entity->focaly += 16.0; + } + modifiedFocal = true; + } + if ( modifiedFocal ) + { + real_t fx = entity->focalx; + real_t fy = entity->focaly; + entity->focalx = fx * cos(entity->yaw) - fy * cos(entity->yaw + PI / 2); + entity->focaly = -fx * sin(entity->yaw) + fy * sin(entity->yaw + PI / 2); } entity->flags[BLOCKSIGHT] = false; entity->flags[PASSABLE] = true; @@ -8102,6 +9864,11 @@ void assignActions(map_t* map) case RACE_TROLL: victoryType = 3; break; case RACE_SPIDER: victoryType = 3; break; case RACE_IMP: victoryType = 5; break; + case RACE_DRYAD: victoryType = 4; break; + case RACE_MYCONID: victoryType = 4; break; + case RACE_GREMLIN: victoryType = 5; break; + case RACE_SALAMANDER: victoryType = 4; break; + case RACE_GNOME: victoryType = 4; break; } entity->portalVictoryType = victoryType; entity->skill[28] = 1; // is a mechanism @@ -8237,352 +10004,635 @@ void assignActions(map_t* map) { buf[totalChars] = '\0'; } - std::string output = buf; - size_t found = output.find("\\n"); - while ( found != std::string::npos ) + std::string output = buf; + size_t found = output.find("\\n"); + while ( found != std::string::npos ) + { + output.erase(found, 2); + output.insert(found, 1, '\n'); + found = output.find("\\n"); + } + strcpy(buf, output.c_str()); + + int index = -1; + bool foundBook = false; + for ( auto& book : allBooks ) + { + ++index; + if ( book.default_name == buf ) + { + foundBook = true; + entity->skill[14] = getBook(buf); + break; + } + } + if ( !foundBook && allBooks.size() > 0 ) + { + entity->skill[14] = map_rng.rand() % allBooks.size(); + } + + if ( entity->skill[15] == 1 ) // editor set as identified + { + entity->skill[15] = 1; + } + else if ( entity->skill[15] == 0 ) // unidentified (default) + { + entity->skill[15] = 0; + } + else if ( entity->skill[15] == 2 ) // editor set as random + { + entity->skill[15] = map_rng.rand() % 2; + } + else + { + entity->skill[15] = 0; // unidentified. + } + + auto item = newItemFromEntity(entity); + entity->sprite = itemModel(item); + if ( !entity->itemNotMoving ) + { + entity->z = 7.5 - models[entity->sprite]->sizey * .25; + } + entity->itemNotMoving = 1; // so the item retains its position + entity->itemNotMovingClient = 1; // so the item retains its position for clients + free(item); + item = nullptr; + break; + } + case 168: + //Statue Animator + entity->sizex = 4; + entity->sizey = 4; + entity->x += 8; + entity->y += 8; + entity->z = 3.5; + entity->behavior = &actStatueAnimator; + entity->sprite = 995; + entity->skill[0] = 0; + break; + case 169: + //Statue + entity->sizex = 4; + entity->sizey = 4; + entity->x += 8; + entity->y += 8; + entity->z = 3.5; + entity->behavior = &actStatue; + entity->sprite = 995; + entity->yaw = entity->statueDir * PI / 2; + break; + case 177: + // teleport shrine + entity->sizex = 4; + entity->sizey = 4; + entity->x += 8; + entity->y += 8; + entity->z = 3.5 - entity->shrineZ * 0.25; + entity->behavior = &actTeleportShrine; + entity->sprite = 1192; + entity->yaw = entity->shrineDir * PI / 2; + break; + case 178: + // spell shrine + entity->sizex = 4; + entity->sizey = 4; + entity->x += 8; + entity->y += 8; + entity->z = 3.5 - entity->shrineZ * 0.25; + //entity->behavior = &actSpellShrine; + entity->sprite = 1193; + entity->yaw = entity->shrineDir * PI / 2; + break; + case 179: + { + // collider decoration + entity->x += 8; + entity->y += 8; + Entity::colliderAssignProperties(entity, true, map); + break; + } + //AND gate + case 185: + case 186: + case 187: + { + entity->sizex = 2; + entity->sizey = 2; + entity->x += 8; + entity->y += 8; + entity->behavior = &actSignalGateAND; + entity->flags[SPRITE] = true; + entity->flags[INVISIBLE] = true; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->skill[28] = 1; // is a mechanism + if ( entity->sprite == 186 ) { entity->signalInputDirection += 4; } + if ( entity->sprite == 187 ) { entity->signalInputDirection += 8; } + entity->sprite = -1; + break; + } + case 190: + entity->x += 8; + entity->y += 8; + entity->sizex = 4; + entity->sizey = 4; + entity->behavior = &actDaedalusShrine; + entity->flags[PASSABLE] = false; + entity->z = -0.25; + entity->sprite = 1481; + //entity->focalx = 0.75; + entity->yaw = (map_rng.rand() % 360) * PI / 180.0; + entity->seedEntityRNG(map_rng.getU32()); + { + Entity* childEntity = newEntity(1480, 1, map->entities, nullptr); // base + childEntity->parent = entity->getUID(); + childEntity->x = entity->x; + childEntity->y = entity->y; + childEntity->z = entity->z + 6.5; + childEntity->yaw = 0.0; + childEntity->sizex = 4; + childEntity->sizey = 4; + childEntity->flags[PASSABLE] = true; + childEntity->flags[UNCLICKABLE] = false; + TileEntityList.addEntity(*childEntity); + //node_t* tempNode = list_AddNodeLast(&entity->children); + //tempNode->element = childEntity; // add the node to the children list. + //tempNode->deconstructor = &emptyDeconstructor; + //tempNode->size = sizeof(Entity*); + } + break; + case 191: + { + entity->x += 8; + entity->y += 8; + entity->sizex = 2; + entity->sizey = 2; + entity->z = 0.0; + entity->behavior = &actBell; + entity->flags[PASSABLE] = true; + entity->flags[BLOCKSIGHT] = false; + entity->flags[BURNABLE] = true; + entity->sprite = 1478; // rope + entity->seedEntityRNG(map_rng.getU32()); + entity->skill[11] = map_rng.rand(); // buff type + { + Entity* childEntity = newEntity(1475, 1, map->entities, nullptr); // bell + childEntity->parent = entity->getUID(); + childEntity->x = entity->x - 2 * cos(entity->yaw); + childEntity->y = entity->y - 2 * sin(entity->yaw); + childEntity->z = -22.25; + childEntity->yaw = entity->yaw; + childEntity->sizex = 6; + childEntity->sizey = 6; + childEntity->flags[PASSABLE] = true; + childEntity->flags[UNCLICKABLE] = false; + childEntity->flags[UPDATENEEDED] = true; + childEntity->flags[NOCLIP_CREATURES] = true; + childEntity->z = entity->z; + TileEntityList.addEntity(*childEntity); + node_t* tempNode = list_AddNodeLast(&entity->children); + tempNode->element = childEntity; // add the node to the children list. + tempNode->deconstructor = &emptyDeconstructor; + tempNode->size = sizeof(Entity*); + } + + auto& bellRng = entity->entity_rng ? *entity->entity_rng : map_rng; + int roll = bellRng.rand() % 4; + if ( roll == 0 ) + { + Entity* itemEntity = newEntity(8, 1, map->entities, nullptr); // item + setSpriteAttributes(itemEntity, nullptr, nullptr); + itemEntity->x = entity->x - 8.0; + itemEntity->y = entity->y - 8.0; + itemEntity->z = -16; + itemEntity->flags[INVISIBLE] = true; + itemEntity->itemContainer = entity->getUID(); + itemEntity->yaw = entity->yaw; + itemEntity->skill[16] = SPELLBOOK + 1; + entity->skill[1] = itemEntity->getUID(); + } + else if ( roll == 1 ) + { + Entity* goldEntity = newEntity(9, 1, map->entities, nullptr); // gold + goldEntity->x = entity->x - 8.0; + goldEntity->y = entity->y - 8.0; + goldEntity->z = -16; + goldEntity->goldAmount = 50 + bellRng.rand() % 50; + goldEntity->flags[INVISIBLE] = true; + entity->skill[1] = goldEntity->getUID(); + } + } + break; + case 201: + entity->x += 8; + entity->y += 8; + entity->sizex = 4; + entity->sizey = 4; + entity->behavior = &actAssistShrine; + entity->flags[PASSABLE] = false; + entity->z = 8.0; + entity->sprite = 1484; + //entity->focalx = 0.75; + entity->yaw = 0.0;// (270)* PI / 180.0; + entity->seedEntityRNG(map_rng.getU32()); + break; + case 208: + case 209: + case 210: + case 211: + { + entity->wallLockDir = entity->sprite - 208; + entity->sizex = 1; + entity->sizey = 1; + entity->x += 8; + entity->y += 8; + entity->z = 0.0; + entity->behavior = &actWallLock; + entity->sprite = 1161; // stone base + int keySprite = 1592; + switch ( entity->wallLockMaterial ) + { + case 0: + entity->sprite = 1161; + keySprite = 1592; + break; + case 1: + entity->sprite = 1154; + keySprite = 1585; + break; + case 2: + entity->sprite = 1155; + keySprite = 1586; + break; + case 3: + entity->sprite = 1158; + keySprite = 1589; + break; + case 4: + entity->sprite = 1160; + keySprite = 1591; + break; + case 5: + entity->sprite = 1157; + keySprite = 1588; + break; + case 6: + entity->sprite = 1156; + keySprite = 1587; + break; + case 7: + entity->sprite = 1159; + keySprite = 1590; + break; + default: + break; + } + entity->flags[PASSABLE] = true; + entity->flags[BLOCKSIGHT] = false; + entity->yaw = (PI / 2) * entity->wallLockDir; + const real_t offsetWallDist = 7.25; + + int nodigtilex = (static_cast(entity->x) >> 4); + int nodigtiley = (static_cast(entity->y) >> 4); + + switch ( entity->wallLockDir ) { - output.erase(found, 2); - output.insert(found, 1, '\n'); - found = output.find("\\n"); + case 0: + entity->x -= offsetWallDist; + nodigtilex -= 1; + break; + case 1: + entity->y -= offsetWallDist; + nodigtiley -= 1; + break; + case 2: + entity->x += offsetWallDist; + nodigtilex += 1; + break; + case 3: + entity->y += offsetWallDist; + nodigtiley += 1; + break; + default: + break; } - strcpy(buf, output.c_str()); - int index = -1; - bool foundBook = false; - for ( auto& book : allBooks ) + map->tileAttributes[OBSTACLELAYER + (nodigtiley) + *MAPLAYERS + (nodigtilex) + *MAPLAYERS * map->height] |= map_t::TILE_ATTRIBUTE_NODIG; + { - ++index; - if ( book.default_name == buf ) + Entity* childEntity = newEntity(keySprite, 1, map->entities, nullptr); // lock + childEntity->parent = entity->getUID(); + childEntity->x = entity->x + 4 * cos(entity->yaw); + childEntity->y = entity->y + 4 * sin(entity->yaw); + childEntity->z = entity->z - 0.5; + if ( entity->sprite == 1154 ) { - foundBook = true; - entity->skill[14] = getBook(buf); - break; + childEntity->z += 1.0; } + else if ( entity->sprite == 1156 ) + { + childEntity->z -= 0.25; + } + childEntity->yaw = entity->yaw + PI; + childEntity->sizex = 1; + childEntity->sizey = 1; + childEntity->flags[PASSABLE] = true; + childEntity->flags[BLOCKSIGHT] = false; + childEntity->flags[UNCLICKABLE] = false; + childEntity->flags[PASSABLE] = true; + childEntity->flags[INVISIBLE] = true; + node_t* tempNode = list_AddNodeLast(&entity->children); + tempNode->element = childEntity; // add the node to the children list. + tempNode->deconstructor = &emptyDeconstructor; + tempNode->size = sizeof(Entity*); } - if ( !foundBook && allBooks.size() > 0 ) - { - entity->skill[14] = map_rng.rand() % allBooks.size(); - } - - if ( entity->skill[15] == 1 ) // editor set as identified - { - entity->skill[15] = 1; - } - else if ( entity->skill[15] == 0 ) // unidentified (default) - { - entity->skill[15] = 0; - } - else if ( entity->skill[15] == 2 ) // editor set as random - { - entity->skill[15] = map_rng.rand() % 2; - } - else + } + break; + case 212: + case 213: + case 214: + case 215: + { + entity->wallLockDir = entity->sprite - 212; + entity->sizex = 5; + entity->sizey = 3; + entity->x += 8; + entity->y += 8; + entity->z = 0.0; + entity->behavior = &actWallButton; + entity->sprite = 1153; // stone base + entity->flags[PASSABLE] = true; + entity->flags[BLOCKSIGHT] = false; + entity->yaw = (PI / 2) * entity->wallLockDir; + const real_t offsetWallDist = 7.25; + + int nodigtilex = (static_cast(entity->x) >> 4); + int nodigtiley = (static_cast(entity->y) >> 4); + + switch ( entity->wallLockDir ) { - entity->skill[15] = 0; // unidentified. + case 0: + entity->x -= offsetWallDist; + entity->sizey = 2; + nodigtilex -= 1; + break; + case 1: + entity->y -= offsetWallDist; + entity->sizex = 2; + nodigtiley -= 1; + break; + case 2: + entity->x += offsetWallDist; + entity->sizey = 2; + nodigtilex += 1; + break; + case 3: + entity->y += offsetWallDist; + entity->sizex = 2; + nodigtiley += 1; + break; + default: + break; } - auto item = newItemFromEntity(entity); - entity->sprite = itemModel(item); - if ( !entity->itemNotMoving ) + map->tileAttributes[OBSTACLELAYER + (nodigtiley) + * MAPLAYERS + (nodigtilex) + * MAPLAYERS * map->height] |= map_t::TILE_ATTRIBUTE_NODIG; + { - entity->z = 7.5 - models[entity->sprite]->sizey * .25; + Entity* childEntity = newEntity(1152, 1, map->entities, nullptr); // button + childEntity->parent = entity->getUID(); + childEntity->x = entity->x + 4 * cos(entity->yaw); + childEntity->y = entity->y + 4 * sin(entity->yaw); + childEntity->z = entity->z - 0.5; + childEntity->yaw = entity->yaw; + childEntity->sizex = 1; + childEntity->sizey = 1; + childEntity->flags[PASSABLE] = true; + childEntity->flags[BLOCKSIGHT] = false; + childEntity->flags[UNCLICKABLE] = false; + childEntity->flags[PASSABLE] = true; + node_t* tempNode = list_AddNodeLast(&entity->children); + tempNode->element = childEntity; // add the node to the children list. + tempNode->deconstructor = &emptyDeconstructor; + tempNode->size = sizeof(Entity*); } - entity->itemNotMoving = 1; // so the item retains its position - entity->itemNotMovingClient = 1; // so the item retains its position for clients - free(item); - item = nullptr; - break; } - case 168: - //Statue Animator - entity->sizex = 4; - entity->sizey = 4; - entity->x += 8; - entity->y += 8; - entity->z = 3.5; - entity->behavior = &actStatueAnimator; - entity->sprite = 995; - entity->skill[0] = 0; - break; - case 169: - //Statue - entity->sizex = 4; - entity->sizey = 4; - entity->x += 8; - entity->y += 8; - entity->z = 3.5; - entity->behavior = &actStatue; - entity->sprite = 995; - entity->yaw = entity->statueDir * PI / 2; break; - case 177: - // teleport shrine - entity->sizex = 4; - entity->sizey = 4; - entity->x += 8; - entity->y += 8; - entity->z = 3.5 - entity->shrineZ * 0.25; - entity->behavior = &actTeleportShrine; - entity->sprite = 1192; - entity->yaw = entity->shrineDir * PI / 2; + case 216: // nodig tile + map->tileAttributes[OBSTACLELAYER + (static_cast(entity->y) >> 4) + * MAPLAYERS + (static_cast(entity->x) >> 4) + * MAPLAYERS * map->height] |= map_t::TILE_ATTRIBUTE_NODIG; + list_RemoveNode(entity->mynode); + entity = nullptr; break; - case 178: - // spell shrine - entity->sizex = 4; - entity->sizey = 4; + // east/west iron door: + case 217: + { entity->x += 8; entity->y += 8; - entity->z = 3.5 - entity->shrineZ * 0.25; - //entity->behavior = &actSpellShrine; - entity->sprite = 1193; - entity->yaw = entity->shrineDir * PI / 2; + entity->yaw -= PI / 2.0; + entity->sprite = doorFrameSprite(); + entity->flags[PASSABLE] = true; + entity->behavior = &actDoorFrame; + auto childEntity = newEntity(1162, 0, map->entities, nullptr); //Door frame entity. + childEntity->x = entity->x; + childEntity->y = entity->y; + TileEntityList.addEntity(*childEntity); + //printlog("19 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); + childEntity->sizex = 8; + childEntity->sizey = 1; + childEntity->yaw -= PI / 2.0; + childEntity->behavior = &actIronDoor; + childEntity->flags[BLOCKSIGHT] = true; + childEntity->skill[28] = 1; //It's a mechanism. + childEntity->skill[0] = 1; // signify behavior code of DOOR_DIR + childEntity->seedEntityRNG(map_server_rng.getU32()); + + // copy editor options from frame to door itself. + childEntity->doorDisableLockpicks = entity->doorDisableLockpicks; + childEntity->doorForceLockedUnlocked = entity->doorForceLockedUnlocked; + childEntity->doorDisableOpening = entity->doorDisableOpening; + childEntity->doorUnlockWhenPowered = entity->doorUnlockWhenPowered; + + childEntity = newEntity(doorFrameSprite(), 0, map->entities, nullptr); //Door entity. + childEntity->flags[INVISIBLE] = true; + childEntity->flags[BLOCKSIGHT] = true; + childEntity->x = entity->x - 7; + childEntity->y = entity->y; + childEntity->yaw -= PI / 2.0; + + TileEntityList.addEntity(*childEntity); + //printlog("20 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); + childEntity->sizex = 2; + childEntity->sizey = 2; + childEntity->behavior = &actDoorFrame; + + childEntity = newEntity(doorFrameSprite(), 0, map->entities, nullptr); //Door frame entity. + childEntity->flags[INVISIBLE] = true; + childEntity->flags[BLOCKSIGHT] = true; + childEntity->x = entity->x + 7; + childEntity->y = entity->y; + childEntity->yaw -= PI / 2.0; + + TileEntityList.addEntity(*childEntity); + //printlog("21 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); + childEntity->sizex = 2; + childEntity->sizey = 2; + childEntity->behavior = &actDoorFrame; break; - case 179: + } + // north/south door: + case 218: { - // collider decoration entity->x += 8; entity->y += 8; - int dir = entity->colliderDecorationRotation; - if ( dir == -1 ) - { - dir = map_rng.rand() % 8; - entity->colliderDecorationRotation = dir; - } - entity->yaw = dir * (PI / 4); - /*static ConsoleVariable debugColliderType("/collider_type", 14); - entity->colliderDamageTypes = *debugColliderType;*/ - auto find = EditorEntityData_t::colliderData.find(entity->colliderDamageTypes); - if ( find != EditorEntityData_t::colliderData.end() ) - { - auto& data = find->second; - if ( data.hasOverride("dir_offset") ) - { - entity->yaw = ((dir + data.getOverride("dir_offset")) * (PI / 4)); - } - if ( data.hasOverride("model") ) - { - entity->colliderDecorationModel = data.getOverride("model"); - } - if ( data.hasOverride("height") ) - { - entity->colliderDecorationHeightOffset = data.getOverride("height"); - } - if ( dir == 0 ) - { - if ( data.hasOverride("east_x") ) - { - entity->colliderDecorationXOffset = data.getOverride("east_x"); - } - if ( data.hasOverride("east_y") ) - { - entity->colliderDecorationYOffset = data.getOverride("east_y"); - } - } - else if ( dir == 2 ) - { - if ( data.hasOverride("south_x") ) - { - entity->colliderDecorationXOffset = data.getOverride("south_x"); - } - if ( data.hasOverride("south_y") ) - { - entity->colliderDecorationYOffset = data.getOverride("south_y"); - } - } - else if ( dir == 4 ) - { - if ( data.hasOverride("west_x") ) - { - entity->colliderDecorationXOffset = data.getOverride("west_x"); - } - if ( data.hasOverride("west_y") ) - { - entity->colliderDecorationYOffset = data.getOverride("west_y"); - } - } - else if ( dir == 6 ) - { - if ( data.hasOverride("north_x") ) - { - entity->colliderDecorationXOffset = data.getOverride("north_x"); - } - if ( data.hasOverride("north_y") ) - { - entity->colliderDecorationYOffset = data.getOverride("north_y"); - } - } - if ( data.hasOverride("collision") ) - { - entity->colliderHasCollision = data.getOverride("collision"); - } - if ( data.hasOverride("collision_x") ) - { - entity->colliderSizeX = data.getOverride("collision_x"); - } - if ( data.hasOverride("collision_y") ) - { - entity->colliderSizeY = data.getOverride("collision_y"); - } - if ( data.hasOverride("hp") ) - { - entity->colliderMaxHP = data.getOverride("hp"); - } - if ( data.hasOverride("diggable") ) - { - entity->colliderDiggable = data.getOverride("diggable"); - } - } + entity->sprite = doorFrameSprite(); + entity->flags[PASSABLE] = true; + entity->behavior = &actDoorFrame; + auto childEntity = newEntity(1162, 0, map->entities, nullptr); //Door frame entity. + childEntity->x = entity->x; + childEntity->y = entity->y; + TileEntityList.addEntity(*childEntity); + //printlog("16 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); + childEntity->sizex = 1; + childEntity->sizey = 8; + childEntity->behavior = &actIronDoor; + childEntity->flags[BLOCKSIGHT] = true; + childEntity->skill[28] = 1; //It's a mechanism. + childEntity->skill[0] = 0; // signify behavior code of DOOR_DIR + childEntity->seedEntityRNG(map_server_rng.getU32()); + + // copy editor options from frame to door itself. + childEntity->doorDisableLockpicks = entity->doorDisableLockpicks; + childEntity->doorForceLockedUnlocked = entity->doorForceLockedUnlocked; + childEntity->doorDisableOpening = entity->doorDisableOpening; + childEntity->doorUnlockWhenPowered = entity->doorUnlockWhenPowered; - entity->sprite = entity->colliderDecorationModel; - entity->sizex = entity->colliderSizeX; - entity->sizey = entity->colliderSizeY; - entity->x += entity->colliderDecorationXOffset * 0.25; - entity->y += entity->colliderDecorationYOffset * 0.25; - entity->z = 7.5 - entity->colliderDecorationHeightOffset * 0.25; + childEntity = newEntity(doorFrameSprite(), 0, map->entities, nullptr); //Door entity. + childEntity->flags[INVISIBLE] = true; + childEntity->flags[BLOCKSIGHT] = true; + childEntity->x = entity->x; + childEntity->y = entity->y - 7; + TileEntityList.addEntity(*childEntity); - entity->flags[PASSABLE] = entity->colliderHasCollision == 0; - entity->flags[BLOCKSIGHT] = false; - entity->behavior = &actColliderDecoration; - entity->colliderCurrentHP = entity->colliderMaxHP; - entity->colliderOldHP = entity->colliderMaxHP; - if ( entity->isDamageableCollider() ) - { - entity->flags[UNCLICKABLE] = false; - } - else - { - entity->flags[UNCLICKABLE] = true; - } - /*if ( multiplayer != CLIENT ) - { - entity_uids--; - } - entity->setUID(-3);*/ + //printlog("17 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); + childEntity->sizex = 2; + childEntity->sizey = 2; + childEntity->behavior = &actDoorFrame; + childEntity = newEntity(doorFrameSprite(), 0, map->entities, nullptr); //Door frame entity. + childEntity->flags[INVISIBLE] = true; + childEntity->flags[BLOCKSIGHT] = true; + childEntity->x = entity->x; + childEntity->y = entity->y + 7; + TileEntityList.addEntity(*childEntity); + //printlog("18 Generated entity. Sprite: %d Uid: %d X: %.2f Y: %.2f\n",childEntity->sprite,childEntity->getUID(),childEntity->x,childEntity->y); + childEntity->sizex = 2; + childEntity->sizey = 2; + childEntity->behavior = &actDoorFrame; break; } - //AND gate - case 185: - case 186: - case 187: + case 219: // slippery tile + map->tileAttributes[0 + (static_cast(entity->y) >> 4) + * MAPLAYERS + (static_cast(entity->x) >> 4) + * MAPLAYERS * map->height] |= map_t::TILE_ATTRIBUTE_SLIPPERY; + list_RemoveNode(entity->mynode); + entity = nullptr; + break; + case 220: // wind { - entity->sizex = 2; - entity->sizey = 2; entity->x += 8; entity->y += 8; - entity->behavior = &actSignalGateAND; - entity->flags[SPRITE] = true; + entity->sprite = -1; entity->flags[INVISIBLE] = true; + entity->behavior = &actWind; + int dir = entity->skill[0]; + if ( dir == -1 ) + { + dir = map_rng.rand() % 8; + } + entity->yaw = dir * PI / 4; + entity->skill[0] = 0; + entity->sizex = 6; + entity->sizey = 6; entity->flags[PASSABLE] = true; entity->flags[NOUPDATE] = true; - entity->skill[28] = 1; // is a mechanism - if ( entity->sprite == 186 ) { entity->signalInputDirection += 4; } - if ( entity->sprite == 187 ) { entity->signalInputDirection += 8; } - entity->sprite = -1; + entity->flags[UPDATENEEDED] = false; break; } - case 190: + case 221: // slow tile + map->tileAttributes[0 + (static_cast(entity->y) >> 4) + * MAPLAYERS + (static_cast(entity->x) >> 4) + * MAPLAYERS * map->height] |= map_t::TILE_ATTRIBUTE_SLOW; + list_RemoveNode(entity->mynode); + entity = nullptr; + break; + case 254: // leaves entity->x += 8; entity->y += 8; + entity->sprite = 1913; + entity->z = 0.0; + entity->yaw = map_rng.rand() % 360 * (PI / 180.0); entity->sizex = 4; entity->sizey = 4; - entity->behavior = &actDaedalusShrine; + entity->behavior = &actLeafPile; + entity->skill[0] = 0; + entity->skill[10] = 1; // denote map gen + entity->flags[NOCLIP_CREATURES] = true; + entity->flags[UPDATENEEDED] = true; + entity->flags[NOUPDATE] = false; + entity->flags[PASSABLE] = true; + entity->flags[UNCLICKABLE] = true; + break; + case 300: + entity->sizex = 4; + entity->sizey = 4; + entity->x += 8; + entity->y += 8; + entity->z = 7.5; entity->flags[PASSABLE] = false; - entity->z = -0.25; - entity->sprite = 1481; - //entity->focalx = 0.75; - entity->yaw = (map_rng.rand() % 360) * PI / 180.0; + entity->behavior = &actCauldron; + entity->yaw = entity->yaw * (PI / 2); // rotate as set in editor + entity->sprite = 1622; // firepit entity->seedEntityRNG(map_rng.getU32()); - { - Entity* childEntity = newEntity(1480, 1, map->entities, nullptr); // base - childEntity->parent = entity->getUID(); - childEntity->x = entity->x; - childEntity->y = entity->y; - childEntity->z = entity->z + 6.5; - childEntity->yaw = 0.0; - childEntity->sizex = 4; - childEntity->sizey = 4; - childEntity->flags[PASSABLE] = true; - childEntity->flags[UNCLICKABLE] = false; - TileEntityList.addEntity(*childEntity); - //node_t* tempNode = list_AddNodeLast(&entity->children); - //tempNode->element = childEntity; // add the node to the children list. - //tempNode->deconstructor = &emptyDeconstructor; - //tempNode->size = sizeof(Entity*); - } break; - case 191: - { + case 301: + entity->sizex = 4; + entity->sizey = 4; entity->x += 8; entity->y += 8; - entity->sizex = 2; - entity->sizey = 2; - entity->z = 0.0; - entity->behavior = &actBell; - entity->flags[PASSABLE] = true; - entity->flags[BLOCKSIGHT] = false; - entity->flags[BURNABLE] = true; - entity->sprite = 1478; // rope + entity->z = 7.5; + entity->flags[PASSABLE] = false; + entity->behavior = &actWorkbench; + entity->yaw = entity->yaw * (PI / 2); // rotate as set in editor + entity->sprite = 1617; entity->seedEntityRNG(map_rng.getU32()); - entity->skill[11] = map_rng.rand(); // buff type - { - Entity* childEntity = newEntity(1475, 1, map->entities, nullptr); // bell - childEntity->parent = entity->getUID(); - childEntity->x = entity->x - 2 * cos(entity->yaw); - childEntity->y = entity->y - 2 * sin(entity->yaw); - childEntity->z = -22.25; - childEntity->yaw = entity->yaw; - childEntity->sizex = 6; - childEntity->sizey = 6; - childEntity->flags[PASSABLE] = true; - childEntity->flags[UNCLICKABLE] = false; - childEntity->flags[UPDATENEEDED] = true; - childEntity->flags[NOCLIP_CREATURES] = true; - childEntity->z = entity->z; - TileEntityList.addEntity(*childEntity); - node_t* tempNode = list_AddNodeLast(&entity->children); - tempNode->element = childEntity; // add the node to the children list. - tempNode->deconstructor = &emptyDeconstructor; - tempNode->size = sizeof(Entity*); - } - - auto& bellRng = entity->entity_rng ? *entity->entity_rng : map_rng; - int roll = bellRng.rand() % 4; - if ( roll == 0 ) - { - Entity* itemEntity = newEntity(8, 1, map->entities, nullptr); // item - setSpriteAttributes(itemEntity, nullptr, nullptr); - itemEntity->x = entity->x - 8.0; - itemEntity->y = entity->y - 8.0; - itemEntity->z = -16; - itemEntity->flags[INVISIBLE] = true; - itemEntity->itemContainer = entity->getUID(); - itemEntity->yaw = entity->yaw; - itemEntity->skill[16] = SPELLBOOK + 1; - entity->skill[1] = itemEntity->getUID(); - } - else if ( roll == 1 ) - { - Entity* goldEntity = newEntity(9, 1, map->entities, nullptr); // gold - goldEntity->x = entity->x - 8.0; - goldEntity->y = entity->y - 8.0; - goldEntity->z = -16; - goldEntity->goldAmount = 50 + bellRng.rand() % 50; - goldEntity->flags[INVISIBLE] = true; - entity->skill[1] = goldEntity->getUID(); - } - } break; - case 201: + case 302: // yellow mailbox + entity->sizex = 4; + entity->sizey = 4; entity->x += 8; entity->y += 8; + entity->z = 7.5; + entity->flags[PASSABLE] = false; + entity->behavior = &actMailbox; + entity->yaw = entity->yaw * (PI / 2); // rotate as set in editor + entity->sprite = 1619; + entity->seedEntityRNG(map_rng.getU32()); + break; + case 303: // blue mailbox entity->sizex = 4; entity->sizey = 4; - entity->behavior = &actAssistShrine; + entity->x += 8; + entity->y += 8; + entity->z = 7.5; entity->flags[PASSABLE] = false; - entity->z = 8.0; - entity->sprite = 1484; - //entity->focalx = 0.75; - entity->yaw = 0.0;// (270)* PI / 180.0; + entity->behavior = &actMailbox; + entity->yaw = entity->yaw * (PI / 2); // rotate as set in editor + entity->sprite = 1620; entity->seedEntityRNG(map_rng.getU32()); break; default: @@ -8690,7 +10740,7 @@ void assignActions(map_t* map) { for ( int c = 0; c < MAXPLAYERS; ++c ) { - if ( client_classes[c] == CLASS_ACCURSED ) + if ( client_classes[c] == CLASS_ACCURSED && !client_disconnected[c] ) { vampireQuestChest->chestHasVampireBook = 1; break; @@ -8699,6 +10749,12 @@ void assignActions(map_t* map) } std::vector chests; + std::vector textScripts; + static ConsoleVariable cvar_spellbookdebug("/spellbook_debug", false); + if ( currentlevel == 0 ) + { + generatedSpellbooks.clear(); + } for ( auto node = map->entities->first; node != nullptr; ) { Entity* postProcessEntity = (Entity*)node->element; @@ -8707,12 +10763,24 @@ void assignActions(map_t* map) { if ( postProcessEntity->behavior == &actTextSource ) { - textSourceScript.parseScriptInMapGeneration(*postProcessEntity); + textScripts.push_back(postProcessEntity); } if ( postProcessEntity->behavior == &actChest ) { chests.push_back(postProcessEntity); } +#ifndef NDEBUG + if ( *cvar_spellbookdebug ) + { + if ( postProcessEntity->behavior == &actItem ) + { + if ( items[postProcessEntity->skill[10]].category == SPELLBOOK ) + { + generatedSpellbooks[postProcessEntity->skill[10]] += 1; + } + } + } +#endif } } @@ -8724,7 +10792,7 @@ void assignActions(map_t* map) numMimics = 0; } - static ConsoleVariable cvar_mimic_chance("/mimic_chance", 10); + static ConsoleVariable cvar_mimic_chance("/mimic_chance", 5); static ConsoleVariable cvar_mimic_debug("/mimic_debug", false); std::vector mimics; @@ -8749,7 +10817,7 @@ void assignActions(map_t* map) Entity* chest = *it; if ( allowedGenerateMimicOnChest(chest->x / 16, chest->y / 16, *map) ) { - int chance = 10; + int chance = 5; if ( svFlags & SV_FLAG_CHEATS ) { chance = std::min(100, std::max(0, *cvar_mimic_chance)); @@ -8853,21 +10921,100 @@ void assignActions(map_t* map) monsterCurveCustomManager.generateFollowersForLeaders(); } + for ( auto postProcessEntity : textScripts ) + { + textSourceScript.parseScriptInMapGeneration(*postProcessEntity); + } + +#ifndef NDEBUG + if ( *cvar_spellbookdebug ) + { + for ( auto node = map->entities->first; node != nullptr; ) + { + Entity* postProcessEntity = (Entity*)node->element; + node = node->next; + + list_t* inventory = nullptr; + if ( postProcessEntity->behavior == &actMonster ) + { + if ( Stat* myStats = postProcessEntity->getStats() ) + { + inventory = &myStats->inventory; + } + } + else if ( postProcessEntity->behavior == &actChest ) + { + if ( postProcessEntity->children.first ) + { + inventory = (list_t*)postProcessEntity->children.first->element; + } + } + + if ( inventory ) + { + for ( auto node2 = inventory->first; node2; node2 = node2->next ) + { + if ( Item* item = static_cast(node2->element) ) + { + if ( items[item->type].category == SPELLBOOK ) + { + generatedSpellbooks[item->type] += 1; + } + } + } + } + } + for ( auto spellbook : generatedSpellbooks ) + { + printlog("spellbook %s: %d", items[spellbook.first].getIdentifiedName(), spellbook.second); + } + } +#endif + keepInventoryGlobal = svFlags & SV_FLAG_KEEPINVENTORY; } -void mapLevel(int player) +int mapLevel(int player, int radius, int _x, int _y, bool usingSpell) { - int x, y; + int revealed = 0; + int x = 0; + int y = 0; for ( y = 0; y < map.height; ++y ) { for ( x = 0; x < map.width; ++x ) { + bool tileCheck = false; + if ( radius == 0 ) + { + tileCheck = true; + } + else + { + if ( minimap[y][x] ) + { + continue; + } + + if ( x >= (_x - radius) && x <= (_x + radius) + && y >= (_y - radius) && y <= (_y + radius) ) + { + if ( pow(_x - x, 2) + pow(_y - y, 2) <= radius * radius ) + { + tileCheck = true; + } + } + } + + if ( !tileCheck ) + { + continue; + } if ( map.tiles[OBSTACLELAYER + y * MAPLAYERS + x * MAPLAYERS * map.height] ) { if ( !minimap[y][x] ) { minimap[y][x] = 4; + ++revealed; } } else if ( map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height] ) @@ -8875,6 +11022,7 @@ void mapLevel(int player) if ( !minimap[y][x] ) { minimap[y][x] = 3; + ++revealed; } } else @@ -8883,6 +11031,19 @@ void mapLevel(int player) } } } + + if ( usingSpell ) + { + if ( player >= 0 && player < MAXPLAYERS ) + { + if ( players[player]->isLocalPlayer() ) + { + players[player]->mechanics.updateSustainedSpellEvent(SPELL_MAGICMAPPING, revealed, 0.2, nullptr); + } + } + } + + return revealed; } void mapLevel2(int player) @@ -8952,13 +11113,29 @@ void mapFoodOnLevel(int player) else { messagePlayerColor(player, MESSAGE_HINT, makeColorRGB(0, 255, 0),Language::get(3424)); + if ( numFood > 0 ) + { + if ( multiplayer != CLIENT ) + { + if ( players[player]->entity ) + { + players[player]->mechanics.updateSustainedSpellEvent(SPELL_DETECT_FOOD, numFood * 10, 1.0, nullptr); + } + } + else + { + magicOnSpellCastEvent(players[player]->entity, players[player]->entity, + nullptr, + SPELL_DETECT_FOOD, spell_t::SPELL_LEVEL_EVENT_DEFAULT, numFood); + } + } } } int loadMainMenuMap(bool blessedAdditionMaps, bool forceVictoryMap, int forcemap) { bool foundVictory = false; - for ( node_t* node = topscores.first; node != nullptr && !foundVictory; node = node->next ) + for ( node_t* node = topscores_json.first; node != nullptr && !foundVictory; node = node->next ) { score_t* score = (score_t*)node->element; if ( score && (score->victory == 3 || score->victory == 4 || score->victory == 5) ) @@ -8966,7 +11143,7 @@ int loadMainMenuMap(bool blessedAdditionMaps, bool forceVictoryMap, int forcemap foundVictory = true; } } - for ( node_t* node = topscoresMultiplayer.first; node != nullptr && !foundVictory; node = node->next ) + for ( node_t* node = topscoresMultiplayer_json.first; node != nullptr && !foundVictory; node = node->next ) { score_t* score = (score_t*)node->element; if ( score && (score->victory == 3 || score->victory == 4 || score->victory == 5) ) @@ -9071,4 +11248,39 @@ int loadMainMenuMap(bool blessedAdditionMaps, bool forceVictoryMap, int forcemap assert(0 && "selected invalid main menu map"); return -1; } +} + +bool map_t::tileHasAttribute(int x, int y, int layer, Uint32 attribute) +{ + auto find = tileAttributes.find(layer + y * MAPLAYERS + x * MAPLAYERS * height); + if ( find != tileAttributes.end() ) + { + return find->second & attribute; + } + return false; +} + +void map_t::setMapHDRSettings() +{ + if ( !strncmp(map.filename, "fortress", 8) ) + { + *cvar_hdrBrightness = defaultBrightness; + if ( !*MainMenu::cvar_hdrEnabled ) + { + *cvar_fogColor = { 0.7, 0.7f, 0.7f, 1.0f }; + } + else + { + *cvar_fogColor = { 1.0f, 1.0f, 1.2f, 1.0f }; + } + *cvar_fogDistance = 384.f; + *cvar_hdrLimitLow = 1.2f; + } + else + { + *cvar_hdrBrightness = defaultBrightness; + *cvar_fogColor = { 0.0f, 0.0f, 0.0f, 1.0f }; + *cvar_fogDistance = 0.0f; + *cvar_hdrLimitLow = defaultLimitLow; + } } \ No newline at end of file diff --git a/src/mechanisms.cpp b/src/mechanisms.cpp index c74eaf93f..721231bea 100644 --- a/src/mechanisms.cpp +++ b/src/mechanisms.cpp @@ -18,6 +18,7 @@ #include "player.hpp" #include "scores.hpp" #include "mod_tools.hpp" +#include "collision.hpp" //Circuits do not overlap. They connect to all their neighbors, allowing for circuits to interfere with eachother. static ConsoleVariable cvar_wire_debug("/wire_debug", false); @@ -483,6 +484,13 @@ void actTrap(Entity* my) } break; } + + if ( entity->behavior == &actMonster && entity->getMonsterTypeFromSprite() == DUCK_SMALL + && entity->monsterSpecialState == DUCK_RETURN ) + { + continue; + } + somebodyonme = true; if ( !TRAP_ON ) { @@ -658,6 +666,12 @@ void actTrapPermanent(Entity* my) break; } + if ( entity->behavior == &actMonster && entity->getMonsterTypeFromSprite() == DUCK_SMALL + && entity->monsterSpecialState == DUCK_RETURN ) + { + continue; + } + my->toggleSwitch(); TRAPPERMANENT_ON = 1; } @@ -1474,4 +1488,894 @@ void Entity::actSignalGateAND() free(neighbors); } } +} + +void actWallButton(Entity* my) +{ + if ( !my ) + { + return; + } + + my->actWallButton(); +} + +void Entity::actWallButton() +{ + if ( this->ticks == 1 ) + { + createWorldUITooltip(); + } + node_t* nextnode = nullptr; + Entity* key = nullptr; + for ( node_t* node = this->children.first; node != nullptr; node = nextnode ) + { + nextnode = node->next; + if ( node->element != nullptr ) + { + key = (Entity*)node->element; + } + } + + // check wall behind me. (e.g mined or destroyed then remove lock) + int checkx = x + (wallLockDir == 0 ? -8 : (wallLockDir == 2 ? 8 : 0)); + int checky = y + (wallLockDir == 1 ? -8 : (wallLockDir == 3 ? 8 : 0)); + checkx = checkx >> 4; + checky = checky >> 4; + if ( !map.tiles[OBSTACLELAYER + checky * MAPLAYERS + checkx * MAPLAYERS * map.height] ) + { + if ( key ) + { + list_RemoveNode(key->mynode); + } + list_RemoveNode(mynode); + return; + } + + static ConsoleVariable cvar_wall_button_key_z("/wall_button_key_z", 0.5); + + bool updateNeighbors = false; + if ( wallLockPower > 0 ) + { + --wallLockPower; + if ( ticks % 10 == 0 ) + { + playSoundEntityLocal(this, 247, 32); + } + + if ( multiplayer != CLIENT ) + { + if ( wallLockPower == 0 ) + { + if ( wallLockState == 1 ) + { + wallLockState = 0; + serverUpdateEntitySkill(this, 0); + updateNeighbors = true; + playSoundEntity(this, 56, 64); + } + } + } + } + + if ( multiplayer != CLIENT ) + { + if ( !wallLockInit ) + { + wallLockInit = 1; + if ( key ) + { + key->fskill[0] = 0.25; + } + if ( wallLockInvertPower != 0 ) + { + updateNeighbors = true; // once off power the neighbours since needs an external kick + } + } + + int player = -1; + bool interacted = false; + bool shotByArrow = false; + if ( wallLockPlayerInteracting > 0 ) + { + player = wallLockPlayerInteracting - 1; + wallLockPlayerInteracting = 0; + shotByArrow = true; + interacted = true; + if ( player >= MAXPLAYERS ) + { + player = -1; + } + } + + Entity* monsterInteracting = nullptr; + for ( int i = 0; i < MAXPLAYERS; i++ ) + { + if ( (client_selected[i] == this || selectedEntity[i] == this) ) + { + if ( inrange[i] ) + { + if ( players[i] && players[i]->entity ) + { + player = i; + interacted = true; + } + } + } + if ( this->isInteractWithMonster() ) + { + monsterInteracting = uidToEntity(this->interactedByMonster); + interacted = true; + this->clearMonsterInteract(); + } + } + + if ( interacted ) + { + if ( wallLockState == 0 ) + { + wallLockState = 1; + updateNeighbors = true; + if ( wallLockTimer > 0 ) + { + wallLockPower = wallLockTimer; // countdown + serverUpdateEntitySkill(this, 8); + } + else + { + // perma on + wallLockPower = -1; + } + serverUpdateEntitySkill(this, 0); + playSoundEntity(this, 248, 64); + playSoundEntity(this, 56, 64); + if ( monsterInteracting ) + { + if ( monsterInteracting->getMonsterTypeFromSprite() == GYROBOT ) + { + Entity* leader = monsterInteracting->monsterAllyGetPlayerLeader(); + if ( leader ) + { + achievementObserver.playerAchievements[monsterInteracting->monsterAllyIndex].checkPathBetweenObjects(leader, this, AchievementObserver::BARONY_ACH_LEVITANT_LACKEY); + } + } + Compendium_t::Events_t::eventUpdateWorld(monsterInteracting->monsterAllyIndex, Compendium_t::CPDM_BUTTON_FOLLOWER_PRESSED, "button", 1); + } + else + { + if ( shotByArrow ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6382)); + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_BUTTON_SHOT, "button", 1); + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6381)); + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_BUTTON_PRESSED, "button", 1); + } + } + } + else + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6380)); + } + } + } + + if ( key ) + { + real_t& inset = key->fskill[0]; + + if ( wallLockState == 0 ) + { + inset = std::min(0.25, inset + 0.025); + } + else if ( wallLockState == 1 ) + { + inset -= std::max(0.01, inset / 10.0); + inset = std::max(0.0, inset); + } + + key->focalz = *cvar_wall_button_key_z; + + real_t travel = std::max(0.05, 2.0 * inset); + key->x = this->x + travel * cos(this->yaw); + key->y = this->y + travel * sin(this->yaw); + } + + if ( multiplayer != CLIENT ) + { + int tx = x / 16; + int ty = y / 16; + list_t* neighbors = nullptr; + bool power_to_neighbors = wallLockInvertPower ? wallLockPower == 0 : wallLockPower != 0; + // comment out power_to_neighbors if we dont want this running all the time + // running all the time matches switch behavior so that 1 thing toggling doesnt shut the network + // if multiple active mechanisms are also powering it + if ( updateNeighbors || power_to_neighbors ) + { + switch ( wallLockDir ) + { + case 0: // west + getPowerablesOnTile(tx - 1, ty, &neighbors); + break; + case 1: // south + getPowerablesOnTile(tx, ty - 1, &neighbors); + break; + case 2: // east + getPowerablesOnTile(tx + 1, ty, &neighbors); + break; + case 3: // north + getPowerablesOnTile(tx, ty + 1, &neighbors); + break; + } + if ( neighbors != nullptr ) + { + node_t* node = nullptr; + for ( node = neighbors->first; node != nullptr; node = node->next ) + { + if ( node->element ) + { + Entity* powerable = (Entity*)(node->element); + + if ( powerable ) + { + if ( powerable->behavior == actCircuit ) + { + (power_to_neighbors) ? powerable->circuitPowerOn() : powerable->circuitPowerOff(); + } + else + { + if ( powerable->behavior == &::actSignalTimer ) + { + switch ( powerable->signalInputDirection ) + { + case 0: // west + if ( static_cast(this->x / 16) == static_cast((powerable->x / 16) - 1) ) + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + break; + case 1: // south + if ( static_cast(this->y / 16) == static_cast((powerable->y / 16) - 1) ) + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + break; + case 2: // east + if ( static_cast(this->x / 16) == static_cast((powerable->x / 16) + 1) ) + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + break; + case 3: // north + if ( static_cast(this->y / 16) == static_cast((powerable->y / 16) + 1) ) + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + break; + default: + break; + } + } + else if ( powerable->behavior == &::actSignalGateAND ) + { + int x1 = static_cast(this->x / 16); + int y1 = static_cast(this->y / 16); + //messagePlayer(0, "%d, %d, %d, %d", x1, x2, y1, y2); + signalGateANDOnReceive(*powerable, power_to_neighbors, x1, y1); + } + else + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + } + } + } + } + list_FreeAll(neighbors); //Free the list. + free(neighbors); + } + } + } +} + +void actWallLock(Entity* my) +{ + if ( !my ) + { + return; + } + + my->actWallLock(); +} + +void Entity::actWallLock() +{ + if ( this->ticks == 1 ) + { + createWorldUITooltip(); + } + node_t* nextnode = nullptr; + Entity* key = nullptr; + for ( node_t* node = this->children.first; node != nullptr; node = nextnode ) + { + nextnode = node->next; + if ( node->element != nullptr ) + { + key = (Entity*)node->element; + } + } + + // check wall behind me. (e.g mined or destroyed then remove lock) + int checkx = x + (wallLockDir == 0 ? -8 : (wallLockDir == 2 ? 8 : 0)); + int checky = y + (wallLockDir == 1 ? -8 : (wallLockDir == 3 ? 8 : 0)); + checkx = checkx >> 4; + checky = checky >> 4; + if ( !map.tiles[OBSTACLELAYER + checky * MAPLAYERS + checkx * MAPLAYERS * map.height] ) + { + if ( key ) + { + list_RemoveNode(key->mynode); + } + list_RemoveNode(mynode); + return; + } + + static ConsoleVariable cvar_wall_lock_key_z("/wall_lock_key_z", 0.5); + static ConsoleVariable cvar_wall_lock_key_scale("/wall_lock_key_scale", 0.9); + const real_t scaleDown = 1.0 - *cvar_wall_lock_key_scale; + + if ( wallLockClientInteractDelay > 0 ) + { + --wallLockClientInteractDelay; + if ( multiplayer != CLIENT ) + { + if ( wallLockClientInteractDelay == 0 ) + { + wallLockPlayerInteracting = 0; + } + } + } + + bool updateNeighbors = false; + if ( multiplayer != CLIENT ) + { + if ( !wallLockInit ) + { + wallLockInit = 1; + wallLockPickHealth = 50; + wallLockPreventLockpickExploit = 0; + if ( wallLockInvertPower != 0 ) + { + updateNeighbors = true; // once off power the neighbours since needs an external kick + } + } + + if ( wallLockPower == 2 || wallLockPower == 3 ) + { + // overridden by skeleton key + wallLockPower -= 2; + updateNeighbors = true; + } + + for ( int i = 0; i < MAXPLAYERS; i++ ) + { + if ( (client_selected[i] == this || selectedEntity[i] == this) ) + { + if ( inrange[i] ) + { + if ( players[i] && players[i]->entity ) + { + if ( wallLockState == LOCK_NO_KEY ) + { + if ( !players[i]->isLocalPlayer() ) + { + if ( wallLockPower == 1 ) + { + // already been lockpicked + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6430), Language::get(6383 + wallLockMaterial)); + playSoundEntity(this, 152, 64); + } + else if ( wallLockPlayerInteracting == players[i]->entity->getUID() ) + { + // chosen one, allow them to unlock the door. + wallLockClientInteractDelay = TICKS_PER_SECOND * 10; + + strcpy((char*)net_packet->data, "LKEY"); + net_packet->data[4] = i; + SDLNet_Write32(getUID(), &net_packet->data[5]); + net_packet->len = 9; + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + else if ( wallLockPlayerInteracting != 0 ) + { + // someone else is using that + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6377), Language::get(6383 + wallLockMaterial)); + } + else + { + // no matching key + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6379), Language::get(6383 + wallLockMaterial)); + playSoundEntity(this, 152, 64); + } + } + else // local player + { + if ( wallLockPower == 1 ) + { + // already been lockpicked + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6430), Language::get(6383 + wallLockMaterial)); + playSoundEntity(this, 152, 64); + } + else if ( wallLockPlayerInteracting != 0 ) + { + // someone else is using that + if ( wallLockPlayerInteracting != players[i]->entity->getUID() ) + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6377), Language::get(6383 + wallLockMaterial)); + } + } + else + { + if ( Item* foundWallLockKey = players[i]->inventoryUI.hasKeyForWallLock(*this) ) + { + Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY, "wall locks", 1); + Compendium_t::Events_t::eventUpdate(i, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY, foundWallLockKey->type, 1); + if ( foundWallLockKey->type == KEY_IRON ) + { + Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY_IRON, "wall locks", 1); + } + else if ( foundWallLockKey->type == KEY_SILVER ) + { + Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY_SILVER, "wall locks", 1); + steamStatisticUpdate(STEAM_STAT_PREMIUM_LOOTBOX, STEAM_STAT_INT, 1); + } + else if ( foundWallLockKey->type == KEY_GOLD ) + { + Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY_GOLD, "wall locks", 1); + steamStatisticUpdate(STEAM_STAT_PREMIUM_LOOTBOX, STEAM_STAT_INT, 1); + } + else if ( foundWallLockKey->type == KEY_BRONZE ) + { + Compendium_t::Events_t::eventUpdateWorld(i, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY_BRONZE, "wall locks", 1); + steamStatisticUpdate(STEAM_STAT_PREMIUM_LOOTBOX, STEAM_STAT_INT, 1); + } + + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6378), foundWallLockKey->getName()); + consumeItem(foundWallLockKey, i); + wallLockState = LOCK_KEY_START; + serverUpdateEntitySkill(this, 0); + + } + else + { + // no matching key + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6379), Language::get(6383 + wallLockMaterial)); + playSoundEntity(this, 152, 64); + } + } + } + } + else + { + if ( wallLockState == LOCK_KEY_ACTIVE || wallLockState == LOCK_KEY_INACTIVE ) + { + if ( wallLockState == LOCK_KEY_ACTIVE ) + { + if ( wallLockTurnable != 0 ) + { + playSoundEntity(this, 57, 64); + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6374), Language::get(6383 + wallLockMaterial)); + wallLockState = LOCK_KEY_INACTIVE_START; + serverUpdateEntitySkill(this, 0); + break; + } + else + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6376), Language::get(6383 + wallLockMaterial)); + playSoundEntity(this, 92, 64); + break; + } + } + else if ( wallLockState == LOCK_KEY_INACTIVE ) + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6375), Language::get(6383 + wallLockMaterial)); + playSoundEntity(this, 56, 64); + wallLockState = LOCK_KEY_ACTIVE_START; + serverUpdateEntitySkill(this, 0); + break; + } + } + else + { + messagePlayer(i, MESSAGE_INTERACTION, Language::get(6377), Language::get(6383 + wallLockMaterial)); + } + } + } + } + } + } + } + + if ( key ) + { + real_t& inset = key->fskill[0]; + if ( wallLockState == LOCK_NO_KEY ) + { + key->flags[INVISIBLE] = true; + } + else + { + key->flags[INVISIBLE] = false; + } + + if ( wallLockState == LOCK_KEY_START ) + { + playSoundEntityLocal(this, 702 + local_rng.rand() % 5, 128); + wallLockState = LOCK_KEY_ENTER; + key->roll = 0.0; + inset = 1.0; + fskill[0] = 0.0; + } + else if ( wallLockState == LOCK_KEY_ENTER ) + { + fskill[0] = 0.0; + inset -= std::max(0.01, inset / 10.0); + if ( inset <= 0.001 ) + { + inset = 0.0; + wallLockState = LOCK_KEY_ACTIVE_START; + playSoundEntityLocal(this, 56, 64); + } + } + else if ( wallLockState == LOCK_KEY_ACTIVE_START ) + { + fskill[0] = 0.0; + wallLockState = LOCK_KEY_ACTIVE; + } + else if ( wallLockState == LOCK_KEY_ACTIVE ) + { + // active + fskill[0] = std::min(PI / 2, fskill[0] + 0.1); + inset = std::max(0.0, inset - 0.025); + + if ( multiplayer != CLIENT ) + { + if ( !wallLockPower ) + { + wallLockPower = 1; + updateNeighbors = true; + } + } + } + else if ( wallLockState == LOCK_KEY_INACTIVE_START ) + { + fskill[0] = PI / 2; + wallLockState = LOCK_KEY_INACTIVE; + } + else if ( wallLockState == LOCK_KEY_INACTIVE ) + { + // inactive + fskill[0] = std::max(0.0, fskill[0] - 0.1); + inset = std::min(0.25, inset + 0.025); + if ( multiplayer != CLIENT ) + { + if ( wallLockPower ) + { + wallLockPower = 0; + updateNeighbors = true; + } + } + } + + key->roll = (3 * PI / 2) * sin(fskill[0]); + + key->scalex = *cvar_wall_lock_key_scale; + key->scaley = *cvar_wall_lock_key_scale; + key->scalez = *cvar_wall_lock_key_scale; + key->focalz = *cvar_wall_lock_key_z; + if ( key->sprite == 1590 ) // machine + { + key->focalz -= 0.5; + } + key->focalz -= scaleDown / 2; + + real_t travel = 0.95 + 1.5 * inset; + key->x = this->x + travel * cos(this->yaw); + key->y = this->y + travel * sin(this->yaw); + } + + if ( multiplayer != CLIENT ) + { + int tx = x / 16; + int ty = y / 16; + list_t* neighbors = nullptr; + bool power_to_neighbors = wallLockInvertPower ? wallLockPower == 0 : wallLockPower != 0; + // comment out power_to_neighbors if we dont want this running all the time + // running all the time matches switch behavior so that 1 thing toggling doesnt shut the network + // if multiple active mechanisms are also powering it + + if ( updateNeighbors || power_to_neighbors ) + { + switch ( wallLockDir ) + { + case 0: // west + getPowerablesOnTile(tx - 1, ty, &neighbors); + break; + case 1: // south + getPowerablesOnTile(tx, ty - 1, &neighbors); + break; + case 2: // east + getPowerablesOnTile(tx + 1, ty, &neighbors); + break; + case 3: // north + getPowerablesOnTile(tx, ty + 1, &neighbors); + break; + } + if ( neighbors != nullptr ) + { + node_t* node = nullptr; + for ( node = neighbors->first; node != nullptr; node = node->next ) + { + if ( node->element ) + { + Entity* powerable = (Entity*)(node->element); + + if ( powerable ) + { + if ( powerable->behavior == actCircuit ) + { + (power_to_neighbors) ? powerable->circuitPowerOn() : powerable->circuitPowerOff(); + } + else + { + if ( powerable->behavior == &::actSignalTimer ) + { + switch ( powerable->signalInputDirection ) + { + case 0: // west + if ( static_cast(this->x / 16) == static_cast((powerable->x / 16) - 1) ) + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + break; + case 1: // south + if ( static_cast(this->y / 16) == static_cast((powerable->y / 16) - 1) ) + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + break; + case 2: // east + if ( static_cast(this->x / 16) == static_cast((powerable->x / 16) + 1) ) + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + break; + case 3: // north + if ( static_cast(this->y / 16) == static_cast((powerable->y / 16) + 1) ) + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + break; + default: + break; + } + } + else if ( powerable->behavior == &::actSignalGateAND ) + { + int x1 = static_cast(this->x / 16); + int y1 = static_cast(this->y / 16); + //messagePlayer(0, "%d, %d, %d, %d", x1, x2, y1, y2); + signalGateANDOnReceive(*powerable, power_to_neighbors, x1, y1); + } + else + { + (power_to_neighbors) ? powerable->mechanismPowerOn() : powerable->mechanismPowerOff(); + } + } + } + } + } + list_FreeAll(neighbors); //Free the list. + free(neighbors); + } + } + } +} + +void actWind(Entity* my) +{ + if ( !my ) + { + return; + } + + my->actWind(); +} + +static ConsoleVariable cvar_map_tile_wind("/map_tile_wind", 1.0); + +bool entityInsideWind(Entity* entity1, Entity* wind) +{ + if ( !entity1 || !wind ) { return false; } + real_t startx = wind->x; + real_t starty = wind->y; + int numTiles = wind->actWindTileBonusLength; + bool result = false; + while ( numTiles >= 0 ) + { + int map_x = wind->x / 16; + int map_y = wind->y / 16; + if ( map_x > 0 && map_x < map.width - 1 && map_y > 0 && map_y < map.height - 1 ) + { + if ( map.tiles[OBSTACLELAYER + map_y * MAPLAYERS + map_x * MAPLAYERS * map.height] ) + { + break; // no blow through walls + } + if ( entityInsideEntity(entity1, wind) ) + { + result = true; + break; + } + } + --numTiles; + wind->x += 16.0 * cos(wind->yaw); + wind->y += 16.0 * sin(wind->yaw); + } + + wind->x = startx; + wind->y = starty; + return result; +} + +void Entity::actWind() +{ + if ( actWindLifetime > 0 ) + { + --actWindLifetime; + if ( actWindLifetime <= 0 ) + { + list_RemoveNode(this->mynode); + return; + } + } + + if ( !(svFlags & SV_FLAG_CHEATS) ) + { + *cvar_map_tile_wind = 1.0; + } + + if ( multiplayer == CLIENT ) + { + Entity* entity = players[clientnum]->entity; + if ( entity ) + { + if ( windEffectsEntity(entity) && entityInsideWind(entity, this) ) + { + real_t dirx = entity->creatureWindVelocity * cos(entity->creatureWindDir); + real_t diry = entity->creatureWindVelocity * sin(entity->creatureWindDir); + entity->creatureWindVelocity = *cvar_map_tile_wind; + dirx += cos(this->yaw); + diry += sin(this->yaw); + entity->creatureWindDir = atan2(diry, dirx); + } + } + } + else + { + int map_x = static_cast(x / 16); + int map_y = static_cast(y / 16); + std::vector entLists = TileEntityList.getEntitiesWithinRadius(map_x, map_y, actWindTileBonusLength + 2); + for ( std::vector::iterator it = entLists.begin(); it != entLists.end(); ++it ) + { + list_t* currentList = *it; + for ( node_t* node = currentList->first; node != nullptr; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( windEffectsEntity(entity) && entityInsideWind(entity, this) ) + { + auto hitProps = getParticleEmitterHitProps(getUID(), entity); + if ( !hitProps ) + { + continue; + } + if ( entity->behavior != &actPlayer && entity->behavior != &actMonster ) + { + if ( !hitProps ) + { + continue; + } + if ( hitProps->hits == 0 ) + { + if ( entity->behavior == &actArrow || entity->behavior == &actThrown || entity->behavior == &actMagicMissile ) + { + if ( Entity* caster = uidToEntity(this->parent) ) + { + if ( entity->behavior == &actArrow ) + { + entity->arrowPower += getSpellDamageSecondaryFromID(SPELL_WINDGATE, caster, nullptr, caster); + } + else if ( entity->behavior == &actThrown ) + { + entity->thrownProjectilePower += getSpellDamageSecondaryFromID(SPELL_WINDGATE, caster, nullptr, caster); + } + else if ( entity->behavior == &actMagicMissile ) + { + entity->actmagicAdditionalDamage += getSpellDamageSecondaryFromID(SPELL_WINDGATE, caster, nullptr, caster); + } + + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_WINDGATE, 10.0, 1.0, nullptr); + } + } + } + } + if ( hitProps->hits > 0 ) + { + continue; + } + } + else if ( hitProps->hits == 0 ) + { + if ( Entity* caster = uidToEntity(this->parent) ) + { + //magicOnSpellCastEvent(caster, caster, entity, SPELL_WINDGATE, spell_t::SPELL_LEVEL_EVENT_DEFAULT, 1); + if ( caster->behavior == &actPlayer ) + { + players[caster->skill[2]]->mechanics.updateSustainedSpellEvent(SPELL_WINDGATE, 20.0, 1.0, nullptr); + } + } + } + + real_t dirx = entity->creatureWindVelocity * cos(entity->creatureWindDir); + real_t diry = entity->creatureWindVelocity * sin(entity->creatureWindDir); + entity->creatureWindVelocity = *cvar_map_tile_wind; + dirx += cos(this->yaw); + diry += sin(this->yaw); + entity->creatureWindDir = atan2(diry, dirx); + hitProps->hits++; + hitProps->tick = ::ticks; + } + } + } + } + + if ( actWindParticleEffect == 1 ) + { + real_t eff_x = static_cast(x / 16) * 16.0 + 8.0; + real_t eff_y = static_cast(y / 16) * 16.0 + 8.0; + + if ( ticks % 10 == 0 ) + { + Entity* fx = newEntity(982, 1, map.entities, nullptr); + fx->x = eff_x - 8.0 * cos(yaw) + (-4.0 + local_rng.rand() % 9) * cos(yaw + PI / 2); + fx->y = eff_y - 8.0 * sin(yaw) + (-4.0 + local_rng.rand() % 9) * sin(yaw + PI / 2); + fx->z = 4.0 - local_rng.rand() % 9; + fx->vel_x = 1.5 * cos(yaw); + fx->vel_y = 1.5 * sin(yaw); + fx->scalex = 0.5; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->yaw = yaw; + fx->parent = getUID(); + fx->behavior = &actMagiclightMoving; + fx->skill[0] = std::max(20, (1 + actWindTileBonusLength) * 10); + fx->skill[3] = 1; + fx->flags[NOUPDATE] = true; + fx->flags[PASSABLE] = true; + fx->flags[UNCLICKABLE] = true; + //fx->flags[NOCLIP_WALLS] = true; + fx->flags[INVISIBLE] = true; + fx->flags[INVISIBLE_DITHER] = true; + if ( multiplayer != CLIENT ) + { + entity_uids--; + } + fx->setUID(-3); + } + } } \ No newline at end of file diff --git a/src/menu.cpp b/src/menu.cpp index e75eed3e0..9e183276f 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -9,8 +9,8 @@ -------------------------------------------------------------------------------*/ -#include #include "main.hpp" +#include #include "draw.hpp" #include "game.hpp" #include "stat.hpp" @@ -155,8 +155,6 @@ bool gamemods_modPreload = false; sex_t lastSex = MALE; PlayerRaces lastRace = RACE_HUMAN; int lastAppearance = 0; -bool enabledDLCPack1 = false; -bool enabledDLCPack2 = false; bool showRaceInfo = false; #ifdef STEAMWORKS std::vector workshopSubscribedItemList; @@ -276,6 +274,10 @@ int fourthendmovietime = 0; int fourthEndNumLines = 13; bool losingConnection[MAXPLAYERS] = { false }; +bool enabledDLCPack1 = false; +bool enabledDLCPack2 = false; +bool enabledDLCPack3 = false; + // new text crawls... int DLCendmoviealpha[8][30] = { 0 }; int DLCendmovieStageAndTime[8][2] = { 0 }; @@ -392,6 +394,26 @@ bool isAchievementUnlockedForClassUnlock(int race) { return unlocked; } + else if ( enabledDLCPack3 && race == RACE_DRYAD && SteamUserStats()->GetAchievement("BARONY_ACH_BARKSKIN_BARON", &unlocked) ) + { + return unlocked; + } + else if ( enabledDLCPack3 && race == RACE_MYCONID && SteamUserStats()->GetAchievement("BARONY_ACH_BOLETE_BARON", &unlocked) ) + { + return unlocked; + } + else if ( enabledDLCPack3 && race == RACE_GREMLIN && SteamUserStats()->GetAchievement("BARONY_ACH_BONKERS_BARON", &unlocked) ) + { + return unlocked; + } + else if ( enabledDLCPack3 && race == RACE_SALAMANDER && SteamUserStats()->GetAchievement("BARONY_ACH_BURNINATION_BARON", &unlocked) ) + { + return unlocked; + } + else if ( enabledDLCPack3 && race == RACE_GNOME && SteamUserStats()->GetAchievement("BARONY_ACH_BITTY_BARON", &unlocked) ) + { + return unlocked; + } #elif (defined USE_EOS || defined LOCAL_ACHIEVEMENTS) if ( enabledDLCPack1 && race == RACE_SKELETON && achievementUnlocked("BARONY_ACH_BONY_BARON") ) { @@ -425,6 +447,26 @@ bool isAchievementUnlockedForClassUnlock(int race) { return true; } + else if ( enabledDLCPack3 && race == RACE_DRYAD && achievementUnlocked("BARONY_ACH_BARKSKIN_BARON") ) + { + return true; + } + else if ( enabledDLCPack3 && race == RACE_MYCONID && achievementUnlocked("BARONY_ACH_BOLETE_BARON") ) + { + return true; + } + else if ( enabledDLCPack3 && race == RACE_GREMLIN && achievementUnlocked("BARONY_ACH_BONKERS_BARON") ) + { + return true; + } + else if ( enabledDLCPack3 && race == RACE_SALAMANDER && achievementUnlocked("BARONY_ACH_BURNINATION_BARON") ) + { + return true; + } + else if ( enabledDLCPack3 && race == RACE_GNOME && achievementUnlocked("BARONY_ACH_BITTY_BARON") ) + { + return true; + } #else return false; #endif // STEAMWORKS @@ -465,6 +507,17 @@ int isCharacterValidFromDLC(Stat& myStats, int characterClass) } } + if ( myStats.playerRace == RACE_RAT + || myStats.playerRace == RACE_TROLL + || myStats.playerRace == RACE_IMP + || myStats.playerRace == RACE_SPIDER ) + { + if ( !enabledDLCPack2 ) + { + return INVALID_REQUIREDLC2; + } + } + switch ( characterClass ) { case CLASS_CONJURER: @@ -491,6 +544,19 @@ int isCharacterValidFromDLC(Stat& myStats, int characterClass) } } break; + case CLASS_BARD: + case CLASS_SAPPER: + case CLASS_SCION: + case CLASS_HERMIT: + case CLASS_PALADIN: + if ( !enabledDLCPack3 ) + { + if ( !challengeClass ) + { + return INVALID_REQUIREDLC3; + } + } + break; default: break; } @@ -521,6 +587,19 @@ int isCharacterValidFromDLC(Stat& myStats, int characterClass) } } break; + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_GREMLIN: + case RACE_SALAMANDER: + case RACE_GNOME: + if ( !enabledDLCPack3 ) + { + if ( !challengeRace ) + { + return INVALID_REQUIREDLC3; + } + } + break; default: break; } @@ -596,6 +675,41 @@ int isCharacterValidFromDLC(Stat& myStats, int characterClass) } return isAchievementUnlockedForClassUnlock(RACE_INSECTOID) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; break; + case CLASS_BARD: + if ( myStats.playerRace == RACE_GNOME ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_GNOME) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; + case CLASS_SAPPER: + if ( myStats.playerRace == RACE_GREMLIN ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_GREMLIN) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; + case CLASS_SCION: + if ( myStats.playerRace == RACE_DRYAD ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_DRYAD) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; + case CLASS_HERMIT: + if ( myStats.playerRace == RACE_MYCONID ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_MYCONID) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; + case CLASS_PALADIN: + if ( myStats.playerRace == RACE_SALAMANDER ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_SALAMANDER) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; default: break; } @@ -8544,9 +8658,18 @@ void doNewGame(bool makeHighscore) { { client_keepalive[i] = ticks; // this way nobody times out when we reset ticks! players[i]->init(); + if ( !loadingsavegame ) + { + players[i]->was_connected_to_game = !client_disconnected[i]; + } + else if ( !client_disconnected[i] ) + { + players[i]->was_connected_to_game = true; + } players[i]->hud.reset(); players[i]->hud.followerBars.clear(); players[i]->hud.playerBars.clear(); + players[i]->worldUI.tooltipsInRange.clear(); // fix bug if multiplayer was ghost and host disconnect, then start new solo game deinitShapeshiftHotbar(i); for ( int c = 0; c < NUM_HOTBAR_ALTERNATES; ++c ) { @@ -8554,13 +8677,18 @@ void doNewGame(bool makeHighscore) { } players[i]->shootmode = true; players[i]->magic.clearSelectedSpells(); + spellcastingAnimationManager_deactivate(&cast_animation[i]); + cast_animation[i].overcharge = 0; + cast_animation[i].overcharge_init = 0; players[i]->paperDoll.resetPortrait(); // reset paper doll camera view. players[i]->closeAllGUIs(CLOSEGUI_ENABLE_SHOOTMODE, CLOSEGUI_CLOSE_ALL); } EnemyHPDamageBarHandler::dumpCache(); + AOEIndicators_t::cleanup(); monsterAllyFormations.reset(); PingNetworkStatus_t::reset(); particleTimerEmitterHitEntities.clear(); + particleTimerEffects.clear(); monsterTrapIgnoreEntities.clear(); minimapHighlights.clear(); @@ -8604,6 +8732,11 @@ void doNewGame(bool makeHighscore) { camera.luminance = defaultLuminance; } gameplayCustomManager.readFromFile(); + if ( gameplayCustomManager.inUse() ) + { + conductGameChallenges[CONDUCT_MODDED] = 1; + Mods::disableSteamAchievements = true; + } textSourceScript.scriptVariables.clear(); if ( multiplayer == CLIENT ) @@ -8739,6 +8872,7 @@ void doNewGame(bool makeHighscore) { // generate mimics { mimic_generator.init(); + treasure_room_generator.init(); } Compendium_t::Events_t::clientReceiveData.clear(); @@ -8780,6 +8914,7 @@ void doNewGame(bool makeHighscore) { { soundNotification_group->stop(); } + ensembleSounds.stopPlaying(true); VoiceChat.deinitRecording(false); #elif defined USE_OPENAL if ( sound_group ) @@ -8810,7 +8945,7 @@ void doNewGame(bool makeHighscore) { Uint32 oldSvFlags = gameModeManager.currentSession.serverFlags; bool bOldSvFlags = gameModeManager.currentSession.bHasSavedServerFlags; for (int c = 0; c < MAXPLAYERS; ++c) { - if (!client_disconnected[c]) { + if (!client_disconnected[c] || players[c]->was_connected_to_game ) { stats[c]->clearStats(); loadGame(c, saveGameInfo); } @@ -8852,6 +8987,7 @@ void doNewGame(bool makeHighscore) { players[i]->hud.weapon = nullptr; players[i]->hud.magicLeftHand = nullptr; players[i]->hud.magicRightHand = nullptr; + players[i]->hud.magicRangefinder = nullptr; players[i]->ghost.reset(); FollowerMenu[i].recentEntity = nullptr; FollowerMenu[i].followerToCommand = nullptr; @@ -8967,19 +9103,19 @@ void doNewGame(bool makeHighscore) { { if ( players[c] && players[c]->entity && !client_disconnected[c] ) { - if ( stats[c] && stats[c]->EFFECTS[EFF_POLYMORPH] && stats[c]->playerPolymorphStorage != NOTHING ) + if ( stats[c] && stats[c]->getEffectActive(EFF_POLYMORPH) && stats[c]->playerPolymorphStorage != NOTHING ) { players[c]->entity->effectPolymorph = stats[c]->playerPolymorphStorage; serverUpdateEntitySkill(players[c]->entity, 50); // update visual polymorph effect for clients. serverUpdateEffects(c); } - if ( stats[c] && stats[c]->EFFECTS[EFF_SHAPESHIFT] && stats[c]->playerShapeshiftStorage != NOTHING ) + if ( stats[c] && stats[c]->getEffectActive(EFF_SHAPESHIFT) && stats[c]->playerShapeshiftStorage != NOTHING ) { players[c]->entity->effectShapeshift = stats[c]->playerShapeshiftStorage; serverUpdateEntitySkill(players[c]->entity, 53); // update visual shapeshift effect for clients. serverUpdateEffects(c); } - if ( stats[c] && stats[c]->EFFECTS[EFF_VAMPIRICAURA] && stats[c]->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) + if ( stats[c] && stats[c]->getEffectActive(EFF_VAMPIRICAURA) && stats[c]->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) { players[c]->entity->playerVampireCurse = 1; serverUpdateEntitySkill(players[c]->entity, 51); // update curse progression @@ -9062,6 +9198,10 @@ void doNewGame(bool makeHighscore) { { monster->monsterAllySummonRank = (stats[c]->playerSummon2PERCHR & 0x0000FF00) >> 8; } + else if ( monsterStats->getAttribute("SUMMONED_CREATURE") != "" ) + { + monster->monsterAllySummonRank = std::stoi(monsterStats->getAttribute("SUMMONED_CREATURE")); + } serverUpdateEntitySkill(monster, 46); // update monsterAllyClass serverUpdateEntitySkill(monster, 44); // update monsterAllyPickupItems serverUpdateEntitySkill(monster, 50); // update monsterAllySummonRank @@ -9182,6 +9322,7 @@ void doNewGame(bool makeHighscore) { players[i]->hud.weapon = nullptr; players[i]->hud.magicLeftHand = nullptr; players[i]->hud.magicRightHand = nullptr; + players[i]->hud.magicRangefinder = nullptr; players[i]->ghost.reset(); FollowerMenu[i].recentEntity = nullptr; FollowerMenu[i].followerToCommand = nullptr; @@ -9253,6 +9394,7 @@ void doNewGame(bool makeHighscore) { { soundNotification_group->stop(); } + ensembleSounds.stopPlaying(true); VoiceChat.deinitRecording(false); #elif defined USE_OPENAL if ( sound_group ) @@ -9357,13 +9499,14 @@ void doNewGame(bool makeHighscore) { steamAchievement("BARONY_ACH_SPICE_OF_LIFE"); } - if ( stats[clientnum]->playerRace >= 0 && stats[clientnum]->playerRace <= RACE_INSECTOID ) + if ( (stats[clientnum]->playerRace >= 0 && stats[clientnum]->playerRace <= RACE_INSECTOID) + || (stats[clientnum]->playerRace > RACE_IMP && stats[clientnum]->playerRace < RACE_ENUM_END) ) { usedRace[stats[clientnum]->playerRace] = true; } // new achievement usedAllClasses = true; - for ( int c = CLASS_CONJURER; c <= CLASS_HUNTER; ++c ) + for ( int c = CLASS_CONJURER; c <= CLASS_PALADIN; ++c ) { if ( !usedClass[c] ) { @@ -9371,8 +9514,12 @@ void doNewGame(bool makeHighscore) { } } bool usedAllRaces = true; - for ( int c = RACE_SKELETON; c <= RACE_INSECTOID; ++c ) + for ( int c = RACE_SKELETON; c < RACE_ENUM_END; ++c ) { + if ( c > RACE_INSECTOID && c <= RACE_IMP ) + { + continue; + } if ( !usedRace[c] ) { usedAllRaces = false; @@ -9624,6 +9771,7 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { bool localScores = gameModeManager.allowsHiscores(); bool onlineScores = gameModeManager.allowsGlobalHiscores(); bool allowedSavegames = gameModeManager.allowsSaves(); + bool customRun = gameModeManager.getMode() == GameModeManager_t::GAME_MODE_CUSTOM_RUN; if ( gameModeManager.getMode() == GameModeManager_t::GAME_MODE_TUTORIAL ) { victory = 0; @@ -9661,7 +9809,7 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { { if ( client_classes[clientnum] == CLASS_ACCURSED ) { - if ( stats[clientnum]->EFFECTS[EFF_VAMPIRICAURA] && stats[clientnum]->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) + if ( stats[clientnum]->getEffectActive(EFF_VAMPIRICAURA) && stats[clientnum]->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) { conductGameChallenges[CONDUCT_ACCURSED] = 1; } @@ -9760,6 +9908,7 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { { soundNotification_group->stop(); } + ensembleSounds.stopPlaying(true); VoiceChat.deinitRecording(true); #elif defined USE_OPENAL if ( sound_group ) @@ -9822,8 +9971,13 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { } } + if ( victory ) { + if ( customRun ) + { + gameModeManager.setMode(GameModeManager_t::GAME_MODE_CUSTOM_RUN); // for the achievement checks + } // conduct achievements if ( (victory == 1 && currentlevel >= 20) || (victory == 2 && currentlevel >= 24) @@ -9909,7 +10063,7 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { else if ( client_classes[i] == CLASS_ACCURSED ) { steamAchievement("BARONY_ACH_POWER_HUNGRY"); - if ( stats[i]->EFFECTS[EFF_VAMPIRICAURA] && stats[i]->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) + if ( stats[i]->getEffectActive(EFF_VAMPIRICAURA) && stats[i]->EFFECTS_TIMERS[EFF_VAMPIRICAURA] == -2 ) { if ( stats[i] && (svFlags & SV_FLAG_HUNGER) ) { @@ -9941,6 +10095,26 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { { steamAchievement("BARONY_ACH_LIKE_CLOCKWORK"); } + else if ( client_classes[i] == CLASS_BARD ) + { + steamAchievement("BARONY_ACH_SWAN_SONG"); + } + else if ( client_classes[i] == CLASS_SAPPER ) + { + steamAchievement("BARONY_ACH_DUNGEON_SEIGE"); + } + else if ( client_classes[i] == CLASS_SCION ) + { + steamAchievement("BARONY_ACH_NEPO_BABY"); + } + else if ( client_classes[i] == CLASS_HERMIT ) + { + steamAchievement("BARONY_ACH_REINTEGRATED"); + } + else if ( client_classes[i] == CLASS_PALADIN ) + { + steamAchievement("BARONY_ACH_MISSION_GOD"); + } if ( stats[i] && stats[i]->stat_appearance == 0 ) { @@ -9970,6 +10144,21 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { case RACE_GOBLIN: steamAchievement("BARONY_ACH_BAYOU_BARON"); break; + case RACE_DRYAD: + steamAchievement("BARONY_ACH_BARKSKIN_BARON"); + break; + case RACE_MYCONID: + steamAchievement("BARONY_ACH_BOLETE_BARON"); + break; + case RACE_GREMLIN: + steamAchievement("BARONY_ACH_BONKERS_BARON"); + break; + case RACE_SALAMANDER: + steamAchievement("BARONY_ACH_BURNINATION_BARON"); + break; + case RACE_GNOME: + steamAchievement("BARONY_ACH_BITTY_BARON"); + break; default: break; } @@ -9978,6 +10167,8 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { } } } + + gameModeManager.setMode(GameModeManager_t::GAME_MODE_DEFAULT); } if ( !endTutorial && victory > 0 && allowedSavegames ) @@ -10036,6 +10227,7 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { players[i]->hud.reset(); players[i]->hud.followerBars.clear(); players[i]->hud.playerBars.clear(); + players[i]->worldUI.tooltipsInRange.clear(); // fix bug if multiplayer was ghost and host disconnect, then start new solo game deinitShapeshiftHotbar(i); for ( c = 0; c < NUM_HOTBAR_ALTERNATES; ++c ) { @@ -10047,8 +10239,10 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { players[i]->closeAllGUIs(CLOSEGUI_ENABLE_SHOOTMODE, CLOSEGUI_CLOSE_ALL); } EnemyHPDamageBarHandler::dumpCache(); + AOEIndicators_t::cleanup(); monsterAllyFormations.reset(); particleTimerEmitterHitEntities.clear(); + particleTimerEffects.clear(); monsterTrapIgnoreEntities.clear(); minimapHighlights.clear(); PingNetworkStatus_t::reset(); @@ -10073,6 +10267,7 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { { stats[c]->freePlayerEquipment(); list_FreeAll(&stats[c]->inventory); + list_FreeAll(&stats[c]->void_chest_inventory); list_FreeAll(&stats[c]->FOLLOWERS); } list_FreeAll(&removedEntities); @@ -10103,8 +10298,6 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { stats[c]->type = HUMAN; stats[c]->playerRace = RACE_HUMAN; stats[c]->clearStats(); - entitiesToDelete[c].first = NULL; - entitiesToDelete[c].last = NULL; if ( c == 0 ) { initClass(c); @@ -10118,12 +10311,14 @@ void doEndgame(bool saveHighscore, bool onServerDisconnect) { players[i]->hud.weapon = nullptr; players[i]->hud.magicLeftHand = nullptr; players[i]->hud.magicRightHand = nullptr; + players[i]->hud.magicRangefinder = nullptr; players[i]->ghost.reset(); FollowerMenu[i].recentEntity = nullptr; FollowerMenu[i].followerToCommand = nullptr; FollowerMenu[i].entityToInteractWith = nullptr; CalloutMenu[i].closeCalloutMenuGUI(); CalloutMenu[i].callouts.clear(); + players[i]->was_connected_to_game = false; } if ( !onServerDisconnect ) @@ -10479,10 +10674,10 @@ void openGameoverWindow() snprintf(scorenum, 16, "%d\n\n", total); bool madetop = false; - list_t* scoresPtr = &topscores; + list_t* scoresPtr = &topscores_json; if ( score->conductGameChallenges[CONDUCT_MULTIPLAYER] ) { - scoresPtr = &topscoresMultiplayer; + scoresPtr = &topscoresMultiplayer_json; } if ( !list_Size(scoresPtr) ) { @@ -11062,11 +11257,11 @@ void buttonScoreNext(button_t* my) { if ( scoreDisplayMultiplayer ) { - score_window = std::min(score_window + 1, std::max(1, list_Size(&topscoresMultiplayer))); + score_window = std::min(score_window + 1, std::max(1, list_Size(&topscoresMultiplayer_json))); } else { - score_window = std::min(score_window + 1, std::max(1, list_Size(&topscores))); + score_window = std::min(score_window + 1, std::max(1, list_Size(&topscores_json))); } loadScore(score_window - 1); camera_charsheet_offsetyaw = (330) * PI / 180; @@ -11237,35 +11432,6 @@ void buttonOpenSteamLeaderboards(button_t* my) } #endif -void buttonOpenScoresWindow(button_t* my) -{ - // deprecated - return; -} - -void buttonDeleteCurrentScore(button_t* my) -{ - node_t* node = nullptr; - if ( score_window_delete_multiplayer ) - { - node = list_Node(&topscoresMultiplayer, score_window_to_delete - 1); - if ( node ) - { - list_RemoveNode(node); - score_window_to_delete = std::max(score_window_to_delete - 1, 1); - } - } - else - { - node = list_Node(&topscores, score_window_to_delete - 1); - if ( node ) - { - list_RemoveNode(node); - score_window_to_delete = std::max(score_window_to_delete - 1, 1); - } - } -} - // handles slider void doSlider(int x, int y, int dots, int minvalue, int maxvalue, int increment, int* var, SDL_Surface* slider_font, int slider_font_char_width) { @@ -11518,77 +11684,79 @@ void buttonLoadMultiplayerGame(button_t* button) void buttonRandomCharacter(button_t* my) { - playing_random_char = true; - charcreation_step = 4; - camera_charsheet_offsetyaw = (330) * PI / 180; - stats[0]->sex = static_cast(local_rng.rand() % 2); - client_classes[0] = local_rng.rand() % (CLASS_MONK + 1);//NUMCLASSES; - stats[0]->clearStats(); - if ( enabledDLCPack1 || enabledDLCPack2 ) - { - stats[0]->playerRace = local_rng.rand() % NUMPLAYABLERACES; - if ( !enabledDLCPack1 ) - { - while ( stats[0]->playerRace == RACE_SKELETON || stats[0]->playerRace == RACE_VAMPIRE - || stats[0]->playerRace == RACE_SUCCUBUS || stats[0]->playerRace == RACE_GOATMAN ) - { - stats[0]->playerRace = local_rng.rand() % NUMPLAYABLERACES; - } - } - else if ( !enabledDLCPack2 ) - { - while ( stats[0]->playerRace == RACE_AUTOMATON || stats[0]->playerRace == RACE_GOBLIN - || stats[0]->playerRace == RACE_INCUBUS || stats[0]->playerRace == RACE_INSECTOID ) - { - stats[0]->playerRace = local_rng.rand() % NUMPLAYABLERACES; - } - } - if ( stats[0]->playerRace == RACE_INCUBUS ) - { - stats[0]->sex = MALE; - } - else if ( stats[0]->playerRace == RACE_SUCCUBUS ) - { - stats[0]->sex = FEMALE; - } + return; - if ( stats[0]->playerRace == RACE_HUMAN ) - { - client_classes[0] = local_rng.rand() % (NUMCLASSES); - if ( !enabledDLCPack1 ) - { - while ( client_classes[0] == CLASS_CONJURER || client_classes[0] == CLASS_ACCURSED - || client_classes[0] == CLASS_MESMER || client_classes[0] == CLASS_BREWER ) - { - client_classes[0] = local_rng.rand() % (NUMCLASSES); - } - } - else if ( !enabledDLCPack2 ) - { - while ( client_classes[0] == CLASS_HUNTER || client_classes[0] == CLASS_SHAMAN - || client_classes[0] == CLASS_PUNISHER || client_classes[0] == CLASS_MACHINIST ) - { - client_classes[0] = local_rng.rand() % (NUMCLASSES); - } - } - stats[0]->stat_appearance = local_rng.rand() % NUMAPPEARANCES; - } - else - { - client_classes[0] = local_rng.rand() % (CLASS_MONK + 2); - if ( client_classes[0] > CLASS_MONK ) - { - client_classes[0] = CLASS_MONK + stats[0]->playerRace; // monster specific classes. - } - stats[0]->stat_appearance = 0; - } - } - else - { - stats[0]->playerRace = RACE_HUMAN; - stats[0]->stat_appearance = local_rng.rand() % NUMAPPEARANCES; - } - initClass(0); + //playing_random_char = true; + //charcreation_step = 4; + //camera_charsheet_offsetyaw = (330) * PI / 180; + //stats[0]->sex = static_cast(local_rng.rand() % 2); + //client_classes[0] = local_rng.rand() % (CLASS_MONK + 1);//NUMCLASSES; + //stats[0]->clearStats(); + //if ( enabledDLCPack1 || enabledDLCPack2 ) + //{ + // stats[0]->playerRace = local_rng.rand() % NUMPLAYABLERACES; + // if ( !enabledDLCPack1 ) + // { + // while ( stats[0]->playerRace == RACE_SKELETON || stats[0]->playerRace == RACE_VAMPIRE + // || stats[0]->playerRace == RACE_SUCCUBUS || stats[0]->playerRace == RACE_GOATMAN ) + // { + // stats[0]->playerRace = local_rng.rand() % NUMPLAYABLERACES; + // } + // } + // else if ( !enabledDLCPack2 ) + // { + // while ( stats[0]->playerRace == RACE_AUTOMATON || stats[0]->playerRace == RACE_GOBLIN + // || stats[0]->playerRace == RACE_INCUBUS || stats[0]->playerRace == RACE_INSECTOID ) + // { + // stats[0]->playerRace = local_rng.rand() % NUMPLAYABLERACES; + // } + // } + // if ( stats[0]->playerRace == RACE_INCUBUS ) + // { + // stats[0]->sex = MALE; + // } + // else if ( stats[0]->playerRace == RACE_SUCCUBUS ) + // { + // stats[0]->sex = FEMALE; + // } + + // if ( stats[0]->playerRace == RACE_HUMAN ) + // { + // client_classes[0] = local_rng.rand() % (NUMCLASSES); + // if ( !enabledDLCPack1 ) + // { + // while ( client_classes[0] == CLASS_CONJURER || client_classes[0] == CLASS_ACCURSED + // || client_classes[0] == CLASS_MESMER || client_classes[0] == CLASS_BREWER ) + // { + // client_classes[0] = local_rng.rand() % (NUMCLASSES); + // } + // } + // else if ( !enabledDLCPack2 ) + // { + // while ( client_classes[0] == CLASS_HUNTER || client_classes[0] == CLASS_SHAMAN + // || client_classes[0] == CLASS_PUNISHER || client_classes[0] == CLASS_MACHINIST ) + // { + // client_classes[0] = local_rng.rand() % (NUMCLASSES); + // } + // } + // stats[0]->stat_appearance = local_rng.rand() % NUMAPPEARANCES; + // } + // else + // { + // client_classes[0] = local_rng.rand() % (CLASS_MONK + 2); + // if ( client_classes[0] > CLASS_MONK ) + // { + // client_classes[0] = CLASS_MONK + stats[0]->playerRace; // monster specific classes. + // } + // stats[0]->stat_appearance = 0; + // } + //} + //else + //{ + // stats[0]->playerRace = RACE_HUMAN; + // stats[0]->stat_appearance = local_rng.rand() % NUMAPPEARANCES; + //} + //initClass(0); } bool replayLastCharacter(const int index, int multiplayer) @@ -11632,9 +11800,9 @@ bool replayLastCharacter(const int index, int multiplayer) if ( lastClass >= 0 && lastSex >= 0 && lastRace >= 0 && lastAppearance >= 0 && lastName != "" ) { stats[index]->sex = static_cast(std::min(lastSex, (int)sex_t::FEMALE)); - stats[index]->playerRace = std::min(std::max(static_cast(RACE_HUMAN), lastRace), static_cast(NUMPLAYABLERACES)); + stats[index]->playerRace = std::min(std::max(static_cast(RACE_HUMAN), lastRace), static_cast(RACE_ENUM_END - 1)); stats[index]->stat_appearance = lastAppearance; - client_classes[index] = std::min(std::max(0, lastClass), static_cast(CLASS_HUNTER)); + client_classes[index] = std::min(std::max(0, lastClass), static_cast(NUMCLASSES - 1)); switch ( isCharacterValidFromDLC(*stats[index], lastClass) ) { @@ -11643,6 +11811,7 @@ bool replayLastCharacter(const int index, int multiplayer) break; case INVALID_REQUIREDLC1: case INVALID_REQUIREDLC2: + case INVALID_REQUIREDLC3: // class or race invalid. if ( stats[index]->playerRace > RACE_HUMAN ) { diff --git a/src/menu.hpp b/src/menu.hpp index 3437186e7..4e61e1b48 100644 --- a/src/menu.hpp +++ b/src/menu.hpp @@ -77,7 +77,6 @@ void buttonDisconnect(button_t* my); void buttonScoreNext(button_t* my); void buttonScorePrev(button_t* my); void buttonScoreToggle(button_t* my); -void buttonDeleteCurrentScore(button_t* my); void buttonOpenCharacterCreationWindow(button_t* my); void buttonDeleteSavedSoloGame(button_t* my); void buttonDeleteSavedMultiplayerGame(button_t* my); @@ -87,7 +86,6 @@ void buttonLoadSingleplayerGame(button_t* my); void buttonLoadMultiplayerGame(button_t* my); void buttonRandomCharacter(button_t* my); bool replayLastCharacter(const int index, int multiplayer); -void buttonOpenScoresWindow(button_t* my); void buttonRandomName(button_t* my); void buttonGamemodsOpenDirectory(button_t* my); void buttonGamemodsPrevDirectory(button_t* my); @@ -314,7 +312,8 @@ enum CharacterDLCValidation : int VALID_OK_CHARACTER, INVALID_REQUIREDLC1, INVALID_REQUIREDLC2, - INVALID_REQUIRE_ACHIEVEMENT + INVALID_REQUIRE_ACHIEVEMENT, + INVALID_REQUIREDLC3 }; struct LastCreatedCharacter { diff --git a/src/mod_tools.cpp b/src/mod_tools.cpp index 8839ddd2d..fcee41e41 100644 --- a/src/mod_tools.cpp +++ b/src/mod_tools.cpp @@ -84,7 +84,11 @@ void GameModeManager_t::Tutorial_t::startTutorial(std::string mapToSet) } setTutorialMap(mapToSet); } +#ifndef EDITOR gameModeManager.setMode(gameModeManager.GameModes::GAME_MODE_TUTORIAL); + GameplayPreferences_t::reset(); + gameplayPreferences[0].process(); +#endif stats[0]->clearStats(); strcpy(stats[0]->name, "Player"); stats[0]->sex = static_cast(local_rng.rand() % 2); @@ -834,7 +838,51 @@ void IRCHandler_t::handleMessage(std::string& msg) #endif // !NINTENDO Uint32 ItemTooltips_t::itemsJsonHashRead = 0; -const Uint32 ItemTooltips_t::kItemsJsonHash = 1748555711; +const Uint32 ItemTooltips_t::kItemsJsonHash = 3757883972; + +void ItemTooltips_t::setSpellValueIfKeyPresent(ItemTooltips_t::spellItem_t& t, rapidjson::Value::ConstMemberIterator item_itr, Uint32& hash, Uint32& hashShift, const char* key, int& toSet) +{ + if ( item_itr->value.HasMember(key) ) + { + t.hasExpandedJSON = true; + toSet = item_itr->value[key].GetInt(); + //hash += (Uint32)((Uint32)toSet << (hashShift % 32)); ++hashShift; + } +} +void ItemTooltips_t::setSpellValueIfKeyPresent(ItemTooltips_t::spellItem_t& t, rapidjson::Value::ConstMemberIterator item_itr, Uint32& hash, Uint32& hashShift, const char* key, real_t& toSet) +{ + if ( item_itr->value.HasMember(key) ) + { + t.hasExpandedJSON = true; + toSet = item_itr->value[key].GetFloat(); + //hash += (Uint32)(static_cast(toSet * 100000) << (hashShift % 32)); ++hashShift; + } +} + +#ifdef EDITOR +void lowercaseString(std::string& str) +{ + if ( str.size() < 1 ) { return; } + for ( auto& letter : str ) + { + if ( letter >= 'A' && letter <= 'Z' ) + { + letter = tolower(letter); + } + } +} +#endif + +void hashSpellProp(Uint32& hash, Uint32& hashShift, int& toSet) +{ + hash += (Uint32)((Uint32)toSet << (hashShift % 32)); ++hashShift; +} + +void hashSpellProp(Uint32& hash, Uint32& hashShift, real_t& toSet) +{ + hash += (Uint32)(static_cast(toSet * 100000) << (hashShift % 32)); ++hashShift; +} + void ItemTooltips_t::readItemsFromFile() { printlog("loading items...\n"); @@ -854,8 +902,8 @@ void ItemTooltips_t::readItemsFromFile() return; } - const int bufSize = 360000; - char buf[bufSize]; + const int bufSize = 600000; + static char buf[bufSize]; int count = fp->read(buf, sizeof(buf[0]), sizeof(buf) - 1); buf[count] = '\0'; //rapidjson::FileReadStream is(fp, buf, sizeof(buf)); - use this for large chunks. @@ -936,7 +984,7 @@ void ItemTooltips_t::readItemsFromFile() { assert(i == tmpItems[i].itemId); items[i].level = tmpItems[i].itemLevel; - items[i].value = tmpItems[i].gold; + items[i].gold_value = tmpItems[i].gold; items[i].weight = tmpItems[i].weight; items[i].fpindex = tmpItems[i].fpIndex; items[i].index = tmpItems[i].tpIndex; @@ -1026,6 +1074,10 @@ void ItemTooltips_t::readItemsFromFile() { items[i].category = SPELL_CAT; } + else if ( tmpItems[i].category.compare("TOME_SPELL") == 0 ) + { + items[i].category = TOME_SPELL; + } else { items[i].category = GEM; @@ -1074,7 +1126,7 @@ void ItemTooltips_t::readItemsFromFile() } hash += (Uint32)((Uint32)items[i].weight << (shift % 32)); ++shift; - hash += (Uint32)((Uint32)items[i].value << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)items[i].gold_value << (shift % 32)); ++shift; hash += (Uint32)((Uint32)items[i].level << (shift % 32)); ++shift; /*{ auto pair = std::make_pair(items[i].value, i); @@ -1095,16 +1147,6 @@ void ItemTooltips_t::readItemsFromFile() }*/ } - itemsJsonHashRead = hash; - if ( itemsJsonHashRead != kItemsJsonHash ) - { - printlog("[JSON]: Notice: items.json unknown hash, achievements are disabled: %d", itemsJsonHashRead); - } - else - { - printlog("[JSON]: items.json hash verified successfully."); - } - spellItems.clear(); int spellsRead = 0; @@ -1113,8 +1155,12 @@ void ItemTooltips_t::readItemsFromFile() { spellItem_t t; t.internalName = spell_itr->name.GetString(); + //hash += djb2Hash(const_cast(t.internalName.c_str())); t.name = spell_itr->value["spell_name"].GetString(); + t.name_lowercase = t.name; + lowercaseString(t.name_lowercase); t.id = spell_itr->value["spell_id"].GetInt(); + //hash += (Uint32)((Uint32)t.id << (shift % 32)); ++shift; t.spellTypeStr = spell_itr->value["spell_type"].GetString(); t.spellType = SPELL_TYPE_DEFAULT; if ( t.spellTypeStr == "PROJECTILE" ) @@ -1137,11 +1183,56 @@ void ItemTooltips_t::readItemsFromFile() { t.spellType = SPELL_TYPE_PROJECTILE_SHORT_X3; } + else if ( t.spellTypeStr == "TOUCH_FLOOR" ) + { + t.spellType = SPELL_TYPE_TOUCH_FLOOR; + } + else if ( t.spellTypeStr == "TOUCH_WALL" ) + { + t.spellType = SPELL_TYPE_TOUCH_WALL; + } + else if ( t.spellTypeStr == "TOUCH_ENEMY" ) + { + t.spellType = SPELL_TYPE_TOUCH_ENEMY; + } + else if ( t.spellTypeStr == "TOUCH_ALLY" ) + { + t.spellType = SPELL_TYPE_TOUCH_ALLY; + } + else if ( t.spellTypeStr == "TOUCH_ENTITY" ) + { + t.spellType = SPELL_TYPE_TOUCH_ENTITY; + } + else if ( t.spellTypeStr == "DIVINE_TARGET" ) + { + t.spellType = SPELL_TYPE_DIVINE_TARGET; + } + + //hash += djb2Hash(const_cast(t.spellTypeStr.c_str())); + + if ( spell_itr->value.HasMember("format_tags") ) + { + for ( rapidjson::Value::ConstValueIterator arr_itr = spell_itr->value["format_tags"].Begin(); + arr_itr != spell_itr->value["format_tags"].End(); ++arr_itr ) + { + t.spellFormatTags.push_back(arr_itr->GetString()); + } + } + + if ( spell_itr->value.HasMember("spellbook_item_icon_padding") ) + { + for ( rapidjson::Value::ConstValueIterator arr_itr = spell_itr->value["spellbook_item_icon_padding"].Begin(); + arr_itr != spell_itr->value["spellbook_item_icon_padding"].End(); ++arr_itr ) + { + t.spellbookItemIconPaddingLines.push_back(arr_itr->GetInt()); + } + } for ( rapidjson::Value::ConstValueIterator arr_itr = spell_itr->value["effect_tags"].Begin(); arr_itr != spell_itr->value["effect_tags"].End(); ++arr_itr ) { t.spellTagsStr.push_back(arr_itr->GetString()); + //hash += djb2Hash(const_cast(t.spellTagsStr.back().c_str())); if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "DAMAGE" ) { t.spellTags.insert(SPELL_TAG_DAMAGE); @@ -1162,6 +1253,10 @@ void ItemTooltips_t::readItemsFromFile() { t.spellTags.insert(SPELL_TAG_CURE); } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "BUFF" ) + { + t.spellTags.insert(SPELL_TAG_BUFF); + } else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "BASIC_HIT_MESSAGE" ) { t.spellTags.insert(SPELL_TAG_BASIC_HIT_MESSAGE); @@ -1170,24 +1265,132 @@ void ItemTooltips_t::readItemsFromFile() { t.spellTags.insert(SPELL_TAG_TRACK_HITS); } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "SPELL_SCALE_SPELLBOOK" ) + { + t.spellTags.insert(SPELL_TAG_SPELLBOOK_SCALING); + } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "BONUS_AS_EFFECT_POWER" ) + { + t.spellTags.insert(SPELL_TAG_BONUS_AS_EFFECT_POWER); + } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "SPELL_LEVEL_EVENT" ) + { + t.spellLevelTags.insert(spell_t::SPELL_LEVEL_EVENT_DEFAULT); + } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "SPELL_LEVEL_DMG" ) + { + t.spellLevelTags.insert(spell_t::SPELL_LEVEL_EVENT_DMG); + } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "SPELL_LEVEL_EFFECT" ) + { + t.spellLevelTags.insert(spell_t::SPELL_LEVEL_EVENT_EFFECT); + } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "SPELL_LEVEL_SUMMON" ) + { + t.spellLevelTags.insert(spell_t::SPELL_LEVEL_EVENT_SUMMON); + } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "SPELL_LEVEL_SHAPESHIFT" ) + { + t.spellLevelTags.insert(spell_t::SPELL_LEVEL_EVENT_SHAPESHIFT); + } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "SPELL_LEVEL_SUSTAIN" ) + { + t.spellLevelTags.insert(spell_t::SPELL_LEVEL_EVENT_SUSTAIN); + } + else if ( t.spellTagsStr[t.spellTagsStr.size() - 1] == "SPELL_LEVEL_ASSIST" ) + { + t.spellLevelTags.insert(spell_t::SPELL_LEVEL_EVENT_ASSIST); + } } t.spellbookInternalName = spell_itr->value["spellbook_internal_name"].GetString(); t.magicstaffInternalName = spell_itr->value["magicstaff_internal_name"].GetString(); - + t.fociInternalName = ""; + if ( spell_itr->value.HasMember("foci_internal_name") ) + { + t.fociInternalName = spell_itr->value["foci_internal_name"].GetString(); + } for ( int i = 0; i < NUMITEMS; ++i ) { - if ( items[i].category != SPELLBOOK && items[i].category != MAGICSTAFF ) + if ( items[i].category != SPELLBOOK && items[i].category != MAGICSTAFF && !itemTypeIsFoci((ItemType)i) ) { continue; } if ( t.spellbookInternalName == tmpItems[i].internalName ) { t.spellbookId = i; + if ( !items[i].hasAttribute("spellbook_spell") ) + { + items[i].attributes["spellbook_spell"] = t.id; + } } if ( t.magicstaffInternalName == tmpItems[i].internalName ) { t.magicstaffId = i; + items[i].attributes["magicstaff_spell"] = t.id; + } + if ( t.fociInternalName == tmpItems[i].internalName ) + { + t.fociId = i; + items[i].attributes["foci_spell"] = t.id; + } + } + + //hash += djb2Hash(const_cast(t.spellbookInternalName.c_str())); + //hash += djb2Hash(const_cast(t.magicstaffInternalName.c_str())); + //hash += djb2Hash(const_cast(t.fociInternalName.c_str())); + + t.hasExpandedJSON = false; + + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "mana", t.mana); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "duration", t.duration); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "duration_mult", t.duration_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "duration2", t.duration2); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "duration2_mult", t.duration2_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "damage", t.damage); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "damage_mult", t.damage_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "damage2", t.damage2); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "damage2_mult", t.damage2_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "distance", t.distance); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "distance_mult", t.distance_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "life_time", t.life_time); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "life_mult", t.life_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "cast_time", t.cast_time); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "cast_time_mult", t.cast_time_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "radius", t.radius); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "radius_mult", t.radius_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "difficulty", t.difficulty); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "sustain_mana", t.sustain_mana); + if ( t.sustain_mana > 0 ) + { + t.sustain_duration = std::max(1, t.duration); // set a sane default + } + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "sustain_duration", t.sustain_duration); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "sustain_mult", t.sustain_mult); + setSpellValueIfKeyPresent(t, spell_itr, hash, shift, "drop_table", t.drop_table); + + if ( spell_itr->value.HasMember("school") ) + { + std::string school = spell_itr->value["school"].GetString(); + if ( school == "sorcery" ) + { + t.skillID = PRO_SORCERY; + //hash += (Uint32)((Uint32)t.skillID << (shift % 32)); ++shift; + } + else if ( school == "mysticism" ) + { + t.skillID = PRO_MYSTICISM; + //hash += (Uint32)((Uint32)t.skillID << (shift % 32)); ++shift; + } + else if ( school == "thaumaturgy" ) + { + t.skillID = PRO_THAUMATURGY; + //hash += (Uint32)((Uint32)t.skillID << (shift % 32)); ++shift; + } + else + { + assert(false && "invalid school from items.json!"); + //hash += (Uint32)((Uint32)1 << (shift % 32)); ++shift; } } @@ -1198,6 +1401,69 @@ void ItemTooltips_t::readItemsFromFile() } printlog("[JSON]: Successfully read %d spells from '%s'", spellsRead, inputPath.c_str()); + for ( int i = 0; i < NUM_SPELLS; ++i ) + { + auto find = spellItems.find(i); + if ( find != spellItems.end() ) + { + spellItem_t& t = find->second; + hash += djb2Hash(const_cast(t.internalName.c_str())); + hash += (Uint32)((Uint32)t.id << (shift % 32)); ++shift; + hash += djb2Hash(const_cast(t.spellTypeStr.c_str())); + for ( auto& tag : t.spellTagsStr ) + { + hash += djb2Hash(const_cast(tag.c_str())); + } + + hash += djb2Hash(const_cast(t.spellbookInternalName.c_str())); + hash += djb2Hash(const_cast(t.magicstaffInternalName.c_str())); + hash += djb2Hash(const_cast(t.fociInternalName.c_str())); + + hashSpellProp(hash, shift, t.mana); + hashSpellProp(hash, shift, t.duration); + hashSpellProp(hash, shift, t.duration_mult); + hashSpellProp(hash, shift, t.duration2); + hashSpellProp(hash, shift, t.duration2_mult); + hashSpellProp(hash, shift, t.damage); + hashSpellProp(hash, shift, t.damage_mult); + hashSpellProp(hash, shift, t.damage2); + hashSpellProp(hash, shift, t.damage2_mult); + hashSpellProp(hash, shift, t.distance); + hashSpellProp(hash, shift, t.distance_mult); + hashSpellProp(hash, shift, t.life_time); + hashSpellProp(hash, shift, t.life_mult); + hashSpellProp(hash, shift, t.cast_time); + hashSpellProp(hash, shift, t.cast_time_mult); + hashSpellProp(hash, shift, t.radius); + hashSpellProp(hash, shift, t.radius_mult); + hashSpellProp(hash, shift, t.difficulty); + hashSpellProp(hash, shift, t.sustain_mana); + + hashSpellProp(hash, shift, t.sustain_duration); + hashSpellProp(hash, shift, t.sustain_mult); + hashSpellProp(hash, shift, t.drop_table); + + if ( t.skillID >= 0 ) + { + hash += (Uint32)((Uint32)t.skillID << (shift % 32)); ++shift; + } + else + { + hash += (Uint32)((Uint32)1 << (shift % 32)); ++shift; + } + } + } + + itemsJsonHashRead = hash; + if ( itemsJsonHashRead != kItemsJsonHash ) + { + printlog("[JSON]: Notice: items.json unknown hash, achievements are disabled: %d", itemsJsonHashRead); + } + else + { + printlog("[JSON]: items.json hash verified successfully."); + } + // validation against old items.txt /*for ( int i = 0; i < NUMITEMS; ++i ) { @@ -1336,7 +1602,9 @@ void ItemTooltips_t::readItemLocalizationsFromFile(bool forceLoadBaseDirectory) printlog("[JSON]: Successfully read %d item names, %d spell names from '%s'", itemNameLocalizations.size(), spellNameLocalizations.size(), inputPath.c_str()); assert(itemNameLocalizations.size() == (NUMITEMS)); - assert(spellNameLocalizations.size() == (NUM_SPELLS - 1)); // ignore SPELL_NONE +#ifndef NDEBUG + //assert(spellNameLocalizations.size() == (NUM_SPELLS - 1)); // ignore SPELL_NONE +#endif // apply localizations for ( int i = 0; i < NUMITEMS; ++i ) @@ -1355,6 +1623,8 @@ void ItemTooltips_t::readItemLocalizationsFromFile(bool forceLoadBaseDirectory) for ( auto& spell : spellItems ) { spell.second.name = spellNameLocalizations[spell.second.internalName]; + spell.second.name_lowercase = spell.second.name; + lowercaseString(spell.second.name_lowercase); } /*for ( auto i : itemValueTable ) @@ -1457,7 +1727,7 @@ void ItemTooltips_t::readTooltipsFromFile(bool forceLoadBaseDirectory) { if ( !PHYSFS_getRealDir("/items/item_tooltips.json") ) { - printlog("[JSON]: Error: Could not find file: items/items.json"); + printlog("[JSON]: Error: Could not find file: items/item_tooltips.json"); return; } @@ -1917,6 +2187,7 @@ std::string& ItemTooltips_t::getItemStatusAdjective(Uint32 itemType, Status stat } else if ( items[itemType].category == SCROLL || items[itemType].category == SPELLBOOK + || items[itemType].category == TOME_SPELL || items[itemType].category == BOOK ) { if ( adjectives.find("book_status") == adjectives.end() ) @@ -2105,49 +2376,71 @@ int ItemTooltips_t::getSpellDamageOrHealAmount(const int player, spell_t* spell, int mana = 0; int heal = 0; spellElement_t* primaryElement = nullptr; + real_t damageMult = 1.0; if ( elementRoot ) { node_t* primaryNode = elementRoot->elements.first; - mana = elementRoot->mana; - heal = mana; if ( primaryNode ) { primaryElement = (spellElement_t*)(primaryNode->element); if ( primaryElement ) { - damage = primaryElement->damage; + damage = primaryElement->getDamage(); + heal = primaryElement->getDamage(); + damageMult = primaryElement->getDamageMult(); } } + else + { + damage = elementRoot->getDamage(); + heal = elementRoot->getDamage(); + damageMult = elementRoot->getDamageMult(); + } if ( player >= 0 && players[player] ) { int bonus = 0; - if ( spellbook && itemCategory(spellbook) == MAGICSTAFF ) + if ( spellbook && itemCategory(spellbook) == MAGICSTAFF && spellbook->type != MAGICSTAFF_SCEPTER ) { // no modifier. } else { - if ( spellbook && itemCategory(spellbook) == SPELLBOOK ) + if ( spellbook && (itemCategory(spellbook) == SPELLBOOK || itemTypeIsFoci(spellbook->type)) ) { bonus = getSpellbookBonusPercent( excludePlayerStats ? nullptr : players[player]->entity, excludePlayerStats ? nullptr : stats[player], spellbook); } - damage += (damage * (bonus * 0.01 + damage += damageMult * (damage * (bonus * 0.01 + getBonusFromCasterOfSpellElement( excludePlayerStats ? nullptr : players[player]->entity, - excludePlayerStats ? nullptr : stats[player], primaryElement, spell ? spell->ID : SPELL_NONE))); - heal += (heal * (bonus * 0.01 + excludePlayerStats ? nullptr : stats[player], primaryElement, spell ? spell->ID : SPELL_NONE, spell->skillID))); + heal += damageMult * (heal * (bonus * 0.01 + getBonusFromCasterOfSpellElement( excludePlayerStats ? nullptr : players[player]->entity, - excludePlayerStats ? nullptr : stats[player], primaryElement, spell ? spell->ID : SPELL_NONE))); + excludePlayerStats ? nullptr : stats[player], primaryElement, spell ? spell->ID : SPELL_NONE, spell->skillID))); } } if ( spell->ID == SPELL_HEALING || spell->ID == SPELL_EXTRAHEALING ) { damage = heal; } + if ( spell->ID == SPELL_MUSHROOM ) + { + if ( !excludePlayerStats ) + { + if ( player >= 0 && players[player] ) + { + int bonusEffect = 0; + if ( stats[player]->type == MYCONID && stats[player]->getEffectActive(EFF_GROWTH) >= 2 ) + { + bonusEffect = std::max(bonusEffect, stats[player]->getEffectActive(EFF_GROWTH) - 1); + } + damage += damage * (bonusEffect * 1.0); + } + } + } } return damage; #endif @@ -2188,96 +2481,415 @@ std::string ItemTooltips_t::getSpellDescriptionText(const int player, Item& item std::string& ItemTooltips_t::getIconLabel(Item& item) { #ifndef EDITOR + if ( item.type == SPELL_ITEM && !item.spellNotifyIcon ) { return defaultString; } return tmpItems[item.type].iconLabelPath; #endif } -std::string ItemTooltips_t::getSpellIconText(const int player, Item& item, const bool compendiumTooltipIntro) +const char* spellValueToTier(int val) { -#ifndef EDITOR - spell_t* spell = nullptr; - - if ( itemCategory(&item) == SPELLBOOK ) - { - spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); - } - else if ( itemCategory(&item) == MAGICSTAFF ) - { - for ( auto& s : spellItems ) - { - if ( s.second.magicstaffId == item.type ) - { - spell = getSpellFromID(s.first); - break; - } - } - } - else - { - spell = getSpellFromItem(player, &item, false); - } - if ( !spell || spellItems.find(spell->ID) == spellItems.end() ) + if ( val == 1 ) { return "I"; } + if ( val == 2 ) { return "II"; } + if ( val == 3 ) { return "III"; } + if ( val == 4 ) { return "IV"; } + if ( val == 5 ) { return "V"; } + return "I"; +} + +std::string ItemTooltips_t::getSpellIconFormatText(const int player, Item& item, std::string& format, const spell_t* spell, const int iconIndex, const bool compendiumTooltipIntro) +{ + if ( !spell ) { return defaultString; } + auto def = spellItems.find(spell->ID); + if ( def == spellItems.end() ) { return defaultString; } - std::string templateName = "template_icon_"; - templateName += spellItems[spell->ID].internalName; - if ( templates.find(templateName) == templates.end() ) + enum Comparators + { + COMP_NONE, + COMP_MIN, + COMP_MAX, + COMP_HEAL_OTHER, + COMP_INCOHERENCE, + COMP_WEAKNESS, + COMP_BLOOD_WAVES, + COMP_MAGICIANS_ARMOR, + COMP_BREATHE_FIRE, + COMP_BREATHE_FIRE2, + COMP_SIGIL, + COMP_SANCTUARY + }; + enum Values { - return defaultString; - } + VAL_NUM, + VAL_TIER + }; + Values valueType = VAL_NUM; + Comparators comparator = COMP_NONE; + Entity* caster = compendiumTooltipIntro ? nullptr : players[player]->entity; + Stat* myStats = compendiumTooltipIntro ? nullptr : stats[player]; - std::string str; - for ( auto it = templates[templateName].begin(); - it != templates[templateName].end(); ++it ) + real_t bonusPercent = 0.0; + if ( itemCategory(&item) == SPELLBOOK ) { - str += *it; - if ( std::next(it) != ItemTooltips.templates[templateName].end() ) - { - str += '\n'; - } + bonusPercent = getSpellbookBonusPercent( + caster, + myStats, + &item) / 100.0; } - if ( spellItems[spell->ID].internalName == "spell_summon" ) + for ( auto& formatTag : def->second.spellFormatTags ) { - int numSummons = 1; - if ( !compendiumTooltipIntro ) + if ( formatTag.size() == 0 ) { continue; } + if ( (formatTag.at(0) - '1') != iconIndex ) { continue; } + std::vector vals; + size_t index = 2; + std::string str; + size_t offset = 1; + while ( index + offset < formatTag.size() ) { - if ( (statGetINT(stats[player], players[player]->entity) - + stats[player]->getModifiedProficiency(PRO_MAGIC)) >= SKILL_LEVEL_EXPERT ) + if ( (index + offset == (formatTag.size() - 1)) || formatTag[index + offset] == ' ' ) { - numSummons = 2; + std::string key = (index + offset == (formatTag.size() - 1)) ? formatTag.substr(index) : formatTag.substr(index, offset); + if ( key == "min" ) + { + comparator = COMP_MIN; + } + else if ( key == "dmg" ) + { + vals.push_back(getSpellDamageFromID(spell->ID, caster, myStats, caster, bonusPercent, false)); + } + else if ( key == "dmg2" ) + { + vals.push_back(getSpellDamageSecondaryFromID(spell->ID, caster, myStats, caster, bonusPercent, false)); + } + else if ( key == "dur" ) + { + vals.push_back(getSpellEffectDurationFromID(spell->ID, caster, myStats, caster, bonusPercent)); + } + else if ( key == "dur2" ) + { + vals.push_back(getSpellEffectDurationSecondaryFromID(spell->ID, caster, myStats, caster, bonusPercent)); + } + else if ( key == "tier" ) + { + valueType = VAL_TIER; + } + else if ( key == "comp_heal_other" ) + { + comparator = COMP_HEAL_OTHER; + } + else if ( key == "comp_incoherence" ) + { + comparator = COMP_INCOHERENCE; + } + else if ( key == "comp_weakness" ) + { + comparator = COMP_WEAKNESS; + } + else if ( key == "comp_blood_waves" ) + { + comparator = COMP_BLOOD_WAVES; + } + else if ( key == "comp_magicians_armor" ) + { + comparator = COMP_MAGICIANS_ARMOR; + } + else if ( key == "comp_breathe_fire" ) + { + comparator = COMP_BREATHE_FIRE; + } + else if ( key == "comp_breathe_fire2" ) + { + comparator = COMP_BREATHE_FIRE2; + } + else if ( key == "comp_sigil" ) + { + comparator = COMP_SIGIL; + } + else if ( key == "comp_sanctuary" ) + { + comparator = COMP_SANCTUARY; + } + index += offset + 1; + offset = 1; + } + else + { + ++offset; } } - char buf[128]; - memset(buf, 0, sizeof(buf)); - snprintf(buf, sizeof(buf), str.c_str(), numSummons); - str = buf; - } - else if ( spellItems[spell->ID].spellTags.find(SpellTagTypes::SPELL_TAG_HEALING) != spellItems[spell->ID].spellTags.end() - || spellItems[spell->ID].spellTags.find(SpellTagTypes::SPELL_TAG_DAMAGE) != spellItems[spell->ID].spellTags.end() ) - { - char buf[128]; - memset(buf, 0, sizeof(buf)); - snprintf(buf, sizeof(buf), str.c_str(), getSpellDamageOrHealAmount(player, spell, &item, compendiumTooltipIntro)); - str = buf; - } - return str; -#else - return std::string(""); -#endif -} + if ( vals.size() ) + { + if ( comparator == COMP_HEAL_OTHER && vals.size() >= 4 ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + real_t healTicks = vals[0] * (vals[1] / 100.0) * vals[2]; + snprintf(buf, sizeof(buf), format.c_str(), (int)(healTicks / vals[3])); + str = buf; + } + else if ( (comparator == COMP_INCOHERENCE || comparator == COMP_WEAKNESS) && vals.size() >= 2 ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + int effectStrength = std::min(vals[0], vals[1]); + int reduction = 100.0 * std::min(0.9, 0.2 + (effectStrength - 1) * 0.1); + snprintf(buf, sizeof(buf), format.c_str(), -reduction); + str = buf; + } + else if ( comparator == COMP_MAGICIANS_ARMOR && vals.size() >= 3 ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); -real_t ItemTooltips_t::getSpellSustainCostPerSecond(int spellID) -{ - real_t cost = 0.0; - switch ( spellID ) - { - case SPELL_REFLECT_MAGIC: - cost = 6.0; - break; + int instances = vals[2]; + instances *= ((myStats ? myStats->getModifiedProficiency(spell->skillID) : 0) + statGetINT(myStats, caster)) / std::max(1, vals[1]); + int maxInstances = vals[0]; + instances = std::min(std::max(1, instances), maxInstances); + + snprintf(buf, sizeof(buf), format.c_str(), instances); + str = buf; + } + else if ( comparator == COMP_BREATHE_FIRE && vals.size() >= 2 ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + + snprintf(buf, sizeof(buf), format.c_str(), std::min(10, vals[1]), vals[0]); + str = buf; + } + else if ( comparator == COMP_BREATHE_FIRE2 ) + { + int maxStrength = 10; + int minStrength = 2; + if ( myStats ) + { + if ( myStats->type == SALAMANDER + && myStats->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + minStrength += 3; + } + maxStrength = std::min(maxStrength, minStrength + std::max(0, statGetCHR(myStats, caster)) / 5); + } + else + { + maxStrength = minStrength; + } + + snprintf(buf, sizeof(buf), format.c_str(), maxStrength); + str = buf; + } + else if ( comparator == COMP_SIGIL && vals.size() >= 2 ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + + int tier = std::min(vals[0], vals[1]); + int dmgMult = 10 + 10 * tier; + int healMult = 10 + 10 * tier; + + snprintf(buf, sizeof(buf), format.c_str(), healMult, dmgMult); + str = buf; + } + else if ( comparator == COMP_SANCTUARY && vals.size() >= 2 ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + + int tier = std::min(vals[0], vals[1]); + int reduction = std::min(80, std::max(0, 10 + (15 * tier))); + + snprintf(buf, sizeof(buf), format.c_str(), reduction); + str = buf; + } + else if ( (comparator == COMP_BLOOD_WAVES) && vals.size() >= 2 ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + spellElement_t* element = nullptr; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + int damage = vals[0] + std::max(1, vals[1] * statGetINT(myStats, caster)) * (1.0 + bonusPercent + getBonusFromCasterOfSpellElement(caster, myStats, element, spell->ID, spell->skillID)); + snprintf(buf, sizeof(buf), format.c_str(), damage); + str = buf; + } + else if ( vals.size() == 1 || comparator == COMP_MIN || comparator == COMP_MAX ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + if ( vals.size() == 2 ) + { + if ( comparator == COMP_MIN ) + { + if ( valueType == VAL_TIER ) + { + snprintf(buf, sizeof(buf), format.c_str(), spellValueToTier(std::min(vals[0], vals[1]))); + } + else + { + snprintf(buf, sizeof(buf), format.c_str(), std::min(vals[0], vals[1])); + } + } + else if ( comparator == COMP_MAX ) + { + snprintf(buf, sizeof(buf), format.c_str(), std::max(vals[0], vals[1])); + } + } + else + { + if ( valueType == VAL_TIER ) + { + snprintf(buf, sizeof(buf), format.c_str(), spellValueToTier(vals[0])); + } + else + { + snprintf(buf, sizeof(buf), format.c_str(), vals[0]); + } + } + str = buf; + } + else if ( vals.size() == 2 ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + snprintf(buf, sizeof(buf), format.c_str(), vals[0], vals[1]); + str = buf; + } + return str; + } + } + return defaultString; +} + +std::string ItemTooltips_t::getSpellIconText(const int player, Item& item, const bool compendiumTooltipIntro) +{ +#ifndef EDITOR + spell_t* spell = nullptr; + + if ( itemCategory(&item) == SPELLBOOK ) + { + spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + } + else if ( itemCategory(&item) == MAGICSTAFF ) + { + for ( auto& s : spellItems ) + { + if ( s.second.magicstaffId == item.type ) + { + spell = getSpellFromID(s.first); + break; + } + } + } + else + { + spell = getSpellFromItem(player, &item, false); + } + if ( !spell || spellItems.find(spell->ID) == spellItems.end() ) + { + return defaultString; + } + std::string templateName = "template_icon_"; + templateName += spellItems[spell->ID].internalName; + + if ( templates.find(templateName) == templates.end() ) + { + return defaultString; + } + + std::string str; + for ( auto it = templates[templateName].begin(); + it != templates[templateName].end(); ++it ) + { + str += *it; + if ( std::next(it) != ItemTooltips.templates[templateName].end() ) + { + str += '\n'; + } + } + + if ( spellItems[spell->ID].internalName == "spell_summon" ) + { + int numSummons = 1; + if ( !compendiumTooltipIntro ) + { + if ( (statGetINT(stats[player], players[player]->entity) + + stats[player]->getModifiedProficiency(spell->skillID)) >= SKILL_LEVEL_EXPERT ) + { + numSummons = 2; + } + } + char buf[128]; + memset(buf, 0, sizeof(buf)); + snprintf(buf, sizeof(buf), str.c_str(), numSummons); + str = buf; + } + else if ( spell->ID == SPELL_BREATHE_FIRE ) + { + std::string result = getSpellIconFormatText(player, item, str, spell, 0, compendiumTooltipIntro); + if ( result != "" ) + { + str = result; + } + } + else if ( spellItems[spell->ID].spellTags.find(SpellTagTypes::SPELL_TAG_HEALING) != spellItems[spell->ID].spellTags.end() + || spellItems[spell->ID].spellTags.find(SpellTagTypes::SPELL_TAG_DAMAGE) != spellItems[spell->ID].spellTags.end() ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + snprintf(buf, sizeof(buf), str.c_str(), getSpellDamageOrHealAmount(player, spell, &item, compendiumTooltipIntro)); + str = buf; + } + else if ( spellItems[spell->ID].spellFormatTags.size() ) + { + std::string result = getSpellIconFormatText(player, item, str, spell, 0, compendiumTooltipIntro); + if ( result != "" ) + { + str = result; + } + } + + return str; +#else + return std::string(""); +#endif +} + +real_t ItemTooltips_t::getSpellSustainCostPerSecond(int spellID) +{ + real_t cost = 0.0; + if ( auto spell = getSpellFromID(spellID) ) + { + if ( spell_isChanneled(spell) ) + { + if ( spell->elements.first ) + { + if ( spellElement_t* element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->channeledMana > 0 ) + { + return element->duration / (real_t)TICKS_PER_SECOND; + } + } + } + } + } + /*switch ( spellID ) + { + case SPELL_REFLECT_MAGIC: + cost = 6.0; + break; case SPELL_LEVITATION: cost = 0.6; break; @@ -2295,7 +2907,7 @@ real_t ItemTooltips_t::getSpellSustainCostPerSecond(int spellID) break; default: break; - } + }*/ return cost; } @@ -2326,6 +2938,24 @@ std::string& ItemTooltips_t::getSpellTypeString(const int player, Item& item) case SPELL_TYPE_PROJECTILE_SHORT_X3: return adjectives["spell_strings"]["spell_type_projectile_3x"]; break; + case SPELL_TYPE_TOUCH_FLOOR: + return adjectives["spell_strings"]["spell_type_touch_floor"]; + break; + case SPELL_TYPE_TOUCH_WALL: + return adjectives["spell_strings"]["spell_type_touch_wall"]; + break; + case SPELL_TYPE_TOUCH_ENEMY: + return adjectives["spell_strings"]["spell_type_touch_enemy"]; + break; + case SPELL_TYPE_TOUCH_ALLY: + return adjectives["spell_strings"]["spell_type_touch_ally"]; + break; + case SPELL_TYPE_TOUCH_ENTITY: + return adjectives["spell_strings"]["spell_type_touch_entity"]; + break; + case SPELL_TYPE_DIVINE_TARGET: + return adjectives["spell_strings"]["spell_type_divine_target"]; + break; case SPELL_TYPE_DEFAULT: default: return defaultString; @@ -2376,6 +3006,22 @@ std::string ItemTooltips_t::getCostOfSpellString(const int player, Item& item) } snprintf(buf, sizeof(buf), str.c_str(), getCostOfSpell(spell)); } + else if ( spell->ID == SPELL_LEAD_BOLT || spell->ID == SPELL_MERCURY_BOLT + || spell->ID == SPELL_FORGE_METAL_SCRAP || spell->ID == SPELL_FORGE_MAGIC_SCRAP ) + { + std::string templateName = "template_spell_cost_gold"; + std::string str; + for ( auto it = templates[templateName].begin(); + it != templates[templateName].end(); ++it ) + { + str += *it; + if ( std::next(it) != ItemTooltips.templates[templateName].end() ) + { + str += '\n'; + } + } + snprintf(buf, sizeof(buf), str.c_str(), getCostOfSpell(spell), getGoldCostOfSpell(spell, player)); + } else { std::string templateName = "template_spell_cost"; @@ -2484,6 +3130,10 @@ std::string ItemTooltips_t::getSpellIconPath(const int player, Item& item, int s { spellImageNode = list_Node(&items[SPELL_ITEM].images, getSpellIDFromSpellbook(item.type)); } + else if ( itemCategory(&item) == TOME_SPELL ) + { + spellImageNode = list_Node(&items[SPELL_ITEM].images, item.getTomeSpellID()); + } else if ( item.type == TOOL_SPELLBOT ) { spellImageNode = list_Node(&items[SPELL_ITEM].images, item.status < EXCELLENT ? SPELL_FORCEBOLT : SPELL_MAGICMISSILE); @@ -2592,6 +3242,8 @@ std::string& ItemTooltips_t::getItemProficiencyName(int proficiency) return adjectives["proficiency_types"]["shield"]; case PRO_RANGED: return adjectives["proficiency_types"]["ranged"]; + case PRO_SORCERY: + return adjectives["proficiency_types"]["magic"]; default: return defaultString; } @@ -2732,7 +3384,7 @@ Sint32 getStatAttributeBonusFromItem(const int player, Item& item, std::string& void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, Item& item, std::string& str, int iconIndex, std::string& conditionalAttribute, Frame* parentFrame) { #ifndef EDITOR - auto itemTooltip = tooltips[tooltipType]; + //auto itemTooltip = tooltips[tooltipType]; static Stat itemDummyStat(0); static char buf[1024]; memset(buf, 0, sizeof(buf)); @@ -2768,16 +3420,78 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I } return; } + else if ( item.type == SPELL_ITEM && conditionalAttribute.find("spell_") != std::string::npos ) + { + if ( auto spell = getSpellFromItem(player, &item, false) ) + { + auto def = spellItems.find(spell->ID); + if ( def != spellItems.end() ) + { + if ( def->second.spellFormatTags.size() ) + { + std::string result = getSpellIconFormatText(player, item, str, spell, iconIndex, compendiumTooltipIntro); + if ( result != "" ) + { + str = result; + } + } + } + } + return; + } else if ( conditionalAttribute.find("SPELLBOOK_") != std::string::npos ) { if ( conditionalAttribute == "SPELLBOOK_SPELLINFO_LEARNED" ) { - str = getSpellIconText(player, item, compendiumTooltipIntro); + if ( iconIndex == 1 ) + { + if ( auto spell = getSpellFromID(getSpellIDFromSpellbook(item.type)) ) + { + auto def = spellItems.find(spell->ID); + if ( def != spellItems.end() ) + { + if ( def->second.spellFormatTags.size() ) + { + std::string result = getSpellIconFormatText(player, item, str, spell, iconIndex, compendiumTooltipIntro); + if ( result != "" ) + { + str = result; + } + } + } + } + } + else + { + str = getSpellIconText(player, item, compendiumTooltipIntro); + if ( itemCategory(&item) == SPELLBOOK ) + { + if ( auto spell = getSpellFromID(getSpellIDFromSpellbook(item.type)) ) + { + if ( iconIndex < ItemTooltips.spellItems[spell->ID].spellbookItemIconPaddingLines.size() ) + { + for ( int i = 0; i < ItemTooltips.spellItems[spell->ID].spellbookItemIconPaddingLines[iconIndex]; ++i ) + { + str += '\n'; + } + } + } + } + } return; } - else if ( conditionalAttribute == "SPELLBOOK_SPELLINFO_UNLEARNED" ) + else if ( conditionalAttribute == "SPELLBOOK_SPELLINFO_UNLEARNED" + || conditionalAttribute == "SPELLBOOK_SPELLINFO_TOME" ) { - spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + spell_t* spell = nullptr; + if ( itemCategory(&item) == SPELLBOOK ) + { + spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + } + else if ( itemCategory(&item) == TOME_SPELL ) + { + spell = getSpellFromID(item.getTomeSpellID()); + } if ( spell ) { snprintf(buf, sizeof(buf), str.c_str(), spell->getSpellName()); @@ -2792,14 +3506,27 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I compendiumTooltipIntro ? nullptr : stats[player], &item); spellBookBonusPercent *= ((items[item.type].attributes["SPELLBOOK_CAST_BONUS"]) / 100.0); - int spellID = getSpellIDFromSpellbook(item.type); + int spellID = itemCategory(&item) == SPELLBOOK ? getSpellIDFromSpellbook(item.type) : item.getTomeSpellID(); if ( spellItems.find(spellID) == spellItems.end() ) { return; } SpellItemTypes spellType = spellItems[spellID].spellType; - if ( spellItems[spellID].spellTags.find(SPELL_TAG_DAMAGE) != spellItems[spellID].spellTags.end() ) + if ( spellItems[spellID].spellTags.find(SPELL_TAG_BONUS_AS_EFFECT_POWER) != spellItems[spellID].spellTags.end() ) + { + str = ""; + for ( auto it = ItemTooltips.templates["template_spellbook_icon_pwr_bonus"].begin(); + it != ItemTooltips.templates["template_spellbook_icon_pwr_bonus"].end(); ++it ) + { + str += *it; + if ( std::next(it) != ItemTooltips.templates["template_spellbook_icon_pwr_bonus"].end() ) + { + str += '\n'; + } + } + } + else if ( spellItems[spellID].spellTags.find(SPELL_TAG_DAMAGE) != spellItems[spellID].spellTags.end() ) { str = ""; for ( auto it = ItemTooltips.templates["template_spellbook_icon_damage_bonus"].begin(); @@ -3498,28 +4225,29 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I || (skill == PRO_LEADERSHIP && item.type == HAT_PLUMED_CAP) || (skill == PRO_RANGED && item.type == HAT_BOUNTYHUNTER) || (skill == PRO_STEALTH && item.type == HAT_HOOD_WHISPERS) - || (skill == PRO_SPELLCASTING && (item.type == HAT_CIRCLET || item.type == HAT_CIRCLET_WISDOM)) - || (skill == PRO_ALCHEMY && item.type == MASK_HAZARD_GOGGLES) ) - { + || (skill == PRO_MYSTICISM && (item.type == HAT_CIRCLET)) + || (skill == PRO_SORCERY && (item.type == HAT_CIRCLET_SORCERY)) + || (skill == PRO_THAUMATURGY && (item.type == HAT_CIRCLET_THAUMATURGY)) + || (skill == PRO_ALCHEMY && item.type == MASK_HAZARD_GOGGLES) + || ((skill == PRO_MYSTICISM || skill == PRO_SORCERY || skill == PRO_THAUMATURGY) + && itemTypeIsFoci(item.type)) ) + { + int bonus = 10; + if ( skill == PRO_MYSTICISM || skill == PRO_SORCERY || skill == PRO_THAUMATURGY ) + { + bonus = 5; + } if ( item.beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player]) ) { - equipmentBonus += std::min(Stat::maxEquipmentBonusToSkill, (1 + abs(item.beatitude)) * 10); + equipmentBonus += std::min(Stat::maxEquipmentBonusToSkill, (1 + abs(item.beatitude)) * bonus); } else { - equipmentBonus += 10; + equipmentBonus += bonus; } } - std::string skillName = ""; - for ( auto s : Player::SkillSheet_t::skillSheetData.skillEntries ) - { - if ( s.skillId == skill ) - { - skillName = s.name; - break; - } - } + std::string skillName = Player::SkillSheet_t::getSkillNameFromID(skill); snprintf(buf, sizeof(buf), str.c_str(), equipmentBonus, skillName.c_str()); } else if ( conditionalAttribute == "EFF_BOULDER_RES" ) @@ -3545,19 +4273,74 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I } snprintf(buf, sizeof(buf), str.c_str(), 100 - (int)(mult * 100)); } - else if ( conditionalAttribute.find("EFF_PWR") != std::string::npos ) + else if ( conditionalAttribute.find("EFF_CAST_TOUCHSPEED_") != std::string::npos + || conditionalAttribute.find("EFF_CAST_TOUCH_") != std::string::npos ) { real_t bonus = 0.0; - if ( conditionalAttribute == "EFF_PWR" ) + if ( (item.type == ROBE_HEALER) + || (item.type == ROBE_WIZARD) + || (item.type == ROBE_CULTIST) + || (item.type == ROBE_MONK) ) { - if ( item.type == HAT_CIRCLET - || item.type == HAT_CIRCLET_WISDOM ) + if ( item.beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player]) ) { - if ( item.beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player]) ) - { - bonus += (0.05 + (0.05 * abs(item.beatitude))); - } - else + bonus = std::min(0.5, 0.2 + 0.1 * abs(item.beatitude)); + } + else + { + bonus = 0.2; + } + } + else if ( item.type == SHAWL ) + { + if ( item.beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player]) ) + { + bonus = std::min(1.0, 0.35 + 0.35 * abs(item.beatitude)); + } + else + { + bonus = 0.35; + } + } + + std::string skillName = ""; + if ( conditionalAttribute.find("SORCERY") != std::string::npos ) + { + skillName = Player::SkillSheet_t::getSkillNameFromID(PRO_SORCERY); + } + else if ( conditionalAttribute.find("MYSTICISM") != std::string::npos ) + { + skillName = Player::SkillSheet_t::getSkillNameFromID(PRO_MYSTICISM); + } + else if ( conditionalAttribute.find("THAUMATURGY") != std::string::npos ) + { + skillName = Player::SkillSheet_t::getSkillNameFromID(PRO_THAUMATURGY); + } + if ( skillName != "" ) + { + snprintf(buf, sizeof(buf), str.c_str(), (int)(bonus * 100), + skillName.c_str()); + } + else + { + snprintf(buf, sizeof(buf), str.c_str(), (int)(bonus * 100)); + } + } + else if ( conditionalAttribute.find("EFF_PWR") != std::string::npos ) + { + real_t bonus = 0.0; + if ( conditionalAttribute == "EFF_PWR" ) + { + if ( item.type == HAT_CIRCLET + || item.type == HAT_CIRCLET_SORCERY + || item.type == HAT_CIRCLET_THAUMATURGY + || item.type == HAT_CIRCLET_WISDOM ) + { + if ( item.beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player]) ) + { + bonus += (0.05 + (0.05 * abs(item.beatitude))); + } + else { bonus = 0.05; } @@ -3576,7 +4359,10 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I snprintf(buf, sizeof(buf), str.c_str(), (int)(bonus * 100), getItemEquipmentEffectsForIconText(conditionalAttribute).c_str()); } - else if ( conditionalAttribute == "EFF_PWR_DMG" ) + else if ( conditionalAttribute == "EFF_PWR_DMG" + || conditionalAttribute == "EFF_PWR_SORCERY" + || conditionalAttribute == "EFF_PWR_MYSTICISM" + || conditionalAttribute == "EFF_PWR_THAUMATURGY" ) { if ( item.type == HAT_MITER || item.type == HAT_HEADDRESS ) { @@ -3609,6 +4395,241 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I getItemEquipmentEffectsForIconText(conditionalAttribute).c_str()); } } + else if ( conditionalAttribute == "EFF_CHAIN_RESIST" || conditionalAttribute == "EFF_QUILT_RESIST" + || conditionalAttribute == "EFF_BONE_RESIST" || conditionalAttribute == "EFF_BANDIT_LEATHER" ) + { + real_t base = 0.1; + real_t bonus = 0.05; + if ( items[item.type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BREASTPLATE ) + { + base = 0.2; + } + if ( item.type == BANDIT_BREASTPIECE ) + { + base = 0.15; + real_t mod = 0.0; + if ( item.beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player]) ) + { + mod = Entity::getDamageTableEquipmentMod(*stats[player], item, base, bonus); + } + else + { + mod = base; + } + snprintf(buf, sizeof(buf), str.c_str(), (int)(mod * 100)); + } + else + { + real_t mod = 0.0; + if ( item.beatitude >= 0 || shouldInvertEquipmentBeatitude(stats[player]) ) + { + mod = Entity::getDamageTableEquipmentMod(*stats[player], item, base, bonus); + } + else + { + mod = base; + } + char tmp[128] = ""; + if ( ItemTooltips.templates["template_armor_resist_icon"].size() ) + { + std::string skillnames = ""; + if ( conditionalAttribute == "EFF_CHAIN_RESIST" ) + { + skillnames += getItemProficiencyName(PRO_SWORD); + skillnames += "/"; + skillnames += getItemProficiencyName(PRO_AXE); + } + else if ( conditionalAttribute == "EFF_QUILT_RESIST" ) + { + skillnames += getItemProficiencyName(PRO_POLEARM); + skillnames += "/"; + skillnames += getItemProficiencyName(PRO_RANGED); + } + else if ( conditionalAttribute == "EFF_BONE_RESIST" ) + { + skillnames += getItemProficiencyName(PRO_UNARMED); + skillnames += "/"; + skillnames += getItemProficiencyName(PRO_MACE); + } + snprintf(tmp, sizeof(tmp), ItemTooltips.templates["template_armor_resist_icon"][0].c_str(), + skillnames.c_str()); + } + snprintf(buf, sizeof(buf), str.c_str(), (int)(mod * 100), + tmp); + } + } + else if ( conditionalAttribute.find("EFF_FOCI_MAGE") != std::string::npos ) + { + int spellID = getSpellIDFromFoci(item.type); + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + char buf[128]; + memset(buf, 0, sizeof(buf)); + snprintf(buf, sizeof(buf), str.c_str(), getSpellDamageOrHealAmount(player, spell, &item, compendiumTooltipIntro)); + str = buf; + } + } + return; + } + else if ( conditionalAttribute == "EFF_INSTRUMENT_MP_COST" ) + { + if ( itemTypeIsInstrument(item.type) ) + { + snprintf(buf, sizeof(buf), str.c_str(), items[item.type].attributes[conditionalAttribute]); + str = buf; + return; + } + } + else if ( conditionalAttribute.find("EFF_INSTRUMENT") != std::string::npos ) + { + Sint32 CHR = statGetCHR(stats[player], players[player]->entity); + if ( compendiumTooltipIntro ) { CHR = 0; } + Uint8 effectStrength = std::max(1, std::min(255, CHR + 1)); + int tier = 1; + std::string tierString = "I"; + int nextCHR = Stat::kEnsembleBreakPointTier2; + if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier4 ) + { + nextCHR = 0; + tierString = "IV"; + tier = 4; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier3 ) + { + nextCHR = Stat::kEnsembleBreakPointTier4; + tierString = "III"; + tier = 3; + + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier2 ) + { + nextCHR = Stat::kEnsembleBreakPointTier3; + tierString = "II"; + tier = 2; + } + + int eff1_min = 0; + int eff2_min = 0; + int effTier_min = 0; + int eff1 = 0; + int eff2 = 0; + int effTier = 0; + if ( item.type == INSTRUMENT_FLUTE ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_EFF_1, effectStrength); + eff2 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_EFF_2, effectStrength); + effTier = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_TIER, effectStrength); + + eff1_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_EFF_1, 1); + eff2_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_EFF_2, 1); + effTier_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_TIER, 1); + } + else if ( item.type == INSTRUMENT_LUTE ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_EFF_1, effectStrength); + eff2 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_EFF_2, effectStrength); + effTier = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_TIER, effectStrength); + + eff1_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_EFF_1, 1); + eff2_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_EFF_2, 1); + effTier_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_TIER, 1); + } + else if ( item.type == INSTRUMENT_LYRE ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_EFF_1, effectStrength); + eff2 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_EFF_2, effectStrength); + effTier = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_TIER, effectStrength); + + eff1_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_EFF_1, 1); + eff2_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_EFF_2, 1); + effTier_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_TIER, 1); + } + else if ( item.type == INSTRUMENT_HORN ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_EFF_1, effectStrength); + eff2 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_EFF_2, effectStrength); + effTier = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_TIER, effectStrength); + + eff1_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_EFF_1, 1); + eff2_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_EFF_2, 1); + effTier_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_TIER, 1); + } + else if ( item.type == INSTRUMENT_DRUM ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_EFF_1, effectStrength); + eff2 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_EFF_2, effectStrength); + effTier = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_TIER, effectStrength); + + eff1_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_EFF_1, 1); + eff2_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_EFF_2, 1); + effTier_min = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_TIER, 1); + } + + snprintf(buf, sizeof(buf), str.c_str(), tierString.c_str(), eff1_min, eff1, effTier_min, effTier); + } + else if ( conditionalAttribute.find("EFF_FOCI_") != std::string::npos ) + { + Sint32 CHR = statGetCHR(stats[player], players[player]->entity); + if ( compendiumTooltipIntro ) { CHR = 0; } + int tier = 1; + std::string tierString = "I"; + if ( CHR >= 3 && CHR <= 7 ) + { + tier = 1; + } + else if ( CHR >= 8 && CHR <= 14 ) + { + tierString = "II"; + tier = 2; + } + else if ( CHR >= 15 && CHR <= 29 ) + { + tierString = "III"; + tier = 3; + } + else if ( CHR >= 30 ) + { + tierString = "IV"; + tier = 4; + } + + if ( conditionalAttribute == "EFF_FOCI_PEACE" ) + { + int effect = getSpellEffectDurationSecondaryFromID(SPELL_FOCI_LIGHT_PEACE, nullptr, nullptr, nullptr); + int effectMax = effect - (tier - 1) * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_PEACE, nullptr, nullptr, nullptr); + snprintf(buf, sizeof(buf), str.c_str(), tierString.c_str(), effect / TICKS_PER_SECOND, effectMax / TICKS_PER_SECOND); + } + else if ( conditionalAttribute == "EFF_FOCI_PURITY" ) + { + int effect = getSpellEffectDurationSecondaryFromID(SPELL_FOCI_LIGHT_PURITY, nullptr, nullptr, nullptr); + int effectMax = effect - (tier - 1) * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_PURITY, nullptr, nullptr, nullptr); + snprintf(buf, sizeof(buf), str.c_str(), tierString.c_str(), effect / TICKS_PER_SECOND, effectMax / TICKS_PER_SECOND); + } + else if ( conditionalAttribute == "EFF_FOCI_JUSTICE" ) + { + int effect = getSpellEffectDurationSecondaryFromID(SPELL_FOCI_LIGHT_JUSTICE, nullptr, nullptr, nullptr); + int effectMax = effect + (tier - 1) * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_JUSTICE, nullptr, nullptr, nullptr); + snprintf(buf, sizeof(buf), str.c_str(), tierString.c_str(), effect, effectMax); + } + else if ( conditionalAttribute == "EFF_FOCI_SANCTUARY" ) + { + int effect = getSpellDamageFromID(SPELL_FOCI_LIGHT_SANCTUARY, nullptr, nullptr, nullptr); + int effectMax = effect + (tier - 1) * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_SANCTUARY, nullptr, nullptr, nullptr); + snprintf(buf, sizeof(buf), str.c_str(), tierString.c_str(), effect, effectMax); + } + else if ( conditionalAttribute == "EFF_FOCI_PROVIDENCE" ) + { + int effect = getSpellEffectDurationSecondaryFromID(SPELL_FOCI_LIGHT_PROVIDENCE, nullptr, nullptr, nullptr); + int effectMax = effect + (tier - 1) * getSpellDamageSecondaryFromID(SPELL_FOCI_LIGHT_PROVIDENCE, nullptr, nullptr, nullptr); + snprintf(buf, sizeof(buf), str.c_str(), tierString.c_str(), effect, effectMax); + } + else + { + return; + } + } else if ( conditionalAttribute.find("EFF_ARTIFACT_") != std::string::npos ) { real_t amount = 0.0; @@ -3639,6 +4660,23 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I snprintf(buf, sizeof(buf), str.c_str(), percent, amount); } } + else if ( conditionalAttribute == "EFF_FLAIL" ) + { + int percent = (50 + (compendiumTooltipIntro ? 0 : stats[player]->getModifiedProficiency(PRO_MACE) * 25 / 100.0)) - 100; + snprintf(buf, sizeof(buf), str.c_str(), percent); + } + else if ( conditionalAttribute == "EFF_SHILLELAGH" ) + { + real_t variance = 20; + real_t baseSkillModifier = 50.0; // 40-60 base + real_t skillModifier = baseSkillModifier - (variance / 2) + (compendiumTooltipIntro ? 0 : stats[player]->getModifiedProficiency(PRO_MYSTICISM) / 2.0); + + int valueLow = skillModifier; + int valueHigh = skillModifier + variance; + valueHigh = std::min(100, valueHigh); + + snprintf(buf, sizeof(buf), str.c_str(), valueLow, valueHigh); + } else { snprintf(buf, sizeof(buf), str.c_str(), getItemEquipmentEffectsForIconText(conditionalAttribute).c_str()); @@ -3649,6 +4687,20 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I Sint32 AC = item.armorGetAC(stats[player]); snprintf(buf, sizeof(buf), str.c_str(), AC, getItemStatFullName("AC").c_str()); } + else if ( conditionalAttribute == "FOCI_MP_COST" ) + { + int mpCost = 0; + int spellID = getSpellIDFromFoci(item.type); + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + mpCost = getCostOfSpell(spell, compendiumTooltipIntro ? nullptr : players[player]->entity); + } + } + + snprintf(buf, sizeof(buf), str.c_str(), mpCost); + } else { return; @@ -3674,6 +4726,7 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I || tooltipType.find("tooltip_ranged") != std::string::npos || tooltipType.find("tooltip_quiver") != std::string::npos || tooltipType.compare("tooltip_tool_pickaxe") == 0 + || tooltipType.compare("tooltip_magicstaff_scepter") == 0 ) { Sint32 atk = item.weaponGetAttack(stats[player]); @@ -3763,6 +4816,11 @@ void ItemTooltips_t::formatItemIcon(const int player, std::string tooltipType, I const int atk = 10 + 3 * (item.status + item.beatitude); snprintf(buf, sizeof(buf), str.c_str(), atk); } + else if ( tooltipType.find("tooltip_jewel") != std::string::npos ) + { + int tier = std::max(DECREPIT, item.status); + snprintf(buf, sizeof(buf), str.c_str(), adjectives["jewel_levels"][std::to_string(tier)].c_str(), getItemStatusAdjective(item.type, item.status).c_str()); + } else if ( tooltipType.find("tooltip_scroll") != std::string::npos ) { if ( conditionalAttribute == "SCROLL_LABEL" ) @@ -3840,7 +4898,7 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType } } - auto itemTooltip = ItemTooltips.tooltips[tooltipType]; + //auto itemTooltip = ItemTooltips.tooltips[tooltipType]; memset(buf, 0, sizeof(buf)); @@ -3904,7 +4962,8 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType } else if ( detailTag.compare("weapon_atk_from_player_stat") == 0 ) { - snprintf(buf, sizeof(buf), str.c_str(), (!compendiumTooltipIntro && stats[player]) ? statGetSTR(stats[player], players[player]->entity) : 0); + snprintf(buf, sizeof(buf), str.c_str(), + (int)(((!compendiumTooltipIntro && stats[player]) ? statGetSTR(stats[player], players[player]->entity) : 0) * 100 * Entity::PlayerAttackMeleeStatFactor)); } else if ( detailTag.compare("ring_unarmed_atk") == 0 ) { @@ -3991,15 +5050,7 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType //} //val = ((val * equipmentModifier) * bonusModifier); - std::string skillName = ""; - for ( auto s : Player::SkillSheet_t::skillSheetData.skillEntries ) - { - if ( s.skillId == PRO_STEALTH ) - { - skillName = s.name; - break; - } - } + std::string skillName = Player::SkillSheet_t::getSkillNameFromID(PRO_STEALTH); snprintf(buf, sizeof(buf), str.c_str(), skillName.c_str()); } else if ( detailTag == "EFF_SILKEN_BOW" ) @@ -4028,15 +5079,7 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType } } - std::string skillName = ""; - for ( auto s : Player::SkillSheet_t::skillSheetData.skillEntries ) - { - if ( s.skillId == PRO_LEADERSHIP ) - { - skillName = s.name; - break; - } - } + std::string skillName = Player::SkillSheet_t::getSkillNameFromID(PRO_LEADERSHIP); snprintf(buf, sizeof(buf), str.c_str(), baseBonus, chanceBonus, skillName.c_str(), getItemStatShortName("CHR").c_str()); } @@ -4077,7 +5120,8 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType } else if ( detailTag.compare("thrown_atk_from_player_stat") == 0 ) { - snprintf(buf, sizeof(buf), str.c_str(), (!compendiumTooltipIntro && stats[player]) ? (statGetDEX(stats[player], players[player]->entity) / 4) : 0); + snprintf(buf, sizeof(buf), str.c_str(), + (int)(((!compendiumTooltipIntro && stats[player]) ? (statGetDEX(stats[player], players[player]->entity) / 4) : 0) * 100 * Entity::PlayerAttackThrownStatFactor)); } else if ( detailTag.compare("thrown_skill_modifier") == 0 ) { @@ -4142,6 +5186,18 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType return; } } + else if ( tooltipType == "tooltip_jewel" ) + { + if ( detailTag == "jewel_limit" ) + { + int allowedFollowers = std::min(8, std::max(4, compendiumTooltipIntro ? 0 : 2 * (stats[player]->getModifiedProficiency(PRO_LEADERSHIP) / 20))); + snprintf(buf, sizeof(buf), str.c_str(), allowedFollowers); + } + else + { + return; + } + } else if ( tooltipType.find("tooltip_thrown") != std::string::npos || tooltipType.find("tooltip_boomerang") != std::string::npos ) { @@ -4157,15 +5213,429 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType shouldInvertEquipmentBeatitude(stats[player]) ? abs(item.beatitude) : item.beatitude, getItemBeatitudeAdjective(item.beatitude).c_str()); } - else if ( detailTag.compare("thrown_atk_from_player_stat") == 0 ) + else if ( detailTag.compare("thrown_atk_from_player_stat") == 0 ) + { + snprintf(buf, sizeof(buf), str.c_str(), + (int)(((!compendiumTooltipIntro && stats[player]) ? (statGetDEX(stats[player], players[player]->entity) / 4) : 0) * 100 * Entity::PlayerAttackThrownStatFactor)); + } + else if ( detailTag.compare("thrown_skill_modifier") == 0 ) + { + int skillLVL = compendiumTooltipIntro ? 0 : (stats[player]->getModifiedProficiency(proficiency) / 20); + snprintf(buf, sizeof(buf), str.c_str(), static_cast(100 * thrownDamageSkillMultipliers[std::min(skillLVL, 5)] - 100), + getItemProficiencyName(proficiency).c_str()); + } + else + { + return; + } + } + else if ( tooltipType == "tooltip_instrument" ) + { + Entity* caster = compendiumTooltipIntro ? nullptr : players[player]->entity; + Stat* myStats = compendiumTooltipIntro ? nullptr : stats[player]; + if ( detailTag == "armor_on_cursed_sideeffect" ) + { + snprintf(buf, sizeof(buf), str.c_str(), getItemBeatitudeAdjective(item.beatitude).c_str()); + } + else if ( detailTag == "instrument_duration" ) + { + Sint32 CHR = statGetCHR(stats[player], players[player]->entity); + if ( compendiumTooltipIntro ) { CHR = 0; } + + int skillLVL = std::max(0, myStats ? myStats->getModifiedProficiency(PRO_APPRAISAL) : 0); + int durationMinimum = TICKS_PER_SECOND; + durationMinimum += (skillLVL / 20) * TICKS_PER_SECOND; + + int duration = 1; + duration += 4 * skillLVL * (TICKS_PER_SECOND / 25); + duration += (skillLVL / 20) * TICKS_PER_SECOND; + + int durationMax = TICKS_PER_SECOND * 60 * 5; + duration = std::min(duration, durationMax); + duration = std::max(duration, durationMinimum); + + int chargeTimeBase = 4 * TICKS_PER_SECOND; + int chargeTime = std::max(TICKS_PER_SECOND / 2, + chargeTimeBase - (TICKS_PER_SECOND / 20) * CHR); + + real_t chargeRatio = (100.0 * chargeTimeBase / (real_t)chargeTime) - 100.0; + + snprintf(buf, sizeof(buf), str.c_str(), + chargeTime / (real_t)TICKS_PER_SECOND, duration / (real_t)TICKS_PER_SECOND); + } + else if ( detailTag == "instrument_casting" ) + { + Sint32 CHR = statGetCHR(stats[player], players[player]->entity); + if ( compendiumTooltipIntro ) { CHR = 0; } + Uint8 effectStrength = std::max(1, std::min(255, CHR + 1)); + int tier = 1; + std::string tierString = "I"; + int nextCHR = Stat::kEnsembleBreakPointTier2; + if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier4 ) + { + nextCHR = 0; + tier = 4; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier3 ) + { + nextCHR = Stat::kEnsembleBreakPointTier4; + tier = 3; + + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier2 ) + { + nextCHR = Stat::kEnsembleBreakPointTier3; + tier = 2; + } + + char nextChrStr[64]; + if ( nextCHR > 0 ) + { + snprintf(nextChrStr, sizeof(nextChrStr), "%+d %s", nextCHR, ItemTooltips_t::getItemStatShortName("CHR").c_str()); + } + else + { + snprintf(nextChrStr, sizeof(nextChrStr), "N/A"); + } + + char nextChrStatStr[64]; + int nextCHRStat = effectStrength + 1; + int eff1 = 0; + if ( item.type == INSTRUMENT_FLUTE ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_EFF_1, effectStrength); + } + else if ( item.type == INSTRUMENT_LUTE ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_EFF_1, effectStrength); + } + else if ( item.type == INSTRUMENT_LYRE ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_EFF_1, effectStrength); + } + else if ( item.type == INSTRUMENT_HORN ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_EFF_1, effectStrength); + } + else if ( item.type == INSTRUMENT_DRUM ) + { + eff1 = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_EFF_1, effectStrength); + } + for ( int i = effectStrength + 1; i < effectStrength + 10 && i <= 255; ++i ) + { + int eff1_test = 0; + if ( item.type == INSTRUMENT_FLUTE ) + { + eff1_test = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_EFF_1, i); + } + else if ( item.type == INSTRUMENT_LUTE ) + { + eff1_test = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_EFF_1, i); + } + else if ( item.type == INSTRUMENT_LYRE ) + { + eff1_test = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_EFF_1, i); + } + else if ( item.type == INSTRUMENT_HORN ) + { + eff1_test = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_EFF_1, i); + } + else if ( item.type == INSTRUMENT_DRUM ) + { + eff1_test = stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_EFF_1, i); + } + if ( eff1_test > eff1 ) + { + nextCHRStat = i; + break; + } + } + + if ( nextCHRStat > 0 ) + { + snprintf(nextChrStatStr, sizeof(nextChrStatStr), "%+d %s", nextCHRStat, ItemTooltips_t::getItemStatShortName("CHR").c_str()); + } + else + { + snprintf(nextChrStatStr, sizeof(nextChrStatStr), "N/A"); + } + + snprintf(buf, sizeof(buf), str.c_str(), items[item.type].attributes["EFF_INSTRUMENT_MP_COST"], + nextChrStatStr, nextChrStr); + } + } + else if ( tooltipType == "tooltip_foci_light" + || tooltipType == "tooltip_foci_dark" + || tooltipType == "tooltip_foci_mage" ) + { + Entity* caster = compendiumTooltipIntro ? nullptr : players[player]->entity; + Stat* myStats = compendiumTooltipIntro ? nullptr : stats[player]; + if ( detailTag.compare("armor_on_cursed_sideeffect") == 0 ) + { + snprintf(buf, sizeof(buf), str.c_str(), getItemBeatitudeAdjective(item.beatitude).c_str()); + } + else if ( detailTag == "spell_damage_bonus" + || detailTag == "EFF_FOCI_MAGE_ARCS" ) + { + int spellID = getSpellIDFromFoci(item.type); + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + int baseDamage = getSpellDamageOrHealAmount(-1, spell, nullptr, compendiumTooltipIntro); + real_t bonusINTPercent = 100.0 * getBonusFromCasterOfSpellElement( + compendiumTooltipIntro ? nullptr : players[player]->entity, + compendiumTooltipIntro ? nullptr : stats[player], + nullptr, spell ? spell->ID : SPELL_NONE, spell->skillID); + real_t mult = 1.0; + if ( detailTag == "EFF_FOCI_MAGE_ARCS" ) + { + spellElement_t* element = nullptr; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + if ( element ) + { + baseDamage = element->getDamageSecondary(); + } + mult = getSpellPropertyFromID(spell_t::SPELLPROP_DAMAGE_SECONDARY_MULT, spell->ID, compendiumTooltipIntro ? nullptr : players[player]->entity, nullptr, nullptr); + snprintf(buf, sizeof(buf), str.c_str(), (int)(baseDamage * (1.0 + bonusINTPercent * mult / 100.0))); + } + else + { + mult = getSpellPropertyFromID(spell_t::SPELLPROP_DAMAGE_MULT, spell->ID, compendiumTooltipIntro ? nullptr : players[player]->entity, nullptr, nullptr); + std::string damageOrHealing = adjectives["spell_strings"]["damage"]; + /*std::string statName = getItemStatShortName("INT"); + if ( spell->skillID == PRO_MYSTICISM ) + { + statName += '/'; + statName += getItemStatShortName("CHR"); + } + else if ( spell->skillID == PRO_THAUMATURGY ) + { + statName += '/'; + statName += getItemStatShortName("CON"); + }*/ + + snprintf(buf, sizeof(buf), str.c_str(), damageOrHealing.c_str(), baseDamage, damageOrHealing.c_str(), + bonusINTPercent * mult, damageOrHealing.c_str()); + } + } + } + } + else if ( detailTag == "mage_foci_bless_bonus" ) + { + int bless = shouldInvertEquipmentBeatitude(myStats) ? + abs(item.beatitude) : + std::max((Sint16)0, item.beatitude); + + int bonus = bless * 5; + snprintf(buf, sizeof(buf), str.c_str(), bonus, getItemBeatitudeAdjective(item.beatitude).c_str()); + } + else if ( detailTag == "foci_mage_cast" ) + { + int baseChargeTime = TICKS_PER_SECOND; + int effectDuration = TICKS_PER_SECOND; + int spellID = getSpellIDFromFoci(item.type); + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + spellElement_t* element = nullptr; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + if ( element ) + { + baseChargeTime = element->getChanneledManaDuration(); + effectDuration = element->duration; + } + } + } + snprintf(buf, sizeof(buf), str.c_str(), baseChargeTime / (real_t)TICKS_PER_SECOND, effectDuration / (real_t)TICKS_PER_SECOND); + } + else if ( detailTag == "foci_charge_text" ) { - snprintf(buf, sizeof(buf), str.c_str(), (!compendiumTooltipIntro && stats[player]) ? (statGetDEX(stats[player], players[player]->entity) / 4) : 0); + int nextCHRBonus = 8; + + int tier = 1; + Sint32 CHR = statGetCHR(myStats, caster); + if ( CHR >= 3 && CHR <= 7 ) + { + nextCHRBonus = 8; + } + else if ( CHR >= 8 && CHR <= 14 ) + { + nextCHRBonus = 15; + } + else if ( CHR >= 15 && CHR <= 29 ) + { + nextCHRBonus = 30; + } + else if ( CHR >= 30 ) + { + nextCHRBonus = 0; + } + char nextChrStr[64]; + if ( nextCHRBonus > 0 ) + { + snprintf(nextChrStr, sizeof(nextChrStr), "%+d %s", nextCHRBonus, ItemTooltips_t::getItemStatShortName("CHR").c_str()); + } + else + { + snprintf(nextChrStr, sizeof(nextChrStr), "N/A"); + } + int effectDuration = TICKS_PER_SECOND; + int baseChargeTime = TICKS_PER_SECOND; + int spellID = getSpellIDFromFoci(item.type); + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + spellElement_t* element = nullptr; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + if ( element ) + { + effectDuration = element->duration; + baseChargeTime = element->getChanneledManaDuration(); + } + } + } + + snprintf(buf, sizeof(buf), str.c_str(), baseChargeTime / (real_t)TICKS_PER_SECOND, effectDuration / (real_t)TICKS_PER_SECOND, nextChrStr); } - else if ( detailTag.compare("thrown_skill_modifier") == 0 ) + else if ( detailTag == "foci_mp_cost" ) { - int skillLVL = compendiumTooltipIntro ? 0 : (stats[player]->getModifiedProficiency(proficiency) / 20); - snprintf(buf, sizeof(buf), str.c_str(), static_cast(100 * thrownDamageSkillMultipliers[std::min(skillLVL, 5)] - 100), - getItemProficiencyName(proficiency).c_str()); + int mpCost = 5; + int spellID = getSpellIDFromFoci(item.type); + int freeMPInterval1 = 0; + int freeMPInterval2 = 0; + int moveSpeed = 0; + std::string skillName = ""; + int chargeRefire = 0; + int nextCHRBonus = 8; + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + mpCost = getCostOfSpell(spell, caster); + int bless = shouldInvertEquipmentBeatitude(myStats) ? + abs(item.beatitude) : + std::max((Sint16)0, item.beatitude); + + mpCost = getSpellPropertyFromID(spell_t::SPELLPROP_FOCI_SECONDARY_MANA_COST, spellID, + caster, myStats, caster); + + int tier = bless > 0 ? std::min(2, bless) : 0; + Sint32 CHR = statGetCHR(myStats, caster); + if ( CHR > 0 ) + { + if ( CHR >= 3 && CHR <= 7 ) + { + tier += 1; + nextCHRBonus = 8; + } + else if ( CHR >= 8 && CHR <= 14 ) + { + tier += 2; + nextCHRBonus = 15; + } + else if ( CHR >= 15 && CHR <= 29 ) + { + tier += 3; + nextCHRBonus = 30; + } + else if ( CHR >= 30 && CHR <= 59 ) + { + tier += 4; + nextCHRBonus = 60; + } + else if ( CHR >= 60 ) + { + tier += 5; + nextCHRBonus = 0; + } + } + tier = std::min(tier, 5); + if ( tier == 1 ) { freeMPInterval1 = 1; freeMPInterval2 = 4; } + if ( tier == 2 ) { freeMPInterval1 = 1; freeMPInterval2 = 3; } + if ( tier == 3 ) { freeMPInterval1 = 1; freeMPInterval2 = 2; } + if ( tier == 4 ) { freeMPInterval1 = 2; freeMPInterval2 = 3; } + if ( tier == 5 ) { freeMPInterval1 = 3; freeMPInterval2 = 4; } + + moveSpeed = !myStats ? 0 : myStats->getModifiedProficiency(spell->skillID); + skillName = Player::SkillSheet_t::getSkillNameFromID(spell->skillID); + + real_t modifier = std::min(100, !myStats ? 0 : myStats->getModifiedProficiency(spell->skillID)) / 100.0; + + spellElement_t* element = nullptr; + if ( spell->elements.first ) + { + if ( element = (spellElement_t*)spell->elements.first->element ) + { + if ( element->elements.first && element->elements.first->element ) + { + element = (spellElement_t*)element->elements.first->element; + } + } + } + + chargeRefire = 0; + if ( element ) + { + int result = element->getChanneledManaDuration(); + int modifiedResult = getSpellPropertyFromID(spell_t::SPELLPROP_FOCI_REFIRE_TICKS, spellID, + caster, myStats, caster); + + chargeRefire = (100.0 - (100.0 * modifiedResult / (real_t)result)); + } + } + } + + std::string fmt1 = "-"; + std::string fmt2 = "-"; + if ( freeMPInterval1 > 0 && freeMPInterval2 > 0 ) + { + fmt1 = std::to_string(freeMPInterval1); + fmt2 = std::to_string(freeMPInterval2); + } + char nextChrStr[64]; + if ( nextCHRBonus > 0 ) + { + snprintf(nextChrStr, sizeof(nextChrStr), "%+d %s", nextCHRBonus, ItemTooltips_t::getItemStatShortName("CHR").c_str()); + } + else + { + snprintf(nextChrStr, sizeof(nextChrStr), "N/A"); + } + snprintf(buf, sizeof(buf), str.c_str(), + mpCost, fmt1.c_str(), fmt2.c_str(), + nextChrStr, + skillName.c_str(), + chargeRefire, moveSpeed); } else { @@ -4178,6 +5648,7 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType || tooltipType.find("tooltip_polearm") != std::string::npos || tooltipType.find("tooltip_whip") != std::string::npos || tooltipType.compare("tooltip_tool_pickaxe") == 0 + || tooltipType.compare("tooltip_magicstaff_scepter") == 0 || tooltipType.find("tooltip_ranged") != std::string::npos || tooltipType.find("tooltip_quiver") != std::string::npos ) { @@ -4271,15 +5742,28 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType { atk = 0; } - snprintf(buf, sizeof(buf), str.c_str(), atk); + snprintf(buf, sizeof(buf), str.c_str(), (int)(atk * 100 * Entity::PlayerAttackMeleeStatFactor)); + } + else if ( item.type == MAGICSTAFF_SCEPTER ) + { + int atk = (stats[player] ? statGetSTR(stats[player], players[player]->entity) : 0); + atk = std::min(atk / 2, atk); + snprintf(buf, sizeof(buf), str.c_str(), (int)(atk * 100 * Entity::PlayerAttackMeleeStatFactor)); + } + else if ( item.type == RAPIER ) + { + int atk = (stats[player] ? statGetDEX(stats[player], players[player]->entity) : 0); + snprintf(buf, sizeof(buf), str.c_str(), (int)(atk * 100 * Entity::PlayerAttackMeleeStatFactor)); } else if ( proficiency == PRO_RANGED ) { - snprintf(buf, sizeof(buf), str.c_str(), (!compendiumTooltipIntro && stats[player]) ? statGetDEX(stats[player], players[player]->entity) : 0); + snprintf(buf, sizeof(buf), str.c_str(), + (int)(((!compendiumTooltipIntro && stats[player]) ? statGetDEX(stats[player], players[player]->entity) : 0) * 100 * Entity::PlayerAttackRangedStatFactor)); } else { - snprintf(buf, sizeof(buf), str.c_str(), (!compendiumTooltipIntro && stats[player]) ? statGetSTR(stats[player], players[player]->entity) : 0); + snprintf(buf, sizeof(buf), str.c_str(), + (int)(((!compendiumTooltipIntro && stats[player]) ? statGetSTR(stats[player], players[player]->entity) : 0) * 100 * Entity::PlayerAttackMeleeStatFactor)); } } else if ( detailTag.compare("weapon_durability") == 0 ) @@ -4303,9 +5787,10 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType snprintf(buf, sizeof(buf), str.c_str(), items[item.type].hasAttribute("FRAGILE") ? -items[item.type].attributes["FRAGILE"] : 0); } - else if ( detailTag.compare("weapon_ranged_armor_pierce") == 0 ) + else if ( detailTag.compare("weapon_ranged_armor_pierce") == 0 + || detailTag.compare("whip_base_effects") == 0 ) { - int statChance = std::min(std::max((stats[player] ? statGetPER(stats[player], players[player]->entity) : 0) / 2, 0), 50); // 0 to 50 value. + int statChance = std::min(std::max((stats[player] ? statGetPER(stats[player], players[player]->entity) : 0), 0), 50); // 0 to 50 value. if ( compendiumTooltipIntro ) { statChance = 0; @@ -4327,6 +5812,49 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType snprintf(buf, sizeof(buf), str.c_str(), rof); } } + else if ( detailTag == "spell_damage_bonus" ) + { + if ( item.type == MAGICSTAFF_SCEPTER ) + { + if ( detailTag.compare("spell_damage_bonus") == 0 ) + { + spell_t* spell = getSpellFromID(SPELL_SCEPTER_BLAST); + if ( !spell ) { return; } + + int baseDamage = getSpellDamageOrHealAmount(-1, spell, nullptr, compendiumTooltipIntro); + + real_t mult = getSpellPropertyFromID(spell_t::SPELLPROP_DAMAGE_MULT, spell->ID, compendiumTooltipIntro ? nullptr : players[player]->entity, nullptr, nullptr); + + real_t bonusINTPercent = 100.0 * getBonusFromCasterOfSpellElement( + compendiumTooltipIntro ? nullptr : players[player]->entity, + compendiumTooltipIntro ? nullptr : stats[player], + nullptr, spell ? spell->ID : SPELL_NONE, spell->skillID); + + std::string damageOrHealing = adjectives["spell_strings"]["damage"]; + std::string statName = getItemStatShortName("INT"); + if ( spell->skillID == PRO_MYSTICISM ) + { + statName += '/'; + statName += getItemStatShortName("CHR"); + } + else if ( spell->skillID == PRO_THAUMATURGY ) + { + statName += '/'; + statName += getItemStatShortName("CON"); + } + + snprintf(buf, sizeof(buf), str.c_str(), damageOrHealing.c_str(), baseDamage, damageOrHealing.c_str(), + bonusINTPercent * mult, damageOrHealing.c_str()); + } + } + } + else if ( detailTag == "weapon_parry_ac" ) + { + Entity* my = compendiumTooltipIntro ? nullptr : players[player]->entity; + Stat* myStats = compendiumTooltipIntro ? nullptr : stats[player]; + int parryBonus = Stat::getParryingACBonus(myStats, &item, true, false, proficiency); + snprintf(buf, sizeof(buf), str.c_str(), parryBonus); + } else { return; @@ -4538,16 +6066,26 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType return; } } - else if ( tooltipType.find("tooltip_spellbook") != std::string::npos ) + else if ( tooltipType.find("tooltip_spellbook") != std::string::npos + || tooltipType.find("tooltip_tome") != std::string::npos ) { if ( detailTag.compare("spellbook_cast_bonus") == 0 ) { - spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + spell_t* spell = nullptr; + if ( itemCategory(&item) == SPELLBOOK ) + { + spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + } + else if ( itemCategory(&item) == TOME_SPELL ) + { + spell = getSpellFromID(item.getTomeSpellID()); + } if ( !spell ) { return; } - int intBonus = (statGetINT( - compendiumTooltipIntro ? nullptr : stats[player], - compendiumTooltipIntro ? nullptr : players[player]->entity) * 0.5); + int intBonus = getSpellbookBaseINTBonus( + compendiumTooltipIntro ? nullptr : players[player]->entity, + compendiumTooltipIntro ? nullptr : stats[player], + spell->skillID); real_t mult = ((items[item.type].attributes["SPELLBOOK_CAST_BONUS"]) / 100.0); intBonus *= mult; int beatitudeBonus = (mult * getSpellbookBonusPercent( @@ -4570,14 +6108,39 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType { damageOrHealing = adjectives["spell_strings"]["duration"]; } + if ( spellItems[spell->ID].spellTags.find(SpellTagTypes::SPELL_TAG_BONUS_AS_EFFECT_POWER) + != spellItems[spell->ID].spellTags.end() ) + { + damageOrHealing = adjectives["spell_strings"]["effect"]; + } + + std::string statName = getItemStatShortName("INT"); + if ( spell->skillID == PRO_MYSTICISM ) + { + statName += '/'; + statName += getItemStatShortName("CHR"); + } + else if ( spell->skillID == PRO_THAUMATURGY ) + { + statName += '/'; + statName += getItemStatShortName("CON"); + } snprintf(buf, sizeof(buf), str.c_str(), - intBonus, damageOrHealing.c_str(), getItemStatShortName("INT").c_str(), + intBonus, damageOrHealing.c_str(), statName.c_str(), beatitudeBonus, damageOrHealing.c_str(), getItemBeatitudeAdjective(item.beatitude).c_str()); } else if ( detailTag.compare("spellbook_cast_success") == 0 ) { - spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + spell_t* spell = nullptr; + if ( itemCategory(&item) == SPELLBOOK ) + { + spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + } + else if ( itemCategory(&item) == TOME_SPELL ) + { + spell = getSpellFromID(item.getTomeSpellID()); + } if ( !spell ) { return; } int spellcastingAbility = getSpellcastingAbilityFromUsingSpellbook(spell, players[player]->entity, stats[player]); @@ -4586,7 +6149,15 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType } else if ( detailTag.compare("spellbook_extramana_chance") == 0 ) { - spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + spell_t* spell = nullptr; + if ( itemCategory(&item) == SPELLBOOK ) + { + spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + } + else if ( itemCategory(&item) == TOME_SPELL ) + { + spell = getSpellFromID(item.getTomeSpellID()); + } if ( !spell ) { return; } int spellcastingAbility = getSpellcastingAbilityFromUsingSpellbook(spell, players[player]->entity, stats[player]); @@ -4595,10 +6166,18 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType } else if ( detailTag.compare("spellbook_magic_requirement") == 0 ) { - spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + spell_t* spell = nullptr; + if ( itemCategory(&item) == SPELLBOOK ) + { + spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + } + else if ( itemCategory(&item) == TOME_SPELL ) + { + spell = getSpellFromID(item.getTomeSpellID()); + } if ( !spell ) { return; } - int skillLVL = std::min(100, stats[player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[player], players[player]->entity)); + int skillLVL = std::min(100, stats[player]->getModifiedProficiency(spell->skillID) + statGetINT(stats[player], players[player]->entity)); if ( !playerLearnedSpellbook(player, &item) && (spell && spell->difficulty > skillLVL) ) { str.insert((size_t)0, 1, '^'); // red line character @@ -4615,18 +6194,33 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType } else if ( detailTag.compare("spellbook_magic_current") == 0 ) { - spell_t* spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + spell_t* spell = nullptr; + if ( itemCategory(&item) == SPELLBOOK ) + { + spell = getSpellFromID(getSpellIDFromSpellbook(item.type)); + } + else if ( itemCategory(&item) == TOME_SPELL ) + { + spell = getSpellFromID(item.getTomeSpellID()); + } if ( !spell ) { return; } - int skillLVL = std::min(100, stats[player]->getModifiedProficiency(PRO_MAGIC) + statGetINT(stats[player], players[player]->entity)); + int skillLVL = std::min(100, stats[player]->getModifiedProficiency(spell->skillID) + statGetINT(stats[player], players[player]->entity)); if ( !playerLearnedSpellbook(player, &item) && (spell && spell->difficulty > skillLVL) ) { str.insert((size_t)0, 1, '^'); // red line character } Sint32 INT = stats[player] ? statGetINT(stats[player], players[player]->entity) : 0; - Sint32 skill = stats[player] ? stats[player]->getModifiedProficiency(PRO_MAGIC) : 0; + Sint32 skill = stats[player] ? stats[player]->getModifiedProficiency(spell->skillID) : 0; Sint32 total = std::min(SKILL_LEVEL_LEGENDARY, INT + skill); - snprintf(buf, sizeof(buf), str.c_str(), INT + skill, getProficiencyLevelName(INT + skill).c_str()); + if ( str.find("%s") != std::string::npos ) + { + snprintf(buf, sizeof(buf), str.c_str(), Player::SkillSheet_t::getSkillNameFromID(spell->skillID, true).c_str(), INT + skill, getProficiencyLevelName(INT + skill).c_str()); + } + else + { + snprintf(buf, sizeof(buf), str.c_str(), INT + skill, getProficiencyLevelName(INT + skill).c_str()); + } } else { @@ -4635,23 +6229,74 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType } else if ( tooltipType.compare("tooltip_spell_item") == 0 ) { - if ( detailTag.compare("spell_damage_bonus") == 0 ) + if ( detailTag == "spell_cast_time" ) + { + spell_t* spell = getSpellFromItem(player, &item, false); + if ( !spell ) { return; } + + Entity* caster = compendiumTooltipIntro ? nullptr : players[player]->entity; + Stat* casterStats = compendiumTooltipIntro ? nullptr : stats[player]; + real_t baseCastTime = spell->cast_time * 20; + real_t modifiedCastTime = getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_SPELL_CAST_TIME, spell->ID, caster, casterStats, nullptr) * 20; + real_t diff = -(baseCastTime - modifiedCastTime); + if ( abs(diff) < 0.001 ) + { + diff = 0.0; + } + snprintf(buf, sizeof(buf), str.c_str(), baseCastTime / (real_t)TICKS_PER_SECOND, (diff) / (real_t)TICKS_PER_SECOND); + } + else if ( detailTag == "spell_distance" ) + { + spell_t* spell = getSpellFromItem(player, &item, false); + if ( !spell ) { return; } + + real_t dist = 0.0; + real_t diff = 0.0; + if ( spell->rangefinder != SpellRangefinderType::RANGEFINDER_NONE ) + { + Entity* caster = compendiumTooltipIntro ? nullptr : players[player]->entity; + Stat* casterStats = compendiumTooltipIntro ? nullptr : stats[player]; + dist = spell->distance; + real_t modifiedDistance = getSpellPropertyFromID(spell_t::SPELLPROP_MODIFIED_DISTANCE, spell->ID, caster, casterStats, nullptr); + diff = (modifiedDistance - dist); + if ( abs(diff) < 0.001 ) + { + diff = 0.0; + } + } + snprintf(buf, sizeof(buf), str.c_str(), dist / 16.0, (diff) / 16.0); + } + else if ( detailTag.compare("spell_damage_bonus") == 0 ) { spell_t* spell = getSpellFromItem(player, &item, false); if ( !spell ) { return; } //int totalDamage = getSpellDamageOrHealAmount(player, spell, nullptr); - Sint32 oldINT = stats[player]->INT; + /*Sint32 oldINT = stats[player]->INT; + Sint32 oldCHR = stats[player]->CHR; + Sint32 oldCON = stats[player]->CON; stats[player]->INT = 0; + stats[player]->CHR = 0; + stats[player]->CON = 0;*/ int baseDamage = getSpellDamageOrHealAmount(-1, spell, nullptr, compendiumTooltipIntro); - real_t bonusEquipPercent = 100.0 * getBonusFromCasterOfSpellElement(players[player]->entity, stats[player], nullptr, spell ? spell->ID : SPELL_NONE); + real_t mult = getSpellPropertyFromID(spell_t::SPELLPROP_DAMAGE_MULT, spell->ID, compendiumTooltipIntro ? nullptr : players[player]->entity, nullptr, nullptr); + + /*real_t bonusEquipPercent = 100.0 * getBonusFromCasterOfSpellElement( + compendiumTooltipIntro ? nullptr : players[player]->entity, + compendiumTooltipIntro ? nullptr : stats[player], + nullptr, spell ? spell->ID : SPELL_NONE, spell->skillID); stats[player]->INT = oldINT; + stats[player]->CHR = oldCHR; + stats[player]->CON = oldCON;*/ - real_t bonusINTPercent = 100.0 * getBonusFromCasterOfSpellElement(players[player]->entity, stats[player], nullptr, spell ? spell->ID : SPELL_NONE); - bonusINTPercent -= bonusEquipPercent; + real_t bonusINTPercent = 100.0 * getBonusFromCasterOfSpellElement( + compendiumTooltipIntro ? nullptr : players[player]->entity, + compendiumTooltipIntro ? nullptr : stats[player], + nullptr, spell ? spell->ID : SPELL_NONE, spell->skillID); + //bonusINTPercent -= bonusEquipPercent; std::string damageOrHealing = adjectives["spell_strings"]["damage"]; if ( spellItems[spell->ID].spellTags.find(SpellTagTypes::SPELL_TAG_HEALING) @@ -4659,19 +6304,84 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType { damageOrHealing = adjectives["spell_strings"]["healing"]; } - snprintf(buf, sizeof(buf), str.c_str(), damageOrHealing.c_str(), baseDamage, damageOrHealing.c_str(), - bonusINTPercent, damageOrHealing.c_str(), getItemStatShortName("INT").c_str(), bonusEquipPercent, damageOrHealing.c_str()); + if ( spellItems[spell->ID].spellTags.find(SpellTagTypes::SPELL_TAG_BONUS_AS_EFFECT_POWER) + != spellItems[spell->ID].spellTags.end() ) + { + damageOrHealing = adjectives["spell_strings"]["effect"]; + } + + std::string statName = getItemStatShortName("INT"); + if ( spell->skillID == PRO_MYSTICISM ) + { + statName += '/'; + statName += getItemStatShortName("CHR"); + } + else if ( spell->skillID == PRO_THAUMATURGY ) + { + statName += '/'; + statName += getItemStatShortName("CON"); + } + + if ( spell->ID == SPELL_HOLY_BEAM && templates["template_spell_damage_bonus_pwr_dual"].size() ) + { + int damage = getSpellDamageFromID(SPELL_HOLY_BEAM, nullptr, nullptr, nullptr, 0.0, false); + real_t damageMult = mult; + int healing = getSpellDamageSecondaryFromID(SPELL_HOLY_BEAM, nullptr, nullptr, nullptr, 0.0, false); + real_t healingMult = getSpellPropertyFromID(spell_t::SPELLPROP_DAMAGE_SECONDARY_MULT, spell->ID, compendiumTooltipIntro ? nullptr : players[player]->entity, nullptr, nullptr); + snprintf(buf, sizeof(buf), templates["template_spell_damage_bonus_pwr_dual"][0].c_str(), adjectives["spell_strings"]["effect"].c_str(), damage, adjectives["spell_strings"]["damage"].c_str(), + bonusINTPercent * damageMult, adjectives["spell_strings"]["damage"].c_str(), + healing, adjectives["spell_strings"]["healing"].c_str(), + bonusINTPercent * healingMult, adjectives["spell_strings"]["healing"].c_str()); + } + else + { + snprintf(buf, sizeof(buf), str.c_str(), damageOrHealing.c_str(), baseDamage, damageOrHealing.c_str(), + bonusINTPercent * mult, damageOrHealing.c_str());// , statName.c_str(), bonusEquipPercent* mult, damageOrHealing.c_str()); + } } else if ( detailTag.compare("spell_cast_success") == 0 ) { - int spellcastingAbility = std::min(std::max(0, stats[player]->getModifiedProficiency(PRO_SPELLCASTING) + spell_t* spell = getSpellFromItem(player, &item, false); + if ( !spell ) { return; } + + int spellcastingAbility = std::min(std::max(0, stats[player]->getModifiedProficiency(spell->skillID) + statGetINT(stats[player], players[player]->entity)), 100); - int chance = ((10 - (spellcastingAbility / 10)) * 20 / 3.0); // 33% after rolling to fizzle, 66% success + int chance = ((100 - (spellcastingAbility)) / 3.0); // 33% after rolling to fizzle, 66% success + if ( str.find("%s") != std::string::npos ) + { + snprintf(buf, sizeof(buf), str.c_str(), Player::SkillSheet_t::getSkillNameFromID(spell->skillID, true).c_str(), chance); + } + else + { + snprintf(buf, sizeof(buf), str.c_str(), chance); + } + } + else if ( detailTag.compare("spell_cast_success1") == 0 ) + { + spell_t* spell = getSpellFromItem(player, &item, false); + if ( !spell ) { return; } + + if ( str.find("%s") != std::string::npos ) + { + snprintf(buf, sizeof(buf), str.c_str(), Player::SkillSheet_t::getSkillNameFromID(spell->skillID, true).c_str()); + } + } + else if ( detailTag.compare("spell_cast_success2") == 0 ) + { + spell_t* spell = getSpellFromItem(player, &item, false); + if ( !spell ) { return; } + + int spellcastingAbility = std::min(std::max(0, stats[player]->getModifiedProficiency(spell->skillID) + + statGetINT(stats[player], players[player]->entity)), 100); + int chance = ((100 - (spellcastingAbility)) / 3.0); // 33% after rolling to fizzle, 66% success snprintf(buf, sizeof(buf), str.c_str(), chance); } else if ( detailTag.compare("spell_extramana_chance") == 0 ) { - int spellcastingAbility = std::min(std::max(0, stats[player]->getModifiedProficiency(PRO_SPELLCASTING) + spell_t* spell = getSpellFromItem(player, &item, false); + if ( !spell ) { return; } + + int spellcastingAbility = std::min(std::max(0, stats[player]->getModifiedProficiency(spell->skillID) + statGetINT(stats[player], players[player]->entity)), 100); int chance = (10 - (spellcastingAbility / 10)) * 10; snprintf(buf, sizeof(buf), str.c_str(), chance); @@ -4827,6 +6537,43 @@ void ItemTooltips_t::formatItemDetails(const int player, std::string tooltipType return; } } + else if ( detailTag == "duck_info" ) + { + std::string ownerName = adjectives["duck_default"]["unknown"]; + std::string duckName = adjectives["duck_default"]["unknown"]; + int owner = item.getDuckPlayer(); + if ( owner >= 0 && owner < MAXPLAYERS && !compendiumTooltipIntro ) + { + if ( !client_disconnected[owner] && stats[owner] ) + { + ownerName = stats[owner]->name; + } + } + if ( owner == player && !compendiumTooltipIntro ) + { + for ( auto& duck : players[player]->mechanics.ducksInARow ) + { + if ( duck.first == ((item.appearance % items[TOOL_DUCK].variations) / MAXPLAYERS) ) + { + if ( duck.second >= 15 * 60 * TICKS_PER_SECOND ) + { + if ( adjectives["duck_titles"].size() >= 1 ) + { + int index = uniqueGameKey % adjectives["duck_titles"].size(); + index += 8 * player; + if ( index >= adjectives["duck_titles"].size() ) + { + index = index % adjectives["duck_titles"].size(); + } + duckName = adjectives["duck_titles"][std::to_string(index)]; + } + } + break; + } + } + } + snprintf(buf, sizeof(buf), str.c_str(), ownerName.c_str(), duckName.c_str()); + } else { return; @@ -5288,7 +7035,7 @@ void StatueManager_t::readAllStatues() { std::string baseDir = "data/statues"; auto files = physfsGetFileNamesInDirectory(baseDir.c_str()); - for ( auto file : files ) + for ( auto& file : files ) { std::string checkFile = baseDir + '/' + file; PHYSFS_Stat stat; @@ -5297,7 +7044,7 @@ void StatueManager_t::readAllStatues() if ( stat.filetype == PHYSFS_FileType::PHYSFS_FILETYPE_DIRECTORY ) { auto files2 = physfsGetFileNamesInDirectory(checkFile.c_str()); - for ( auto file2 : files2 ) + for ( auto& file2 : files2 ) { std::string checkFile2 = checkFile + '/' + file2; if ( PHYSFS_stat(checkFile2.c_str(), &stat) == 0 ) { continue; } @@ -5765,7 +7512,7 @@ void ScriptTextParser_t::readAllScripts() std::string baseDir = "/data/scripts"; auto files = physfsGetFileNamesInDirectory(baseDir.c_str()); - for ( auto file : files ) + for ( auto& file : files ) { std::string checkFile = baseDir + '/' + file; PHYSFS_Stat stat; @@ -5774,7 +7521,7 @@ void ScriptTextParser_t::readAllScripts() if ( stat.filetype == PHYSFS_FileType::PHYSFS_FILETYPE_DIRECTORY ) { auto files2 = physfsGetFileNamesInDirectory(checkFile.c_str()); - for ( auto file2 : files2 ) + for ( auto& file2 : files2 ) { std::string checkFile2 = checkFile + '/' + file2; if ( PHYSFS_stat(checkFile2.c_str(), &stat) == 0 ) { continue; } @@ -6094,6 +7841,57 @@ bool ScriptTextParser_t::readFromFile(const std::string& filename) entry.objectType = OBJ_SCRIPT; entry.formattedText = entry_itr->value["script"].GetString(); } + else if ( entry_itr->value.HasMember("bubble_sign") ) + { + entry.objectType = OBJ_BUBBLE_SIGN; + for ( rapidjson::Value::ConstValueIterator text_itr = entry_itr->value["bubble_sign"].Begin(); text_itr != entry_itr->value["bubble_sign"].End(); ++text_itr ) + { + entry.rawText.push_back(text_itr->GetString()); + } + entry.formattedText = ""; + for ( auto& str : entry.rawText ) + { + if ( entry.formattedText != "" ) + { + entry.formattedText += '\n'; + } + entry.formattedText += str; + } + } + else if ( entry_itr->value.HasMember("bubble_grave") ) + { + entry.objectType = OBJ_BUBBLE_GRAVE; + for ( rapidjson::Value::ConstValueIterator text_itr = entry_itr->value["bubble_grave"].Begin(); text_itr != entry_itr->value["bubble_grave"].End(); ++text_itr ) + { + entry.rawText.push_back(text_itr->GetString()); + } + entry.formattedText = ""; + for ( auto& str : entry.rawText ) + { + if ( entry.formattedText != "" ) + { + entry.formattedText += '\n'; + } + entry.formattedText += str; + } + } + else if ( entry_itr->value.HasMember("bubble_dialogue") ) + { + entry.objectType = OBJ_BUBBLE_DIALOGUE; + for ( rapidjson::Value::ConstValueIterator text_itr = entry_itr->value["bubble_dialogue"].Begin(); text_itr != entry_itr->value["bubble_dialogue"].End(); ++text_itr ) + { + entry.rawText.push_back(text_itr->GetString()); + } + entry.formattedText = ""; + for ( auto& str : entry.rawText ) + { + if ( entry.formattedText != "" ) + { + entry.formattedText += '\n'; + } + entry.formattedText += str; + } + } else if ( entry_itr->value.HasMember("message") ) { entry.objectType = OBJ_MESSAGE; @@ -6101,6 +7899,15 @@ bool ScriptTextParser_t::readFromFile(const std::string& filename) { entry.rawText.push_back(text_itr->GetString()); } + entry.formattedText = ""; + for ( auto& str : entry.rawText ) + { + if ( entry.formattedText != "" ) + { + entry.formattedText += '\n'; + } + entry.formattedText += str; + } if ( entry_itr->value.HasMember("variables") ) { for ( rapidjson::Value::ConstValueIterator var_itr = entry_itr->value["variables"].Begin(); @@ -6111,23 +7918,30 @@ bool ScriptTextParser_t::readFromFile(const std::string& filename) if ( (*var_itr).HasMember("type") ) { std::string typeTxt = (*var_itr)["type"].GetString(); - if ( typeTxt == "text" ) + if ( typeTxt == "color_r" ) { - variable.type = TEXT; + variable.type = COLOR_R; } - else if ( typeTxt == "input_glyph" ) + else if ( typeTxt == "color_g" ) { - variable.type = GLYPH; + variable.type = COLOR_G; } - else if ( typeTxt == "image" ) + else if ( typeTxt == "color_b" ) { - variable.type = IMG; + variable.type = COLOR_B; + } + } + if ( (*var_itr).HasMember("value") ) + { + if ( (*var_itr)["value"].IsInt() ) + { + variable.numericValue = (*var_itr)["value"].GetInt(); + } + else if ( (*var_itr)["value"].IsString() ) + { + variable.value = (*var_itr)["value"].GetString(); } } - if ( (*var_itr).HasMember("value") ) - { - variable.value = (*var_itr)["value"].GetString(); - } entry.variables.push_back(variable); } } @@ -7927,7 +9741,7 @@ void ClassHotbarConfig_t::writeToFile(HotbarConfigType fileWriteType, HotbarConf CustomHelpers::addMemberToRoot(exportDocument, "classes", allClassesObject); int classIndex = -1; - for ( auto classname : playerClassInternalNames ) + for ( auto& classname : playerClassInternalNames ) { ++classIndex; rapidjson::Value classObj(rapidjson::kObjectType); @@ -7937,7 +9751,7 @@ void ClassHotbarConfig_t::writeToFile(HotbarConfigType fileWriteType, HotbarConf auto& hotbar_t = players[clientnum]->hotbar; std::vector layoutTypes = { "classic", "modern" }; - for ( auto layout : layoutTypes ) + for ( auto& layout : layoutTypes ) { if ( layout == "classic" ) { @@ -8040,7 +9854,7 @@ void ClassHotbarConfig_t::readFromFile(ClassHotbarConfig_t::HotbarConfigType fil return; } - char buf[80000]; + static char buf[140000]; int count = fp->read(buf, sizeof(buf[0]), sizeof(buf) - 1); buf[count] = '\0'; rapidjson::StringStream is(buf); @@ -8057,7 +9871,7 @@ void ClassHotbarConfig_t::readFromFile(ClassHotbarConfig_t::HotbarConfigType fil for ( auto classes = d["classes"].MemberBegin(); classes != d["classes"].MemberEnd(); ++classes ) { int classIndex = -1; - for ( auto s : playerClassInternalNames ) + for ( auto& s : playerClassInternalNames ) { ++classIndex; if ( s == classes->name.GetString() ) @@ -8088,6 +9902,10 @@ void ClassHotbarConfig_t::readFromFile(ClassHotbarConfig_t::HotbarConfigType fil for ( int i = 0; i < NUM_HOTBAR_SLOTS; ++i ) { std::string slotnum = std::to_string(i); + if ( !layout->value.HasMember(slotnum.c_str()) ) + { + continue; + } auto& slot = layout->value[slotnum.c_str()]; if ( slot.HasMember("items") ) { @@ -8317,7 +10135,7 @@ void LocalAchievements_t::readFromFile() return; } - char buf[65536]; + static char buf[65536 * 2]; int count = fp->read(buf, sizeof(buf[0]), sizeof(buf) - 1); buf[count] = '\0'; rapidjson::StringStream is(buf); @@ -9018,6 +10836,7 @@ EditorEntityData_t editorEntityData; std::map EditorEntityData_t::colliderData; std::map EditorEntityData_t::colliderDmgTypes; std::map> EditorEntityData_t::colliderRandomGenPool; +std::map EditorEntityData_t::colliderNameIndexes; void EditorEntityData_t::readFromFile() { const std::string filename = "data/entity_data.json"; @@ -9055,6 +10874,7 @@ void EditorEntityData_t::readFromFile() colliderData.clear(); colliderDmgTypes.clear(); colliderRandomGenPool.clear(); + colliderNameIndexes.clear(); auto& entityTypes = d["entities"]; if ( entityTypes.HasMember("collider_dmg_calcs") ) { @@ -9100,7 +10920,9 @@ void EditorEntityData_t::readFromFile() } else if ( s == "PRO_MAGIC" ) { - colliderDmg.proficiencyBonusDamage.insert(PRO_MAGIC); + colliderDmg.proficiencyBonusDamage.insert(PRO_SORCERY); + colliderDmg.proficiencyBonusDamage.insert(PRO_MYSTICISM); + colliderDmg.proficiencyBonusDamage.insert(PRO_THAUMATURGY); } else if ( s == "PRO_RANGED" ) { @@ -9135,7 +10957,9 @@ void EditorEntityData_t::readFromFile() } else if ( s == "PRO_MAGIC" ) { - colliderDmg.proficiencyResistDamage.insert(PRO_MAGIC); + colliderDmg.proficiencyResistDamage.insert(PRO_SORCERY); + colliderDmg.proficiencyResistDamage.insert(PRO_MYSTICISM); + colliderDmg.proficiencyResistDamage.insert(PRO_THAUMATURGY); } else if ( s == "PRO_RANGED" ) { @@ -9154,6 +10978,8 @@ void EditorEntityData_t::readFromFile() int index = std::stoi(indexStr); auto& collider = colliderData[index]; collider.name = itr->value["name"].GetString(); + assert(colliderNameIndexes.find(collider.name) == colliderNameIndexes.end()); + colliderNameIndexes[collider.name] = index; collider.gib = itr->value["gib_model"].GetInt(); collider.gib_hit.clear(); if ( itr->value.HasMember("gib_hit_model") ) @@ -9194,8 +11020,14 @@ void EditorEntityData_t::readFromFile() collider.entityLangEntry = itr->value["entity_lang_entry"].GetInt(); collider.hitMessageLangEntry = itr->value["hit_message"].GetInt(); collider.breakMessageLangEntry = itr->value["break_message"].GetInt(); + if ( itr->value.HasMember("jump_message") ) + { + collider.colliderJumpLangEntry = itr->value["jump_message"].GetInt(); + } collider.hpbarLookupName = itr->value["hp_bar_lookup_name"].GetString(); collider.hideMonsters.clear(); + collider.spellTriggers.clear(); + collider.pathableMonsters.clear(); if ( itr->value.HasMember("random_gen_pool") ) { if ( itr->value["random_gen_pool"].IsObject() ) @@ -9216,6 +11048,43 @@ void EditorEntityData_t::readFromFile() for ( auto itr2 = itr->value["events"].MemberBegin(); itr2 != itr->value["events"].MemberEnd(); ++itr2 ) { + if ( !strcmp(itr2->name.GetString(), "spell_trigger") ) + { + auto& data = collider.spellTriggers; + if ( itr2->value.IsArray() ) + { + for ( auto val = itr2->value.Begin(); val != itr2->value.End(); ++val ) + { + if ( val->IsInt() ) + { + data.push_back(val->GetInt()); + } + } + } + continue; + } + if ( !strcmp(itr2->name.GetString(), "pathable") ) + { + auto& data = collider.pathableMonsters; + if ( itr2->value.IsArray() ) + { + for ( auto val = itr2->value.Begin(); val != itr2->value.End(); ++val ) + { + if ( val->IsString() ) + { + for ( int i = 0; i < NUMMONSTERS; ++i ) + { + if ( !strcmp(val->GetString(), monstertypename[i]) ) + { + data.insert(i); + break; + } + } + } + } + } + continue; + } std::string mapname = itr2->name.GetString(); for ( auto itr3 = itr2->value.MemberBegin(); itr3 != itr2->value.MemberEnd(); ++itr3 ) @@ -9466,7 +11335,7 @@ bool Mods::verifyMapFiles(const char* folder, bool ignoreBaseFolder) fullpath += PHYSFS_getDirSeparator(); fullpath += "maps/"; } - for ( auto f : directoryContents(fullpath.c_str(), false, true) ) + for ( auto& f : directoryContents(fullpath.c_str(), false, true) ) { const std::string mapPath = "maps/" + f; auto path = PHYSFS_getRealDir(mapPath.c_str()); @@ -10278,18 +12147,32 @@ int Mods::createBlankModDirectory(std::string foldername) EquipmentModelOffsets_t EquipmentModelOffsets; -bool EquipmentModelOffsets_t::modelOffsetExists(int monster, int sprite) +int EquipmentModelOffsets_t::modelOffsetExists(int monster, int sprite, int monsterSprite) { - auto find = monsterModelsMap.find(monster); - if ( find != monsterModelsMap.end() ) + if ( monsterSprite >= NUMMONSTERS ) { - auto find2 = find->second.find(sprite); - if ( find2 != find->second.end() ) + auto find = monsterModelsMap.find(monsterSprite); + if ( find != monsterModelsMap.end() ) { - return true; + auto find2 = find->second.find(sprite); + if ( find2 != find->second.end() ) + { + return monsterSprite; + } } } - return false; + { + auto find = monsterModelsMap.find(monster); + if ( find != monsterModelsMap.end() ) + { + auto find2 = find->second.find(sprite); + if ( find2 != find->second.end() ) + { + return monster; + } + } + } + return 0; } EquipmentModelOffsets_t::ModelOffset_t& EquipmentModelOffsets_t::getModelOffset(int monster, int sprite) @@ -10297,49 +12180,49 @@ EquipmentModelOffsets_t::ModelOffset_t& EquipmentModelOffsets_t::getModelOffset( return monsterModelsMap[monster][sprite]; } -bool EquipmentModelOffsets_t::expandHelmToFitMask(int monster, int helmSprite, int maskSprite) +int EquipmentModelOffsets_t::expandHelmToFitMask(int monster, int helmSprite, int maskSprite, int monsterSprite) { - if ( modelOffsetExists(monster, maskSprite) ) + if ( int resultMonsterSprite = modelOffsetExists(monster, maskSprite, monsterSprite) ) { - auto& maskOffset = getModelOffset(monster, maskSprite); + auto& maskOffset = getModelOffset(resultMonsterSprite, maskSprite); if ( maskOffset.oversizedMask ) { - if ( modelOffsetExists(monster, helmSprite) ) + if ( modelOffsetExists(resultMonsterSprite, helmSprite, 0) ) { - auto& helmOffset = getModelOffset(monster, helmSprite); + auto& helmOffset = getModelOffset(resultMonsterSprite, helmSprite); if ( helmOffset.expandToFitMask ) { - return true; + return resultMonsterSprite; } } } } - return false; + return 0; } -bool EquipmentModelOffsets_t::maskHasAdjustmentForExpandedHelm(int monster, int helmSprite, int maskSprite) +int EquipmentModelOffsets_t::maskHasAdjustmentForExpandedHelm(int monster, int helmSprite, int maskSprite, int monsterSprite) { - if ( modelOffsetExists(monster, maskSprite) ) + if ( int resultMonsterSprite = modelOffsetExists(monster, maskSprite, monsterSprite) ) { - auto& maskOffset = getModelOffset(monster, maskSprite); + auto& maskOffset = getModelOffset(resultMonsterSprite, maskSprite); if ( maskOffset.adjustToExpandedHelm.find(helmSprite) != maskOffset.adjustToExpandedHelm.end() ) { - return true; + return resultMonsterSprite; } else if ( maskOffset.adjustToExpandedHelm.find(-1) != maskOffset.adjustToExpandedHelm.end() ) { - return true; + return resultMonsterSprite; } } - return false; + return 0; } EquipmentModelOffsets_t::ModelOffset_t::AdditionalOffset_t EquipmentModelOffsets_t::getExpandHelmOffset(int monster, int helmSprite, int maskSprite) { - if ( modelOffsetExists(monster, helmSprite) ) + if ( int resultMonsterSprite = modelOffsetExists(monster, helmSprite, 0) ) { - auto& helmOffset = getModelOffset(monster, helmSprite); + auto& helmOffset = getModelOffset(resultMonsterSprite, helmSprite); if ( helmOffset.adjustToOversizeMask.find(maskSprite) != helmOffset.adjustToOversizeMask.end() ) { return helmOffset.adjustToOversizeMask[maskSprite]; @@ -10355,9 +12238,9 @@ EquipmentModelOffsets_t::ModelOffset_t::AdditionalOffset_t EquipmentModelOffsets EquipmentModelOffsets_t::ModelOffset_t::AdditionalOffset_t EquipmentModelOffsets_t::getMaskOffsetForExpandHelm(int monster, int helmSprite, int maskSprite) { - if ( modelOffsetExists(monster, maskSprite) ) + if ( int resultMonsterSprite = modelOffsetExists(monster, maskSprite, 0) ) { - auto& maskOffset = getModelOffset(monster, maskSprite); + auto& maskOffset = getModelOffset(resultMonsterSprite, maskSprite); if ( maskOffset.adjustToExpandedHelm.find(helmSprite) != maskOffset.adjustToExpandedHelm.end() ) { return maskOffset.adjustToExpandedHelm[helmSprite]; @@ -10370,6 +12253,118 @@ EquipmentModelOffsets_t::ModelOffset_t::AdditionalOffset_t EquipmentModelOffsets return EquipmentModelOffsets_t::ModelOffset_t::AdditionalOffset_t(); } +void EquipmentModelOffsets_t::readBaseItemsFromFile() +{ + std::string filename = "models/creatures/"; + filename += "item_model_positions.json"; + + if ( !PHYSFS_getRealDir(filename.c_str()) ) + { + //printlog("[JSON]: Error: Could not locate json file %s", filename.c_str()); + return; + } + + std::string inputPath = PHYSFS_getRealDir(filename.c_str()); + inputPath.append(PHYSFS_getDirSeparator()); + inputPath.append(filename.c_str()); + + File* fp = FileIO::open(inputPath.c_str(), "rb"); + if ( !fp ) + { + printlog("[JSON]: Error: Could not locate json file %s", inputPath.c_str()); + return; + } + + static char buf[32000]; + int count = fp->read(buf, sizeof(buf[0]), sizeof(buf) - 1); + buf[count] = '\0'; + rapidjson::StringStream is(buf); + FileIO::close(fp); + + rapidjson::Document d; + d.ParseStream(is); + if ( !d.IsObject() ) + { + return; + } + if ( !d.HasMember("version") || !d.HasMember("items") ) + { + printlog("[JSON]: Error: No 'version' value in json file, or JSON syntax incorrect! %s", inputPath.c_str()); + return; + } + + int version = d["version"].GetInt(); + + miscItemsBaseOffsets.clear(); + + auto& itemsArr = d["items"]; + for ( auto it = itemsArr.Begin(); it != itemsArr.End(); ++it ) + { + for ( auto it2 = it->MemberBegin(); it2 != it->MemberEnd(); ++it2 ) + { + std::string itemName = it2->name.GetString(); + if ( ItemTooltips.itemNameStringToItemID.find(itemName) == ItemTooltips.itemNameStringToItemID.end() ) + { + continue; + } + ItemType itemType = (ItemType)ItemTooltips.itemNameStringToItemID[itemName]; + std::vector models; + if ( it2->value.HasMember("models") ) + { + if ( it2->value["models"].IsArray() ) + { + if ( it2->value["models"].Size() == 0 ) + { + for ( int i = items[itemType].index; i < items[itemType].index + items[itemType].variations; ++i ) + { + models.push_back(i); + } + } + else + { + for ( auto itArr = it2->value["models"].Begin(); itArr != it2->value["models"].End(); ++itArr ) + { + if ( itArr->IsInt() ) + { + models.push_back(itArr->GetInt()); + } + } + } + } + } + + real_t focalx = it2->value.HasMember("focalx") ? it2->value["focalx"].GetDouble() : 0.0; + real_t focaly = it2->value.HasMember("focaly") ? it2->value["focaly"].GetDouble() : 0.0; + real_t focalz = it2->value.HasMember("focalz") ? it2->value["focalz"].GetDouble() : 0.0; + real_t scalex = 0.0; + if ( it2->value.HasMember("scalex") ) + { + scalex = it2->value["scalex"].GetDouble(); + } + real_t scaley = 0.0; + if ( it2->value.HasMember("scaley") ) + { + scaley = it2->value["scaley"].GetDouble(); + } + real_t scalez = 0.0; + if ( it2->value.HasMember("scalez") ) + { + scalez = it2->value["scalez"].GetDouble(); + } + for ( auto index : models ) + { + auto& entry = miscItemsBaseOffsets[index]; + entry.focalx = focalx; + entry.focaly = focaly; + entry.focalz = focalz; + entry.scalex = scalex; + entry.scaley = scaley; + entry.scalez = scalez; + } + } + } +} + void EquipmentModelOffsets_t::readFromFile(std::string monsterName, int monsterType) { if ( monsterType == NOTHING ) @@ -10428,11 +12423,15 @@ void EquipmentModelOffsets_t::readFromFile(std::string monsterName, int monsterT return; } + int version = d["version"].GetInt(); monsterModelsMap[monsterType].clear(); real_t baseFocalX = 0.0; real_t baseFocalY = 0.0; real_t baseFocalZ = 0.0; + real_t baseFocalX_rot1 = 0.0; + real_t baseFocalY_rot1 = 0.0; + real_t baseFocalZ_rot1 = 0.0; if ( d.HasMember("base_offsets") ) { if ( d["base_offsets"].HasMember("focalx") ) @@ -10447,6 +12446,18 @@ void EquipmentModelOffsets_t::readFromFile(std::string monsterName, int monsterT { baseFocalZ = d["base_offsets"]["focalz"].GetDouble(); } + if ( d["base_offsets"].HasMember("focalx_rot1") ) + { + baseFocalX_rot1 = d["base_offsets"]["focalx_rot1"].GetDouble(); + } + if ( d["base_offsets"].HasMember("focaly_rot1") ) + { + baseFocalY_rot1 = d["base_offsets"]["focaly_rot1"].GetDouble(); + } + if ( d["base_offsets"].HasMember("focalz_rot1") ) + { + baseFocalZ_rot1 = d["base_offsets"]["focalz_rot1"].GetDouble(); + } } auto& itemsArr = d["items"]; @@ -10515,13 +12526,28 @@ void EquipmentModelOffsets_t::readFromFile(std::string monsterName, int monsterT for ( auto index : models ) { auto& entry = monsterModelsMap[monsterType][index]; - entry.focalx = focalx + baseFocalX; - entry.focaly = focaly + baseFocalY; - entry.focalz = focalz + baseFocalZ; + entry.rotation = rotation * (PI / 2); + entry.focalx = focalx; + entry.focaly = focaly; + entry.focalz = focalz; + if ( items[itemType].item_slot == EQUIPPABLE_IN_SLOT_BREASTPLATE ) + { + } + else if ( static_cast(entry.rotation) == 1 && version >= 2 ) + { + entry.focalx += baseFocalX_rot1; + entry.focaly += baseFocalY_rot1; + entry.focalz += baseFocalZ_rot1; + } + else + { + entry.focalx += baseFocalX; + entry.focaly += baseFocalY; + entry.focalz += baseFocalZ; + } entry.scalex = scalex; entry.scaley = scaley; entry.scalez = scalez; - entry.rotation = rotation * (PI / 2); entry.pitch = pitch * (PI / 2); entry.limbsIndex = limbsIndex; entry.expandToFitMask = expandToFitMask; @@ -10624,6 +12650,66 @@ void EquipmentModelOffsets_t::readFromFile(std::string monsterName, int monsterT } } + if ( d.HasMember("base_offsets") && d["base_offsets"].HasMember("sprite_adjust") && d["base_offsets"]["sprite_adjust"].IsArray() ) + { + for ( auto itr = d["base_offsets"]["sprite_adjust"].Begin(); itr != d["base_offsets"]["sprite_adjust"].End(); ++itr ) + { + if ( (*itr).HasMember("sprite") ) + { + int customSprite = (*itr)["sprite"].GetInt(); + monsterModelsMap[customSprite] = monsterModelsMap[monsterType]; + + real_t baseFocalX = 0.0; + real_t baseFocalY = 0.0; + real_t baseFocalZ = 0.0; + real_t baseFocalX_rot1 = 0.0; + real_t baseFocalY_rot1 = 0.0; + real_t baseFocalZ_rot1 = 0.0; + if ( (*itr).HasMember("focalx") ) + { + baseFocalX = (*itr)["focalx"].GetDouble(); + } + if ( (*itr).HasMember("focaly") ) + { + baseFocalY = (*itr)["focaly"].GetDouble(); + } + if ( (*itr).HasMember("focalz") ) + { + baseFocalZ = (*itr)["focalz"].GetDouble(); + } + if ( (*itr).HasMember("focalx_rot1") ) + { + baseFocalX_rot1 = (*itr)["focalx_rot1"].GetDouble(); + } + if ( (*itr).HasMember("focaly_rot1") ) + { + baseFocalY_rot1 = (*itr)["focaly_rot1"].GetDouble(); + } + if ( (*itr).HasMember("focalz_rot1") ) + { + baseFocalZ_rot1 = (*itr)["focalz_rot1"].GetDouble(); + } + + for ( auto& model : monsterModelsMap[customSprite] ) + { + auto& entry = model.second; + if ( static_cast(entry.rotation) == 1 && version >= 2 ) + { + entry.focalx += baseFocalX_rot1; + entry.focaly += baseFocalY_rot1; + entry.focalz += baseFocalZ_rot1; + } + else + { + entry.focalx += baseFocalX; + entry.focaly += baseFocalY; + entry.focalz += baseFocalZ; + } + } + } + } + } + printlog("[JSON]: Successfully read json file %s", inputPath.c_str()); } @@ -11769,11 +13855,11 @@ void Compendium_t::readMagicFromFile(bool forceLoadBaseDirectory) } if ( item.name.find("spell_") != std::string::npos ) { - for ( auto spell : allGameSpells ) + for ( auto& spell : allGameSpells ) { - if ( item.name == spell->spell_internal_name ) + if ( item.name == spell.second->spell_internal_name ) { - item.spellID = spell->ID; + item.spellID = spell.second->ID; objSpellsLookup.insert(item.name); break; } @@ -11781,14 +13867,14 @@ void Compendium_t::readMagicFromFile(bool forceLoadBaseDirectory) } else if ( item.name.find("spellbook_") != std::string::npos ) { - for ( auto spell : allGameSpells ) + for ( auto& spell : allGameSpells ) { - int book = getSpellbookFromSpellID(spell->ID); + int book = getSpellbookFromSpellID(spell.second->ID); if ( book >= WOODEN_SHIELD && book < NUMITEMS && ::items[book].category == SPELLBOOK ) { if ( item.name == itemNameStrings[book + 2] ) { - item.spellID = spell->ID; + item.spellID = spell.second->ID; break; } } @@ -13037,7 +15123,7 @@ void Compendium_t::readMonstersFromFile(bool forceLoadBaseDirectory) Compendium_t::Events_t::monsterIDToString[type] = monstertypename[i]; } - for ( auto pair : Compendium_t::Events_t::monsterUniqueIDLookup ) + for ( auto& pair : Compendium_t::Events_t::monsterUniqueIDLookup ) { int type = pair.second + Compendium_t::Events_t::kEventMonsterOffset; Compendium_t::Events_t::monsterIDToString[type] = pair.first; @@ -13530,7 +15616,7 @@ std::string Compendium_t::Events_t::formatEventRecordText(Sint32 value, const ch } else if ( !strcmp(formatType, "skills") ) { - std::string tmp = getSkillLangEntry(formatVal); + std::string tmp = Player::SkillSheet_t::getSkillNameFromID(formatVal, true); camelCaseString(tmp); output += tmp; } @@ -13793,17 +15879,32 @@ std::vector> Compendium_t::Events_t::getCustomEve int startOffsetId = -1; if ( def.attributes.find("skills") != def.attributes.end() ) { - for ( int i = 0; i < NUMPROFICIENCIES; ++i ) + if ( cat == "magic skill" ) { - if ( cat == getSkillStringForCompendium(i) ) + startOffsetId = findClassTag->second[0] + PRO_SORCERY * kEventClassesMax; + } + else if ( cat == "casting skill" ) + { + startOffsetId = findClassTag->second[0] + PRO_MYSTICISM * kEventClassesMax; + } + else if ( cat == "swimming skill" ) + { + startOffsetId = findClassTag->second[0] + PRO_THAUMATURGY * kEventClassesMax; + } + else + { + for ( int i = 0; i < NUMPROFICIENCIES; ++i ) { - startOffsetId = findClassTag->second[0] + i * kEventClassesMax; - break; + if ( cat == getSkillStringForCompendium(i) ) + { + startOffsetId = findClassTag->second[0] + i * kEventClassesMax; + break; + } } } } - for ( auto classId : findClassTag->second ) + for ( auto& classId : findClassTag->second ) { if ( startOffsetId >= 0 ) { @@ -13852,7 +15953,7 @@ std::vector> Compendium_t::Events_t::getCustomEve if ( findClassTag != eventClassIds.end() ) { // iterate through classes - for ( auto classId : findClassTag->second ) + for ( auto& classId : findClassTag->second ) { codexIDs.push_back(classId); } @@ -13866,12 +15967,27 @@ std::vector> Compendium_t::Events_t::getCustomEve if ( formatType == "skills" ) { - for ( int i = 0; i < NUMPROFICIENCIES; ++i ) + if ( cat == "magic skill" ) + { + classnum = PRO_SORCERY; + } + else if ( cat == "casting skill" ) + { + classnum = PRO_MYSTICISM; + } + else if ( cat == "swimming skill" ) + { + classnum = PRO_THAUMATURGY; + } + else { - if ( cat == getSkillStringForCompendium(i) ) + for ( int i = 0; i < NUMPROFICIENCIES; ++i ) { - classnum = i; - break; + if ( cat == getSkillStringForCompendium(i) ) + { + classnum = i; + break; + } } } } @@ -13906,12 +16022,27 @@ std::vector> Compendium_t::Events_t::getCustomEve int categoryValue = foundId; if ( formatType == "skills" ) { - for ( int i = 0; i < NUMPROFICIENCIES; ++i ) + if ( cat == "magic skill" ) { - if ( cat == getSkillStringForCompendium(i) ) + categoryValue = PRO_SORCERY; + } + else if ( cat == "casting skill" ) + { + categoryValue = PRO_MYSTICISM; + } + else if ( cat == "swimming skill" ) + { + categoryValue = PRO_THAUMATURGY; + } + else + { + for ( int i = 0; i < NUMPROFICIENCIES; ++i ) { - categoryValue = i; - break; + if ( cat == getSkillStringForCompendium(i) ) + { + categoryValue = i; + break; + } } } } @@ -14263,7 +16394,7 @@ void Compendium_t::Events_t::readEventsFromFile() { for ( int skillnum = 0; skillnum < 16; ++skillnum ) { - for ( int i = 0; i <= CLASS_HUNTER; ++i ) + for ( int i = 0; i <= CLASS_PALADIN; ++i ) { int index = i + skillnum * kEventClassesMax; eventClassIds[id][index] = (classIdIndex + index); @@ -14273,7 +16404,7 @@ void Compendium_t::Events_t::readEventsFromFile() } else { - for ( int i = 0; i <= CLASS_HUNTER; ++i ) + for ( int i = 0; i <= CLASS_PALADIN; ++i ) { eventClassIds[id][i] = (classIdIndex + i); } @@ -14334,6 +16465,12 @@ void Compendium_t::Events_t::loadItemsSaveData() return; } + int version = 0; + if ( d.HasMember("version") ) + { + version = d["version"].GetInt(); + } + playerEvents.clear(); for ( auto itr = d["items"].MemberBegin(); itr != d["items"].MemberEnd(); ++itr ) { @@ -14376,6 +16513,35 @@ void Compendium_t::Events_t::loadItemsSaveData() } } } + + CompendiumEntries.migrateOldSkillIndexes = false; + if ( version == 1 ) + { + CompendiumEntries.migrateOldSkillIndexes = true; + } + if ( CompendiumEntries.migrateOldSkillIndexes ) + { + int oldClass = client_classes[0]; + std::vector skillIndexes = { PRO_MYSTICISM, PRO_SORCERY, PRO_THAUMATURGY }; + for ( int i = 0; i < NUMCLASSES; ++i ) + { + client_classes[0] = i; + for ( auto skillID : skillIndexes ) + { + const char* skillstr = Compendium_t::getSkillStringForCompendium(skillID); + if ( strcmp(skillstr, "") ) + { + Compendium_t::Events_t::eventUpdateCodex(0, Compendium_t::CPDM_CLASS_SKILL_LEGENDS, skillstr, 0, true); + Compendium_t::Events_t::eventUpdateCodex(0, Compendium_t::CPDM_CLASS_SKILL_NOVICES, skillstr, 0, true); + Compendium_t::Events_t::eventUpdateCodex(0, Compendium_t::CPDM_CLASS_SKILL_UPS, skillstr, 0, true); + Compendium_t::Events_t::eventUpdateCodex(0, Compendium_t::CPDM_CLASS_SKILL_UPS_RUN_MAX, skillstr, 0, true); + Compendium_t::Events_t::eventUpdateCodex(0, Compendium_t::CPDM_CLASS_SKILL_MAX, skillstr, 0, true); + } + } + } + client_classes[0] = oldClass; + } + CompendiumEntries.migrateOldSkillIndexes = false; } static ConsoleVariable cvar_compendiumClientSave("/compendium_client_save", false); @@ -14415,7 +16581,7 @@ void Compendium_t::Events_t::createDummyClientData(const int playernum) } for ( auto& pair : eventWorldLookup ) { - for ( auto world : pair.second ) + for ( auto& world : pair.second ) { eventUpdateWorld(playernum, pair.first, world.c_str(), 1); } @@ -14428,7 +16594,7 @@ void Compendium_t::Events_t::createDummyClientData(const int playernum) for ( int c = 0; c < NUMCLASSES; ++c ) { client_classes[playernum] = c; - for ( auto world : pair.second ) + for ( auto& world : pair.second ) { eventUpdateCodex(playernum, pair.first, world.c_str(), 1); } @@ -14437,7 +16603,7 @@ void Compendium_t::Events_t::createDummyClientData(const int playernum) } else { - for ( auto world : pair.second ) + for ( auto& world : pair.second ) { eventUpdateCodex(playernum, pair.first, world.c_str(), 1); } @@ -14757,7 +16923,7 @@ void Compendium_t::Events_t::writeItemsSaveData() rapidjson::Document exportDocument; exportDocument.SetObject(); - const int VERSION = 1; + const int VERSION = 2; CustomHelpers::addMemberToRoot(exportDocument, "version", rapidjson::Value(VERSION)); rapidjson::Value itemsObj(rapidjson::kObjectType); @@ -14893,7 +17059,6 @@ void Compendium_t::Events_t::updateEventsInMainLoop(const int playernum) auto myStats = stats[playernum]; { real_t resistance = 100.0 * Entity::getDamageTableMultiplier(entity, *myStats, DAMAGE_TABLE_MAGIC); - resistance /= (Entity::getMagicResistance(myStats) + 1); resistance = -(resistance - 100.0); eventUpdateCodex(playernum, CPDM_RES_MAX, "res", (int)resistance); eventUpdateCodex(playernum, CPDM_CLASS_RES_MAX, "res", (int)resistance); @@ -14982,20 +17147,32 @@ void Compendium_t::Events_t::updateEventsInMainLoop(const int playernum) } { + int skillID = NUMPROFICIENCIES; + if ( auto spell = players[playernum]->magic.selectedSpell() ) + { + skillID = spell->skillID; + } + { // base PWR INT Bonus - real_t bonus = getSpellBonusFromCasterINT(entity, myStats) * 100.0; + real_t bonus = getSpellBonusFromCasterINT(entity, myStats, skillID) * 100.0; real_t val = bonus; eventUpdateCodex(playernum, CPDM_CLASS_PWR_MAX, "pwr", (int)val); } // equip/effect bonus (minus INT) { - real_t val = (getBonusFromCasterOfSpellElement(entity, myStats, nullptr, SPELL_NONE) * 100.0); + real_t val = (getBonusFromCasterOfSpellElement(entity, myStats, nullptr, SPELL_NONE, NUMPROFICIENCIES) * 100.0); // look for damage/healing spell bonus for mitre/magus hat - val = std::max(val, getBonusFromCasterOfSpellElement(entity, myStats, nullptr, SPELL_FIREBALL) * 100.0); - val = std::max(val, getBonusFromCasterOfSpellElement(entity, myStats, nullptr, SPELL_HEALING) * 100.0); - real_t bonus = getSpellBonusFromCasterINT(entity, myStats); + if ( auto spell = getSpellFromID(SPELL_FIREBALL) ) + { + val = std::max(val, getBonusFromCasterOfSpellElement(entity, myStats, nullptr, SPELL_FIREBALL, spell->skillID) * 100.0); + } + if ( auto spell = getSpellFromID(SPELL_HEALING) ) + { + val = std::max(val, getBonusFromCasterOfSpellElement(entity, myStats, nullptr, SPELL_HEALING, spell->skillID) * 100.0); + } + real_t bonus = getSpellBonusFromCasterINT(entity, myStats, skillID); val -= bonus * 100.0; eventUpdateCodex(playernum, CPDM_PWR_MAX_EQUIP, "pwr", (int)val); } @@ -16530,7 +18707,7 @@ void Compendium_t::Events_t::sendClientDataOverNet(const int playernum) void Compendium_t::readModelLimbsFromFile(std::string section) { std::string fullpath = "data/compendium/" + section + "_models/"; - for ( auto f : directoryContents(fullpath.c_str(), false, true) ) + for ( auto& f : directoryContents(fullpath.c_str(), false, true) ) { std::string inputPath = fullpath + f; std::string path = PHYSFS_getRealDir(inputPath.c_str()) ? PHYSFS_getRealDir(inputPath.c_str()) : ""; @@ -16544,7 +18721,7 @@ void Compendium_t::readModelLimbsFromFile(std::string section) printlog("[JSON]: Error: Could not locate json file %s", inputPath.c_str()); return; } - char buf[65536]; + static char buf[65536]; int count = fp->read(buf, sizeof(buf[0]), sizeof(buf) - 1); buf[count] = '\0'; rapidjson::StringStream is(buf); diff --git a/src/mod_tools.hpp b/src/mod_tools.hpp index bd95e51b9..9f317b6ed 100644 --- a/src/mod_tools.hpp +++ b/src/mod_tools.hpp @@ -2761,7 +2761,13 @@ class ItemTooltips_t SPELL_TYPE_PROJECTILE_SHORT_X3, SPELL_TYPE_SELF, SPELL_TYPE_AREA, - SPELL_TYPE_SELF_SUSTAIN + SPELL_TYPE_SELF_SUSTAIN, + SPELL_TYPE_TOUCH_FLOOR, + SPELL_TYPE_TOUCH_ALLY, + SPELL_TYPE_TOUCH_ENEMY, + SPELL_TYPE_TOUCH_ENTITY, + SPELL_TYPE_TOUCH_WALL, + SPELL_TYPE_DIVINE_TARGET }; enum SpellTagTypes : int { @@ -2771,7 +2777,10 @@ class ItemTooltips_t SPELL_TAG_HEALING, SPELL_TAG_CURE, SPELL_TAG_BASIC_HIT_MESSAGE, - SPELL_TAG_TRACK_HITS + SPELL_TAG_TRACK_HITS, + SPELL_TAG_BUFF, + SPELL_TAG_SPELLBOOK_SCALING, + SPELL_TAG_BONUS_AS_EFFECT_POWER }; private: struct spellItem_t @@ -2779,16 +2788,57 @@ class ItemTooltips_t Sint32 id; std::string internalName; std::string name; + std::string name_lowercase; std::string spellTypeStr; SpellItemTypes spellType; std::string spellbookInternalName; std::string magicstaffInternalName; - Sint32 spellbookId; - Sint32 magicstaffId; + std::string fociInternalName; + Sint32 spellbookId = -1; + Sint32 magicstaffId = -1; + Sint32 fociId = -1; std::vector spellTagsStr; std::set spellTags; + std::vector spellFormatTags; + std::vector spellbookItemIconPaddingLines; + std::set spellLevelTags; + + bool hasExpandedJSON = false; + int damage = 0; + int damage2 = 0; + real_t damage_mult = 1.0; + real_t damage2_mult = 1.0; + int duration = 0; + real_t duration_mult = 1.0; + int duration2 = 0; + real_t duration2_mult = 1.0; + int mana = 1; + real_t distance = 0.0; + real_t distance_mult = 1.0; + int life_time = 0; + real_t life_mult = 1.0; + real_t cast_time = 1.0; + real_t cast_time_mult = 1.0; + int skillID = PRO_SORCERY; + int difficulty = 100; + int sustain_mana = 0; + int sustain_duration = 0; + real_t sustain_mult = 1.0; + real_t radius = 0; + real_t radius_mult = 0.0; + int drop_table = -1; }; + Uint32 defaultHeadingTextColor = 0xFFFFFFFF; + Uint32 defaultIconTextColor = 0xFFFFFFFF; + Uint32 defaultDescriptionTextColor = 0xFFFFFFFF; + Uint32 defaultDetailsTextColor = 0xFFFFFFFF; + Uint32 defaultPositiveTextColor = 0xFFFFFFFF; + Uint32 defaultNegativeTextColor = 0xFFFFFFFF; + Uint32 defaultStatusEffectTextColor = 0xFFFFFFFF; + Uint32 defaultFaintTextColor = 0xFFFFFFFF; + +public: struct ItemTooltipIcons_t { std::string iconPath = ""; @@ -2804,16 +2854,6 @@ class ItemTooltips_t void setConditionalAttribute(std::string str) { conditionalAttribute = str; } }; - Uint32 defaultHeadingTextColor = 0xFFFFFFFF; - Uint32 defaultIconTextColor = 0xFFFFFFFF; - Uint32 defaultDescriptionTextColor = 0xFFFFFFFF; - Uint32 defaultDetailsTextColor = 0xFFFFFFFF; - Uint32 defaultPositiveTextColor = 0xFFFFFFFF; - Uint32 defaultNegativeTextColor = 0xFFFFFFFF; - Uint32 defaultStatusEffectTextColor = 0xFFFFFFFF; - Uint32 defaultFaintTextColor = 0xFFFFFFFF; - -public: struct ItemTooltip_t { Uint32 headingTextColor = 0; @@ -2838,6 +2878,8 @@ class ItemTooltips_t void setColorStatus(Uint32 color) { statusEffectTextColor = color; } void setColorFaintText(Uint32 color) { faintTextColor = color; } }; + void setSpellValueIfKeyPresent(spellItem_t& t, rapidjson::Value::ConstMemberIterator item_itr, Uint32& hash, Uint32& hashShift, const char* key, int& toSet); + void setSpellValueIfKeyPresent(spellItem_t& t, rapidjson::Value::ConstMemberIterator item_itr, Uint32& hash, Uint32& hashShift, const char* key, real_t& toSet); void readItemsFromFile(); static const Uint32 kItemsJsonHash; static Uint32 itemsJsonHashRead; @@ -2878,6 +2920,7 @@ class ItemTooltips_t std::string& getProficiencyLevelName(Sint32 proficiencyLevel); std::string& getIconLabel(Item& item); std::string getSpellIconText(const int player, Item& item, const bool excludePlayerStats); + std::string getSpellIconFormatText(const int player, Item& item, std::string& format, const spell_t* spell, const int iconIndex, const bool compendiumTooltipIntro); std::string getSpellDescriptionText(const int player, Item& item); std::string getSpellIconPath(const int player, Item& item, int spellID); std::string getCostOfSpellString(const int player, Item& item); @@ -3024,13 +3067,19 @@ class ScriptTextParser_t enum ObjectType_t : int { OBJ_SIGN, OBJ_MESSAGE, - OBJ_SCRIPT + OBJ_SCRIPT, + OBJ_BUBBLE_SIGN, + OBJ_BUBBLE_GRAVE, + OBJ_BUBBLE_DIALOGUE }; enum VariableTypes : int { TEXT, GLYPH, IMG, - SCRIPT + SCRIPT, + COLOR_R, + COLOR_G, + COLOR_B }; struct Entry_t @@ -3041,6 +3090,7 @@ class ScriptTextParser_t { VariableTypes type = TEXT; std::string value = ""; + int numericValue = 0; int sizex = 0; int sizey = 0; }; @@ -3343,6 +3393,9 @@ struct EditorEntityData_t int hitMessageLangEntry = 2509; int breakMessageLangEntry = 2510; std::map> hideMonsters; + std::vector spellTriggers; + std::set pathableMonsters; + int colliderJumpLangEntry = 6234; std::map overrideProperties; bool hasOverride(std::string key) { @@ -3384,6 +3437,16 @@ struct EditorEntityData_t static std::map colliderDmgTypes; static std::map colliderData; static std::map> colliderRandomGenPool; + static std::map colliderNameIndexes; + static int getColliderIndexFromName(std::string name) + { + auto find = colliderNameIndexes.find(name); + if ( find != colliderNameIndexes.end() ) + { + return find->second; + } + return 0; + } static void readFromFile(); }; extern EditorEntityData_t editorEntityData; @@ -3488,6 +3551,9 @@ struct EquipmentModelOffsets_t real_t scalez = 0.0; real_t rotation = 0.0; real_t pitch = 0.0; + real_t x = 0.0; + real_t y = 0.0; + real_t z = 0.0; int limbsIndex = 0; bool oversizedMask = false; bool expandToFitMask = false; @@ -3505,10 +3571,12 @@ struct EquipmentModelOffsets_t std::map adjustToExpandedHelm; }; std::map> monsterModelsMap; + std::map miscItemsBaseOffsets; + void readBaseItemsFromFile(); void readFromFile(std::string monsterName, int monsterType = NOTHING); - bool modelOffsetExists(int monster, int sprite); - bool expandHelmToFitMask(int monster, int helmSprite, int maskSprite); - bool maskHasAdjustmentForExpandedHelm(int monster, int helmSprite, int maskSprite); + int modelOffsetExists(int monster, int sprite, int monsterSprite); + int expandHelmToFitMask(int monster, int helmSprite, int maskSprite, int monsterSprite); + int maskHasAdjustmentForExpandedHelm(int monster, int helmSprite, int maskSprite, int monsterSprite); ModelOffset_t::AdditionalOffset_t getExpandHelmOffset(int monster, int helmSprite, int maskSprite); ModelOffset_t::AdditionalOffset_t getMaskOffsetForExpandHelm(int monster, int helmSprite, int maskSprite); ModelOffset_t& getModelOffset(int monster, int sprite); @@ -3575,7 +3643,9 @@ struct Compendium_t ACH_TYPE_NORMAL, ACH_TYPE_DLC1, ACH_TYPE_DLC2, - ACH_TYPE_DLC1_DLC2 + ACH_TYPE_DLC1_DLC2, + ACH_TYPE_DLC3, + ACH_TYPE_DLC1_DLC2_DLC3 }; std::string name; std::string desc; @@ -3959,6 +4029,55 @@ struct Compendium_t MONSTERS_LEFT_BEHIND, ITEMS_LEFT_BEHIND, ITEM_VALUE_LEFT_BEHIND, + CPDM_OFFHAND_CASTING_MP, + CPDM_OFFHAND_CHARGING_TIME, + CPDM_OFFHAND_CHARGING_TIME_RUN, + CPDM_OFFHAND_CHR_MAX, + CPDM_GOLD_COLLECTED, + CPDM_GOLD_COLLECTED_RUN, + CPDM_GOLD_CASTED, + CPDM_GOLD_CASTED_RUN, + CPDM_BUTTON_PRESSED, + CPDM_BUTTON_FOLLOWER_PRESSED, + CPDM_BUTTON_SHOT, + CPDM_KEYLOCK_UNLOCKED_KEY, + CPDM_KEYLOCK_PICKED, + CPDM_KEYLOCK_SKELETON_KEY, + CPDM_KEYLOCK_UNLOCKED_KEY_IRON, + CPDM_KEYLOCK_UNLOCKED_KEY_BRONZE, + CPDM_KEYLOCK_UNLOCKED_KEY_SILVER, + CPDM_KEYLOCK_UNLOCKED_KEY_GOLD, + CPDM_ASSIST_CLOAKS, + CPDM_ASSIST_RINGS, + CPDM_ASSIST_AMULETS, + CPDM_ASSIST_MASKS, + CPDM_ASSIST_INTERACTS, + CPDM_CAULDRON_INTERACTS, + CPDM_WORKBENCH_INTERACTS, + CPDM_WORKBENCH_SALVAGE, + CPDM_WORKBENCH_CRAFTS, + CPDM_WORKBENCH_REPAIRED, + CPDM_WORKBENCH_SKILLUPS, + CPDM_COOK_MEALS, + CPDM_COOK_FLAVORED_MEALS, + CPDM_CAULDRON_SKILLUPS, + CPDM_COOK_SLOP_BALLS, + CPDM_COOK_GREASE_BALLS, + CPDM_PARRIES, + CPDM_PARRIES_DMG, + CPDM_ARCHON_SPELLS_FORGOTTEN, + CPDM_MULTI_HITS, + CPDM_MAGIC_KILLS, + CPDM_EFFECT_DURATION, + CPDM_JEWEL_RECRUIT_DECREPIT, + CPDM_JEWEL_RECRUIT_WORN, + CPDM_JEWEL_RECRUIT_SERVICABLE, + CPDM_JEWEL_RECRUIT_EXCELLENT, + CPDM_FORGED, + CPDM_UPGRADED, + CPDM_SHILLELAGH_DEBUFFS_MAX, + CPDM_DUCK_DODGE, + CPDM_DUCK_CAUGHT, CPDM_EVENT_TAGS_MAX }; @@ -4082,6 +4201,7 @@ struct Compendium_t static int numUnread; }; std::map codex; + bool migrateOldSkillIndexes = false; void readCodexFromFile(bool forceLoadBaseDirectory = false); void readCodexTranslationsFromFile(bool forceLoadBaseDirectory = false); static const char* compendiumCurrentLevelToWorldString(const int currentlevel, const bool secretlevel); @@ -4148,11 +4268,14 @@ struct Compendium_t case PRO_LOCKPICKING: return "tinkering skill"; case PRO_STEALTH: return "stealth skill"; case PRO_TRADING: return "trading skill"; - case PRO_APPRAISAL: return "appraisal skill"; - case PRO_SWIMMING: return "swimming skill"; + case PRO_APPRAISAL: return "lore skill"; + case PRO_LEGACY_SWIMMING: return "swimming skill"; + case PRO_THAUMATURGY: return "thaumaturgy skill"; case PRO_LEADERSHIP: return "leadership skill"; - case PRO_SPELLCASTING: return "casting skill"; - case PRO_MAGIC: return "magic skill"; + case PRO_LEGACY_SPELLCASTING: return "casting skill"; + case PRO_MYSTICISM: return "mysticism skill"; + case PRO_LEGACY_MAGIC: return "magic skill"; + case PRO_SORCERY: return "sorcery skill"; case PRO_RANGED: return "ranged skill"; case PRO_SWORD: return "sword skill"; case PRO_MACE: return "mace skill"; @@ -4285,4 +4408,18 @@ struct Compendium_t }; }; -extern Compendium_t CompendiumEntries; \ No newline at end of file +extern Compendium_t CompendiumEntries; + +struct TreasureRoomGenerator +{ + BaronyRNG treasure_rng; + std::unordered_set treasure_floors; + std::unordered_set treasure_secret_floors; + std::map orb_floors; + std::map station_floors; + std::map station_secret_floors; + void init(); + bool bForceSpawnForCurrentFloor(int secretlevelexit, bool minotaur, BaronyRNG& mapRNG); + bool bForceStationSpawnForCurrentFloor(int secretlevelexit); +}; +extern TreasureRoomGenerator treasure_room_generator; \ No newline at end of file diff --git a/src/monster.hpp b/src/monster.hpp index 6982deb61..6b26e4252 100644 --- a/src/monster.hpp +++ b/src/monster.hpp @@ -59,6 +59,21 @@ enum Monster : int GYROBOT, DUMMYBOT, BUGBEAR, + DRYAD, + MYCONID, + SALAMANDER, + GREMLIN, + REVENANT_SKULL, + MINIMIMIC, + MONSTER_ADORCISED_WEAPON, + FLAME_ELEMENTAL, + HOLOGRAM, + MOTH_SMALL, + EARTH_ELEMENTAL, + DUCK_SMALL, + MONSTER_UNUSED_6, + MONSTER_UNUSED_7, + MONSTER_UNUSED_8, MAX_MONSTER }; const int NUMMONSTERS = MAX_MONSTER; @@ -160,7 +175,7 @@ static std::vector monsterSprites[NUMMONSTERS] = { // GNOME { - 295, 1426, 1430 + 295, 1426, 1430, 2213, 2214 }, // DEMON @@ -178,7 +193,8 @@ static std::vector monsterSprites[NUMMONSTERS] = { // MIMIC { - 1247 + 1247, + 1792 }, // LICH @@ -220,7 +236,7 @@ static std::vector monsterSprites[NUMMONSTERS] = { // INCUBUS { - 445, 702, + 445, 702, 1826 }, // VAMPIRE @@ -293,9 +309,68 @@ static std::vector monsterSprites[NUMMONSTERS] = { { 1412, }, + + //DRYAD + { + 1485, 1486, 1514, 1515, + 1963, 1964, 1992, 1993 + }, + //MYCONID + { + 1519, 1520, + 1997, 1998 + }, + //SALAMANDER + { + 1536, 1538, 1540, 1537, 1539, 1541, + 2014, 2015, 2016, 2017, 2018, 2019 + }, + //GREMLIN + { + 1569, 1570, 2047, 2048 + }, + //REVENANT_SKULL + { + 1796 + }, + //MINIMIMIC + { + 1794 + }, + //MONSTER_ADORCISED_WEAPON + { + 1797 + }, + //FLAME_ELEMENTAL + { + 1804 + }, + //HOLOGRAM + { + 1803 + }, + //MOTH__SMALL + { + 1819, 1822 + }, + //EARTH_ELEMENTAL + { + 1871, 1876 + }, + // DUCK_SMALL + { + 2225, 2226, 2231, 2232, 2237, 2238, 2307, 2308 + }, + //MONSTER_UNUSED_6 + { + }, + //MONSTER_UNUSED_7 + { + }, + //MONSTER_UNUSED_8 }; -static char monstertypename[][15] = +static char monstertypename[][32] = { "nothing", "human", @@ -334,53 +409,26 @@ static char monstertypename[][15] = "spellbot", "gyrobot", "dummybot", - "bugbear" -}; - -static char monstertypenamecapitalized[][15] = -{ - "Nothing", - "Human", - "Rat", - "Goblin", - "Slime", - "Troll", - "Bat", - "Spider", - "Ghoul", - "Skeleton", - "Scorpion", - "Imp", - "Crab", - "Gnome", - "Demon", - "Succubus", - "Mimic", - "Lich", - "Minotaur", - "Devil", - "Shopkeeper", - "Kobold", - "Scarab", - "Crystalgolem", - "Incubus", - "Vampire", - "Shadow", - "Cockatrice", - "Insectoid", - "Goatman", - "Automaton", - "Lichice", - "Lichfire", - "Sentrybot", - "Spellbot", - "Gyrobot", - "Dummybot", - "Bugbear" + "bugbear", + "dryad", + "myconid", + "salamander", + "gremlin", + "revenant_skull", + "minimimic", + "monster_adorcised_weapon", + "flame_elemental", + "hologram", + "moth", + "earth_elemental", + "duck_small", + "monster_unused_6", + "monster_unused_7", + "monster_unused_8" }; // body part focal points -extern float limbs[NUMMONSTERS][20][3]; +extern float limbs[NUMMONSTERS][30][3]; // 0: nothing // 1: red blood @@ -425,7 +473,22 @@ static char gibtype[NUMMONSTERS] = 0, //SPELLBOT 0, //GYROBOT 0, //DUMMYBOT - 1 //BUGBEAR + 1, //BUGBEAR + 1, //DRYAD + 1, //MYCONID + 1, //SALAMANDER + 1, //GREMLIN + 5, //REVENANT_SKULL + 1, //MINIMIMIC + 0, //MONSTER_ADORCISED_WEAPON + 0, //FLAME_ELEMENTAL + 0, //HOLOGRAM + 2, //MOTH_SMALL + 0, //EARTH_ELEMENTAL + 0, //DUCK_SMALL + 1, //MONSTER_UNUSED_6 + 1, //MONSTER_UNUSED_7 + 1 //MONSTER_UNUSED_8 }; // columns go like this: @@ -470,7 +533,22 @@ static double damagetables[NUMMONSTERS][7] = { 1.f, 1.f, 1.f, 1.f, 0.5, 0.5, 1.f }, // sentrybot { 1.f, 1.f, 1.f, 1.f, 0.5, 0.5, 1.f }, // gyrobot { 1.f, 1.f, 1.f, 1.f, 0.5, 1.2, 0.5 }, // dummybot - { 1.3, 1.2, 1.2, 0.7, 0.8, 1.f, 0.7 } // bugbear + { 1.3, 1.2, 1.2, 0.7, 0.8, 1.f, 0.7 }, // bugbear + { 1.3, 1.f, 1.3, 0.9, 0.7, 1.1, 0.8 }, // monster_d + { 1.3, 0.7, 1.1, 1.f, 1.f, 0.7, 1.f }, // monster_m + { 0.8, 1.f, 1.f, 1.2, 1.1, 1.f, 0.9 }, // monster_s + { 1.3, 0.9, 1.1, 0.8, 0.9, 0.8, 0.9 }, // monster_g + { 0.7, 1.5, 0.8, 0.8, 0.8, 1.3, 0.7 }, // revenant_skull + { 0.5, 0.5, 1.0, 0.5, 0.5, 1.3, 0.5 }, // minimimic + { 0.5, 1.f, 0.7, 0.5, 0.5, 1.5, 0.5 }, // monster_adorcised_weapon + { 0.5, 0.5, 0.5, 0.5, 0.5, 1.5, 0.5 }, // flame_elemental + { 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f }, // hologram + { 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f }, // moth_small + { 0.7, 1.5, 1.f, 1.f, 0.7, 1.5, 0.7 }, // earth_elemental + { 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f }, // duck_small + { 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f }, // monster_unused_6 + { 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f }, // monster_unused_7 + { 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f } // monster_unused_8 }; enum DamageTableType : int @@ -489,7 +567,7 @@ static std::vector> classStatGrowth = { // stat weightings for classes on level up // STR DEX CON INT PER CHR -- sum is approx 24. - { 6, 5, 2, 2, 4, 5 }, // BARB 0 + { 6, 5, 5, 2, 4, 2 }, // BARB 0 { 7, 2, 6, 1, 2, 6 }, // WARRIOR 1 { 3, 3, 4, 6, 5, 3 }, // HEALER 2 { 2, 7, 1, 2, 7, 5 }, // ROGUE 3 @@ -499,8 +577,8 @@ static std::vector> classStatGrowth = { 1, 3, 2, 7, 6, 5 }, // WIZARD 7 { 2, 6, 2, 6, 6, 2 }, // ARCANIST 8 { 4, 4, 4, 4, 4, 4 }, // JOKER 9 - { 4, 4, 2, 4, 2, 2 }, // SEXTON 10 - { 5, 5, 3, 2, 2, 1 }, // NINJA 11 + { 3, 4, 2, 4, 2, 3 }, // SEXTON 10 + { 4, 6, 3, 2, 4, 1 }, // NINJA 11 { 4, 2, 5, 3, 2, 2 }, // MONK 12 { 3, 2, 4, 6, 4, 4 }, // CONJURER 13 { 3, 3, 1, 6, 6, 3 }, // ACCURSED 14 @@ -509,7 +587,12 @@ static std::vector> classStatGrowth = { 2, 5, 2, 4, 7, 4 }, // MACHINIST 17 { 4, 3, 2, 3, 4, 4 }, // PUNISHER 18 { 4, 4, 4, 4, 4, 4 }, // SHAMAN 19 - { 1, 7, 1, 4, 7, 4 } // HUNTER 20 + { 1, 7, 1, 4, 7, 4 }, // HUNTER 20 + { 2, 5, 3, 4, 3, 7 }, + { 5, 5, 5, 4, 3, 2 }, + { 2, 3, 2, 8, 4, 5 }, + { 4, 2, 3, 6, 3, 5 }, + { 6, 3, 5, 3, 2, 5 } }; enum AllyNPCCommand : int @@ -625,7 +708,8 @@ enum AllyNPCChatter : int ALLY_EVENT_SPOT_ENEMY, ALLY_EVENT_WAIT, ALLY_EVENT_FOLLOW, - ALLY_EVENT_REST + ALLY_EVENT_REST, + ALLY_EVENT_DROP_FAILED }; enum AllyNPCSpecialCmd : int @@ -710,6 +794,18 @@ void initDummyBot(Entity* my, Stat* myStats); void initMimic(Entity* my, Stat* myStats); void initBat(Entity* my, Stat* myStats); void initBugbear(Entity* my, Stat* myStats); +void initMonsterD(Entity* my, Stat* myStats); +void initMonsterM(Entity* my, Stat* myStats); +void initMonsterS(Entity* my, Stat* myStats); +void initMonsterG(Entity* my, Stat* myStats); +void initRevenantSkull(Entity* my, Stat* myStats); +void initAdorcisedWeapon(Entity* my, Stat* myStats); +void initMiniMimic(Entity* my, Stat* myStats); +void initFlameElemental(Entity* my, Stat* myStats); +void initHologram(Entity* my, Stat* myStats); +void initMoth(Entity* my, Stat* myStats); +void initEarthElemental(Entity* my, Stat* myStats); +void initDuck(Entity* my, Stat* myStats); //--act*Limb functions-- void actHumanLimb(Entity* my); @@ -745,6 +841,18 @@ void actDummyBotLimb(Entity* my); void actMimicLimb(Entity* my); void actBatLimb(Entity* my); void actBugbearLimb(Entity* my); +void actMonsterDLimb(Entity* my); +void actMonsterMLimb(Entity* my); +void actMonsterSLimb(Entity* my); +void actMonsterGLimb(Entity* my); +void actRevenantSkullLimb(Entity* my); +void actMiniMimicLimb(Entity* my); +void actAdorcisedWeaponLimb(Entity* my); +void actFlameElementalLimb(Entity* my); +void actHologramLimb(Entity* my); +void actMothLimb(Entity* my); +void actEarthElementalLimb(Entity* my); +void actDuckLimb(Entity* my); //--*Die functions-- void humanDie(Entity* my); @@ -782,6 +890,18 @@ void dummyBotDie(Entity* my); void mimicDie(Entity* my); void batDie(Entity* my); void bugbearDie(Entity* my); +void monsterDDie(Entity* my); +void monsterMDie(Entity* my); +void monsterSDie(Entity* my); +void monsterGDie(Entity* my); +void revenantSkullDie(Entity* my); +void miniMimicDie(Entity* my); +void adorcisedWeaponDie(Entity* my); +void flameElementalDie(Entity* my); +void hologramDie(Entity* my); +void mothDie(Entity* my); +void earthElementalDie(Entity* my); +void duckDie(Entity* my); void monsterAnimate(Entity* my, Stat* myStats, double dist); //--*MoveBodyparts functions-- @@ -822,6 +942,18 @@ void dummyBotAnimate(Entity* my, Stat* myStats, double dist); void mimicAnimate(Entity* my, Stat* myStats, double dist); void batAnimate(Entity* my, Stat* myStats, double dist); void bugbearMoveBodyparts(Entity* my, Stat* myStats, double dist); +void monsterDMoveBodyparts(Entity* my, Stat* myStats, double dist); +void monsterMMoveBodyparts(Entity* my, Stat* myStats, double dist); +void monsterSMoveBodyparts(Entity* my, Stat* myStats, double dist); +void monsterGMoveBodyparts(Entity* my, Stat* myStats, double dist); +void revenantSkullAnimate(Entity* my, Stat* myStats, double dist); +void hologramAnimate(Entity* my, Stat* myStats, double dist); +int mothGetAttackPose(Entity* my, int basePose); +void mothAnimate(Entity* my, Stat* myStats, double dist); +void earthElementalAnimate(Entity* my, Stat* myStats, double dist); +void duckAnimate(Entity* my, Stat* myStats, double dist); +void duckSpawnFeather(int sprite, real_t x, real_t y, real_t z, Entity* my); +bool duckAreaQuck(Entity* my); //--misc functions-- void actMinotaurTrap(Entity* my); @@ -905,6 +1037,12 @@ static const int MONSTER_POSE_MIMIC_LOCKED2 = 38; static const int MONSTER_POSE_MIMIC_MAGIC1 = 39; static const int MONSTER_POSE_MIMIC_MAGIC2 = 40; static const int MONSTER_POSE_BUGBEAR_SHIELD = 41; +static const int MONSTER_POSE_PARRY = 42; +static const int MONSTER_POSE_EARTH_ELEMENTAL_ROLL = 43; +static const int MONSTER_POSE_FLAIL_SWING = 44; +static const int MONSTER_POSE_FLAIL_SWING_RETURN = 45; +static const int MONSTER_POSE_FLAIL_SWING_WINDUP = 46; +static const int MONSTER_POSE_SWEEP_ATTACK_NO_UPDATE = 47; //--monster special cooldowns static const int MONSTER_SPECIAL_COOLDOWN_GOLEM = 150; @@ -932,7 +1070,16 @@ static const int MONSTER_SPECIAL_COOLDOWN_VAMPIRE_DRAIN = 300; static const int MONSTER_SPECIAL_COOLDOWN_SUCCUBUS_CHARM = 400; static const int MONSTER_SPECIAL_COOLDOWN_MIMIC_EAT = 500; static const int MONSTER_SPECIAL_COOLDOWN_SLIME_SPRAY = 250; +static const int MONSTER_SPECIAL_COOLDOWN_SKULL_CAST = 250; +static const int MONSTER_SPECIAL_COOLDOWN_MOTH_CAST = 250; static const int MONSTER_SPECIAL_COOLDOWN_BUGBEAR = 500; +static const int MONSTER_SPECIAL_COOLDOWN_MONSTER_D = 250; +static const int MONSTER_SPECIAL_COOLDOWN_MONSTER_D_PUSH = 75; +static const int MONSTER_SPECIAL_COOLDOWN_MONSTER_M_THROW = 500; +static const int MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_SHORT = 300; +static const int MONSTER_SPECIAL_COOLDOWN_MONSTER_M_CAST_LONG = 550; +static const int MONSTER_SPECIAL_COOLDOWN_MONSTER_G_THROW = 500; +static const int MONSTER_SPECIAL_COOLDOWN_MONSTER_G_CAST = 550; //--monster target search types static const int MONSTER_TARGET_ENEMY = 0; @@ -1003,7 +1150,7 @@ bool handleMonsterChatter(int monsterclicked, bool ringconflict, char namesays[6 // check qty of a certain creature race alive on a map int numMonsterTypeAliveOnMap(Monster creature, Entity*& lastMonster); // get monster strings from language file -std::string getMonsterLocalizedName(Monster creature); +std::string getMonsterLocalizedName(Monster creature, Stat* optionalStats = nullptr); std::string getMonsterLocalizedPlural(Monster creature); std::string getMonsterLocalizedInjury(Monster creature); @@ -1046,6 +1193,12 @@ static const int SPIDER_CAST = 1; //--Slime-- static const int SLIME_CAST = 1; +//--Skull +static const int SKULL_CAST = 1; + +//--Moth +static const int MOTH_CAST = 1; + //--Shadow-- static const int SHADOW_SPELLCAST = 1; static const int SHADOW_TELEPORT_ONLY = 2; @@ -1100,6 +1253,23 @@ static const int BAT_REST_DISTURBED = 2; //-Bugbear-- static const int BUGBEAR_DEFENSE = 1; +static const int MONSTER_D_SPECIAL_CAST1 = 1; +static const int MONSTER_D_SPECIAL_CAST2 = 2; +static const int MONSTER_D_SPECIAL_CAST3 = 3; + +static const int MONSTER_M_SPECIAL_THROW = 1; +static const int MONSTER_M_SPECIAL_CAST1 = 2; +static const int MONSTER_M_SPECIAL_CAST2 = 3; +static const int MONSTER_M_SPECIAL_CAST3 = 4; + +static const int MONSTER_G_SPECIAL_THROW = 1; +static const int MONSTER_G_SPECIAL_CAST1 = 2; + +//-Duck-- +static const int DUCK_DIVE = 1; +static const int DUCK_INERT = 2; +static const int DUCK_RETURN = 3; + struct MonsterData_t { struct MonsterDataEntry_t @@ -1271,4 +1441,8 @@ struct MimicGenerator void init(); bool bForceSpawnForCurrentFloor(); }; -extern MimicGenerator mimic_generator; \ No newline at end of file +extern MimicGenerator mimic_generator; + +bool monsterDebugModels(Entity* my, real_t* dist); + +extern double sightranges[NUMMONSTERS]; \ No newline at end of file diff --git a/src/monster_automaton.cpp b/src/monster_automaton.cpp index ca69277ca..a246d201c 100644 --- a/src/monster_automaton.cpp +++ b/src/monster_automaton.cpp @@ -22,6 +22,7 @@ #include "magic/magic.hpp" #include "prng.hpp" #include "mod_tools.hpp" +#include "shops.hpp" void initAutomaton(Entity* my, Stat* myStats) { @@ -78,18 +79,27 @@ void initAutomaton(Entity* my, Stat* myStats) else if ( !strncmp(myStats->name, "corrupted automaton", strlen("corrupted automaton")) ) { greaterMonster = true; - myStats->HP = 150; - myStats->MAXHP = 150; + myStats->HP = 200; + myStats->MAXHP = 200; myStats->RANDOM_MAXHP = 0; myStats->RANDOM_HP = 0; - myStats->OLDHP = myStats->HP; myStats->STR = 35; myStats->DEX = 13; - myStats->CON = 8; + myStats->CON = 10; myStats->INT = 10; myStats->PER = 25; myStats->CHR = -3; myStats->LVL = 30; + for ( int c = 1; c < MAXPLAYERS; ++c ) + { + if ( !client_disconnected[c] ) + { + myStats->MAXHP += 50; + myStats->HP = myStats->MAXHP; + myStats->CON += 2; + } + } + myStats->OLDHP = myStats->HP; myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] = 1; myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] = 1; myStats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] = 1; @@ -578,7 +588,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -631,7 +641,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] + if ( myStats->getEffectActive(EFF_ASLEEP) && (my->monsterSpecialState != AUTOMATON_MALFUNCTION_START && my->monsterSpecialState != AUTOMATON_MALFUNCTION_RUN) ) { my->z = 2; @@ -640,37 +650,47 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) else { if ( my->monsterSpecialState != AUTOMATON_MALFUNCTION_START - && my->monsterSpecialState != AUTOMATON_MALFUNCTION_RUN - && !(myStats->amulet && myStats->amulet->type == AMULET_LIFESAVING && myStats->amulet->beatitude >= 0) ) + && my->monsterSpecialState != AUTOMATON_MALFUNCTION_RUN ) { my->z = -.5; my->pitch = 0; - if ( (myStats->HP < 25 && !myStats->EFFECTS[EFF_CONFUSED]) - || (myStats->HP < 50 && !strncmp(myStats->name, "corrupted automaton", strlen("corrupted automaton"))) - ) + if ( !(myStats->amulet && myStats->amulet->type == AMULET_LIFESAVING && myStats->amulet->beatitude >= 0) ) { - // threshold for boom boom - if ( local_rng.rand() % 4 > 0 ) // 3/4 + if ( (myStats->HP < 25 && !myStats->getEffectActive(EFF_CONFUSED)) + || (myStats->HP < 50 && !strncmp(myStats->name, "corrupted automaton", strlen("corrupted automaton"))) + ) { - my->monsterSpecialState = AUTOMATON_MALFUNCTION_START; - my->monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_AUTOMATON_MALFUNCTION; - serverUpdateEntitySkill(my, 33); + // threshold for boom boom + if ( local_rng.rand() % 4 > 0 ) // 3/4 + { + my->monsterSpecialState = AUTOMATON_MALFUNCTION_START; + my->monsterSpecialTimer = MONSTER_SPECIAL_COOLDOWN_AUTOMATON_MALFUNCTION; + serverUpdateEntitySkill(my, 33); - myStats->EFFECTS[EFF_PARALYZED] = true; - myStats->EFFECTS_TIMERS[EFF_PARALYZED] = -1; - } - else - { - myStats->EFFECTS[EFF_CONFUSED] = true; - myStats->EFFECTS_TIMERS[EFF_CONFUSED] = -1; - myStats->EFFECTS[EFF_PARALYZED] = true; - myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 25; - playSoundEntity(my, 263, 128); - spawnMagicEffectParticles(my->x, my->y, my->z, 170); + myStats->setEffectActive(EFF_PARALYZED, 1); + myStats->EFFECTS_TIMERS[EFF_PARALYZED] = -1; + } + else + { + if ( strncmp(myStats->name, "corrupted automaton", strlen("corrupted automaton")) ) + { + my->setEffect(EFF_CONFUSED, Uint8(MAXPLAYERS + 1), -1, true, true, true, true); + } + myStats->setEffectActive(EFF_PARALYZED, 1); + myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 25; + playSoundEntity(my, 263, 128); + spawnMagicEffectParticles(my->x, my->y, my->z, 170); + } } } } } + + if ( my->monsterSpecialState != AUTOMATON_MALFUNCTION_START + && my->monsterSpecialState != AUTOMATON_MALFUNCTION_RUN ) + { + my->creatureHandleLiftZ(); + } } if ( my->monsterSpecialState == AUTOMATON_MALFUNCTION_START || my->monsterSpecialState == AUTOMATON_MALFUNCTION_RUN ) @@ -679,6 +699,27 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->monsterSpecialState = AUTOMATON_MALFUNCTION_RUN; createParticleExplosionCharge(my, 174, 100, 0.1); + + if ( multiplayer != CLIENT && my->monsterCanTradeWith(-1) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i]->isLocalPlayer() && shopkeeper[i] == my->getUID() ) + { + players[i]->closeAllGUIs(CLOSEGUI_ENABLE_SHOOTMODE, CLOSEGUI_CLOSE_ALL); + } + else if ( i > 0 && !client_disconnected[i] && multiplayer == SERVER && !players[i]->isLocalPlayer() ) + { + // inform client of abandonment + strcpy((char*)net_packet->data, "SHPC"); + SDLNet_Write32(my->getUID(), &net_packet->data[4]); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + } + } } if ( multiplayer != CLIENT ) { @@ -686,7 +727,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->attack(MONSTER_POSE_AUTOMATON_MALFUNCTION, 0, my); spawnExplosion(my->x, my->y, my->z); - my->modHP(-1000); + my->setHP(0); } } } @@ -802,7 +843,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) playSoundEntityLocal(my, 170, 32); if ( multiplayer != CLIENT ) { - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 30; } } @@ -891,15 +932,21 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[AUTOMATON][1][0]; + entity->focaly = limbs[AUTOMATON][1][1]; + entity->focalz = limbs[AUTOMATON][1][2]; if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) { entity->sprite = 468; } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); entity->scalex = 1; // shrink the width of the breastplate entity->scaley = 0.8; @@ -1083,7 +1130,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == NULL || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == NULL || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1156,7 +1203,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1200,7 +1247,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == NULL || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == NULL || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1258,7 +1305,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == NULL || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == NULL || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1311,17 +1358,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == NULL || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == NULL || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1386,7 +1423,7 @@ void automatonMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(AUTOMATON, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(AUTOMATON, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); @@ -1483,6 +1520,11 @@ void Entity::automatonRecycleItem() return; } + if ( monsterCanTradeWith(-1) ) + { + return; + } + if ( this->monsterSpecialTimer > 0 && !(this->monsterSpecialState == AUTOMATON_RECYCLE_ANIMATION_COMPLETE) ) { // if we're on cooldown, skip checking @@ -1584,12 +1626,12 @@ void Entity::automatonRecycleItem() //messagePlayer(0, "made it past"); - int maxGoldValue = ((items[item1->type].value + items[item2->type].value) * 2) / 3; + int maxGoldValue = ((item1->getGoldValue() + item2->getGoldValue()) * 2) / 3; if ( local_rng.rand() % 2 == 0 ) { - maxGoldValue = ((items[item1->type].value + items[item2->type].value) * 1) / 2; + maxGoldValue = ((item1->getGoldValue() + item2->getGoldValue()) * 1) / 2; } - int minGoldValue = ((items[item1->type].value + items[item2->type].value) * 1) / 3; + int minGoldValue = ((item1->getGoldValue() + item2->getGoldValue()) * 1) / 3; ItemType type; // generate a weapon/armor piece and add it into the inventory. switch ( local_rng.rand() % 10 ) diff --git a/src/monster_bat.cpp b/src/monster_bat.cpp index c70098b3e..9f88d906f 100644 --- a/src/monster_bat.cpp +++ b/src/monster_bat.cpp @@ -295,15 +295,20 @@ void batAnimate(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { my->z = limbs[BAT_SMALL][5][2]; - if ( !myStats->EFFECTS[EFF_LEVITATING] ) + if ( !myStats->getEffectActive(EFF_LEVITATING) ) { - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; } if ( !my->isMobile() ) { my->monsterRotate(); } + + if ( !my->isUntargetableBat() ) + { + my->creatureHandleLiftZ(); + } } if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) @@ -318,12 +323,12 @@ void batAnimate(Entity* my, Stat* myStats, double dist) MONSTER_ATTACK = MONSTER_POSE_MELEE_WINDUP1; MONSTER_ATTACKTIME = 0; } - if ( keystatus[SDLK_h] ) + /*if ( keystatus[SDLK_h] ) { keystatus[SDLK_h] = 0; myStats->EFFECTS[EFF_STUNNED] = !myStats->EFFECTS[EFF_STUNNED]; myStats->EFFECTS_TIMERS[EFF_STUNNED] = myStats->EFFECTS[EFF_STUNNED] ? -1 : 0; - } + }*/ if ( keystatus[SDLK_j] ) { keystatus[SDLK_j] = 0; diff --git a/src/monster_bugbear.cpp b/src/monster_bugbear.cpp index e218ae305..ee81b1100 100644 --- a/src/monster_bugbear.cpp +++ b/src/monster_bugbear.cpp @@ -401,7 +401,7 @@ void bugbearMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -454,7 +454,7 @@ void bugbearMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = limbs[BUGBEAR][11][0]; } @@ -462,6 +462,7 @@ void bugbearMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->z = limbs[BUGBEAR][11][2]; } + my->creatureHandleLiftZ(); } Entity* weaponarm = nullptr; @@ -954,7 +955,7 @@ void bugbearMoveBodyparts(Entity* my, Stat* myStats, double dist) } }*/ - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1096,7 +1097,7 @@ void bugbearMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->flags[INVISIBLE] = true; } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } diff --git a/src/monster_cockatrice.cpp b/src/monster_cockatrice.cpp index a22a6bedc..bdfd174a2 100644 --- a/src/monster_cockatrice.cpp +++ b/src/monster_cockatrice.cpp @@ -57,16 +57,9 @@ void initCockatrice(Entity* my, Stat* myStats) // boss variants // random effects - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; - // cockatrices don't sleep! - /*if ( rng.rand() % 4 == 0 ) - { - myStats->EFFECTS[EFF_ASLEEP] = true; - myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 3600; - }*/ - // generates equipment and weapons if available from editor createMonsterEquipment(myStats, rng); @@ -330,7 +323,7 @@ void cockatriceMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: use isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -342,7 +335,7 @@ void cockatriceMoveBodyparts(Entity* my, Stat* myStats, double dist) bodypart++; continue; } - if ( bodypart >= 7 ) + if ( bodypart >= 9 ) { break; } @@ -367,7 +360,7 @@ void cockatriceMoveBodyparts(Entity* my, Stat* myStats, double dist) bodypart++; continue; } - if ( bodypart >= 7 ) + if ( bodypart >= 9 ) { break; } @@ -383,7 +376,7 @@ void cockatriceMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->pitch = PI / 4; } @@ -396,7 +389,7 @@ void cockatriceMoveBodyparts(Entity* my, Stat* myStats, double dist) } // cockatrices are always flying - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; } @@ -412,14 +405,34 @@ void cockatriceMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( my->monsterAnimationLimbOvershoot >= ANIMATE_OVERSHOOT_TO_SETPOINT ) { // handle z movement on windup + if ( abs(my->creatureHoverZ) > 0.01 ) + { + my->z = -4.5; + my->creatureHoverZ = 0.0; + } limbAnimateWithOvershoot(my, ANIMATE_Z, 0.2, -3.5, 0.05, -5.5, ANIMATE_DIR_POSITIVE); // default z is -4.5 in actmonster.cpp } } - else if(MONSTER_ATTACK != MONSTER_POSE_MELEE_WINDUP3 ) + else if ( MONSTER_ATTACK != MONSTER_POSE_MELEE_WINDUP3 ) { // post-swing head animation. client doesn't need to adjust the entity pitch, server will handle. limbAnimateWithOvershoot(my, ANIMATE_PITCH, 0.2, PI / 4, 0.1, 0, ANIMATE_DIR_POSITIVE); - limbAnimateToLimit(my, ANIMATE_Z, 0.2, -4.5, false, 0); + + if ( myStats->getEffectActive(EFF_LIFT) ) + { + my->z = -4.5; + my->creatureHandleLiftZ(); + } + else + { + if ( abs(my->creatureHoverZ) > 0.01 ) + { + my->z = -4.5; + my->creatureHoverZ = 0.0; + } + my->z = std::min(my->z, -4.5); + limbAnimateToLimit(my, ANIMATE_Z, 0.2, -4.5, false, 0); + } } } @@ -564,7 +577,7 @@ void cockatriceMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { // cockatrice can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 20; } entity->skill[0] = 0; @@ -649,7 +662,7 @@ void cockatriceMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; // cockatrice can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 50; } } diff --git a/src/monster_crystalgolem.cpp b/src/monster_crystalgolem.cpp index 17504f5d7..0fcf96431 100644 --- a/src/monster_crystalgolem.cpp +++ b/src/monster_crystalgolem.cpp @@ -57,9 +57,9 @@ void initCrystalgolem(Entity* my, Stat* myStats) // boss variants // random effects - if ( rng.rand() % 8 == 0 ) + if ( rng.rand() % 8 == 0 && myStats->getAttribute("spawn_no_sleep") == "" ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 3600; } @@ -264,7 +264,7 @@ void crystalgolemMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -317,7 +317,7 @@ void crystalgolemMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 1.5; } @@ -325,6 +325,7 @@ void crystalgolemMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->z = -1.5; } + my->creatureHandleLiftZ(); } //Move bodyparts @@ -504,7 +505,7 @@ void crystalgolemMoveBodyparts(Entity* my, Stat* myStats, double dist) createParticleDot(my); if ( multiplayer != CLIENT ) { - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 60; } } @@ -543,9 +544,9 @@ void crystalgolemMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = 0; if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_PARALYZED] == true ) + if ( myStats->getEffectActive(EFF_PARALYZED) ) { - myStats->EFFECTS[EFF_PARALYZED] = false; + myStats->clearEffect(EFF_PARALYZED); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 0; } } diff --git a/src/monster_d.cpp b/src/monster_d.cpp new file mode 100644 index 000000000..c0e477f80 --- /dev/null +++ b/src/monster_d.cpp @@ -0,0 +1,2485 @@ +/*------------------------------------------------------------------------------- + + BARONY + File: monster_goatman.cpp + Desc: implements all of the goatman monster's code + + Copyright 2013-2016 (c) Turning Wheel LLC, all rights reserved. + See LICENSE for details. + +-------------------------------------------------------------------------------*/ + +#include "main.hpp" +#include "game.hpp" +#include "stat.hpp" +#include "entity.hpp" +#include "items.hpp" +#include "monster.hpp" +#include "engine/audio/sound.hpp" +#include "net.hpp" +#include "collision.hpp" +#include "player.hpp" +#include "prng.hpp" +#include "scores.hpp" +#include "mod_tools.hpp" + +real_t getNormalHeightMonsterD(Entity& my) +{ + if ( my.sprite == 1514 || my.sprite == 1515 ) + { + return 2.5; + } + return -0.0; +} + +void initMonsterD(Entity* my, Stat* myStats) +{ + if ( !my ) + { + return; + } + node_t* node; + bool spawnedBoss = false; + + my->flags[BURNABLE] = true; + //my->initMonster(1485); + my->initMonster(1514); + my->z = getNormalHeightMonsterD(*my); + + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = 725; + MONSTER_SPOTVAR = 2; + MONSTER_IDLESND = 720; + MONSTER_IDLEVAR = 3; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if (myStats->sex == FEMALE) + { + my->sprite = 1486; + } + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + + // boss variants + //const bool boss = + // rng.rand() % 50 == 0 && + // !my->flags[USERFLAG2] && + // !myStats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS]; + //if ( (boss || (*cvar_summonBosses && conductGameChallenges[CONDUCT_CHEATS_ENABLED])) && myStats->leader_uid == 0 ) + //{ + // myStats->setAttribute("special_npc", "gharbad"); + // strcpy(myStats->name, MonsterData_t::getSpecialNPCName(*myStats).c_str()); + // my->sprite = MonsterData_t::getSpecialNPCBaseModel(*myStats); + // myStats->sex = MALE; + // myStats->STR += 10; + // myStats->DEX += 2; + // myStats->MAXHP += 75; + // myStats->HP = myStats->MAXHP; + // myStats->OLDHP = myStats->MAXHP; + // myStats->CHR = -1; + // spawnedBoss = true; + // //TODO: Boss stats + + // //Spawn in potions. + // int end = rng.rand()%NUM_GOATMAN_BOSS_GHARBAD_POTIONS + 5; + // for ( int i = 0; i < end; ++i ) + // { + // switch ( rng.rand()%10 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // case 8: + // newItem(POTION_BOOZE, EXCELLENT, 0, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + // break; + // case 9: + // newItem(POTION_HEALING, EXCELLENT, 0, 1, rng.rand(), false, &myStats->inventory); + // break; + // default: + // printlog("Tried to spawn goatman boss \"Gharbad\" invalid potion."); + // break; + // } + // } + + // newItem(CRYSTAL_SHURIKEN, EXCELLENT, 1 + rng.rand()%1, rng.rand()%NUM_GOATMAN_BOSS_GHARBAD_THROWN_WEAPONS + 2, rng.rand(), true, &myStats->inventory); + //} + + // random effects + /*if ( rng.rand() % 8 == 0 ) + { + my->setEffect(EFF_ASLEEP, true, 1800 + rng.rand() % 1800, false); + }*/ + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + if ( rng.rand() % 2 == 0 ) + { + myStats->setAttribute("monster_d_type", "watcher"); + } + else + { + myStats->setAttribute("monster_d_type", "nymph"); + } + + //bool isShaman = false; + //if ( rng.rand() % 2 && !spawnedBoss && !minion ) + //{ + // isShaman = true; + // if ( myStats->leader_uid == 0 && !my->flags[USERFLAG2] && rng.rand() % 2 == 0 ) + // { + // Entity* entity = summonMonster(GOATMAN, my->x, my->y); + // if ( entity ) + // { + // entity->parent = my->getUID(); + // if ( Stat* followerStats = entity->getStats() ) + // { + // followerStats->leader_uid = entity->parent; + // } + // entity->seedEntityRNG(rng.getU32()); + // } + // if ( rng.rand() % 5 == 0 ) + // { + // // summon second ally randomly. + // entity = summonMonster(GOATMAN, my->x, my->y); + // if ( entity ) + // { + // entity->parent = my->getUID(); + // if ( Stat* followerStats = entity->getStats() ) + // { + // followerStats->leader_uid = entity->parent; + // } + // entity->seedEntityRNG(rng.getU32()); + // } + // } + // } + //} + //else + //{ + // myStats->DEX += 1; // more speed for brawlers. + //} + + // generate the default inventory items for the monster, provided the editor sprite allowed enough default slots + //switch ( defaultItems ) + //{ + // case 6: + // case 5: + // case 4: + // case 3: + // case 2: + // case 1: + // if ( isShaman && rng.rand() % 10 == 0 ) + // { + // switch ( rng.rand() % 4 ) + // { + // case 0: + // newItem(SPELLBOOK_SLOW, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 1: + // newItem(SPELLBOOK_FIREBALL, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 2: + // newItem(SPELLBOOK_COLD, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 3: + // newItem(SPELLBOOK_FORCEBOLT, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // } + // } + // break; + // default: + // break; + //} + + + ////Give weapons. + //if ( !spawnedBoss ) + //{ + // if ( !isShaman && rng.rand() % 3 > 0 ) + // { + // newItem(STEEL_CHAKRAM, SERVICABLE, 0, rng.rand()%NUM_GOATMAN_THROWN_WEAPONS + 1, rng.rand(), false, &myStats->inventory); + // } + // int numpotions = rng.rand() % NUM_GOATMAN_POTIONS + 2; + // if ( rng.rand() % 3 == 0 ) + // { + // int numhealpotion = rng.rand() % 2 + 1; + // newItem(POTION_HEALING, static_cast(rng.rand() % 3 + DECREPIT), 0, numhealpotion, rng.rand(), false, &myStats->inventory); + // numpotions -= numhealpotion; + // } + // if ( rng.rand() % 4 > 0 ) + // { + // // undroppable + // newItem(POTION_BOOZE, static_cast(rng.rand() % 3 + DECREPIT), 0, numpotions, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + // } + //} + + //if ( isShaman ) + //{ + // //give shield + // if ( myStats->shield == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] == 1 ) + // { + // // give shield + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // myStats->shield = newItem(TOOL_CRYSTALSHARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 8: + // myStats->shield = newItem(MIRROR_SHIELD, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shield = newItem(TOOL_LANTERN, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give cloak + // if ( myStats->cloak == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_CLOAK] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // break; + // default: + // myStats->cloak = newItem(CLOAK, WORN, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give helmet + // if ( myStats->helmet == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_HELM] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // myStats->helmet = newItem(HAT_HOOD, WORN, -1 + rng.rand() % 3, 1, 0, false, nullptr); + // break; + // default: + // myStats->helmet = newItem(HAT_WIZARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give armor + // if ( myStats->breastplate == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // myStats->breastplate = newItem(WIZARD_DOUBLET, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 2: + // myStats->breastplate = newItem(LEATHER_BREASTPIECE, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // break; + // } + // } + // // give booties + // if ( myStats->shoes == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_BOOTS] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // myStats->shoes = newItem(IRON_BOOTS, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->shoes = newItem(CRYSTAL_BOOTS, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shoes = newItem(STEEL_BOOTS, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // give weapon + if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + { + if ( myStats->getAttribute("monster_d_type") == "nymph" ) + { + switch ( rng.rand() % 10 ) + { + case 0: + case 1: + case 2: + case 3: + case 4: + myStats->weapon = newItem(BRANCH_BOW, static_cast(rng.rand() % 2 + SERVICABLE), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + case 5: + case 6: + case 7: + case 8: + case 9: + myStats->weapon = newItem(BRANCH_BOW_INFECTED, static_cast(rng.rand() % 2 + SERVICABLE), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + default: + break; + } + } + else if ( myStats->getAttribute("monster_d_type") == "watcher" ) + { + myStats->weapon = newItem(BRANCH_STAFF, static_cast(rng.rand() % 2 + SERVICABLE), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + + //give shield + if ( myStats->shield == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] == 1 ) + { + if ( myStats->weapon && isRangedWeapon(*myStats->weapon) ) + { + my->monsterGenerateQuiverItem(myStats); + } + else + { + /*switch ( rng.rand() % 10 ) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + break; + case 6: + case 7: + myStats->shield = newItem(WOODEN_SHIELD, DECREPIT, -1 + rng.rand() % 2, 1, rng.rand(), false, nullptr); + break; + case 8: + myStats->shield = newItem(BRONZE_SHIELD, DECREPIT, -1 + rng.rand() % 2, 1, rng.rand(), false, nullptr); + break; + case 9: + myStats->shield = newItem(IRON_SHIELD, DECREPIT, -1 + rng.rand() % 2, 1, rng.rand(), false, nullptr); + break; + }*/ + } + } + // } + //} + //else + //{ + // ////give shield + // //if ( myStats->shield == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] == 1 ) + // //{ + // // switch ( rng.rand() % 20 ) + // // { + // // case 0: + // // case 1: + // // case 2: + // // case 3: + // // case 4: + // // case 5: + // // case 6: + // // case 7: + // // myStats->shield = newItem(TOOL_CRYSTALSHARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // case 8: + // // myStats->shield = newItem(MIRROR_SHIELD, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // default: + // // myStats->shield = newItem(TOOL_LANTERN, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // } + // //} + // // give cloak + // /*if ( myStats->cloak == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_CLOAK] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // break; + // default: + // myStats->cloak = newItem(CLOAK, WORN, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // }*/ + // //// give helmet + // //if ( myStats->helmet == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_HELM] == 1 ) + // //{ + // // switch ( rng.rand() % 10 ) + // // { + // // case 0: + // // case 1: + // // myStats->helmet = newItem(HAT_HOOD, WORN, -1 + rng.rand() % 3, 1, 0, false, nullptr); + // // break; + // // default: + // // myStats->helmet = newItem(HAT_WIZARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, 0, false, nullptr); + // // break; + // // } + // //} + // // give armor + // if ( myStats->breastplate == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // myStats->breastplate = newItem(STEEL_BREASTPIECE, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 3: + // case 4: + // myStats->breastplate = newItem(LEATHER_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 5: + // case 6: + // myStats->breastplate = newItem(IRON_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->breastplate = newItem(CRYSTAL_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // break; + // } + // } + // // give booties + // if ( myStats->shoes == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_BOOTS] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // myStats->shoes = newItem(IRON_BOOTS, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->shoes = newItem(CRYSTAL_BOOTS, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shoes = newItem(STEEL_BOOTS, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give weapon + // if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // myStats->weapon = newItem(STEEL_AXE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 18: + // myStats->weapon = newItem(CRYSTAL_BATTLEAXE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->weapon = newItem(CRYSTAL_MACE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->weapon = newItem(STEEL_MACE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + //} + } + } + + // torso + const int torso_sprite = my->sprite == 1514 ? 1513 : + (my->sprite == 1515 ? 1516 : + (my->sprite == 1485 ? 1511 : 1512)); + Entity* entity = newEntity(torso_sprite, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->scalex = 1.01; + entity->scaley = 1.01; + entity->scalez = 1.01; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DRYAD][1][0]; // 0 + entity->focaly = limbs[DRYAD][1][1]; // 0 + entity->focalz = limbs[DRYAD][1][2]; // 0 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // right leg + entity = newEntity(my->sprite == 1514 ? 1510 : + (my->sprite == 1515 ? 1518 : + (my->sprite == 1485 ? 1508 : 1509)), 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DRYAD][2][0]; // 0 + entity->focaly = limbs[DRYAD][2][1]; // 0 + entity->focalz = limbs[DRYAD][2][2]; // 2 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // left leg + entity = newEntity(my->sprite == 1514 ? 1507 : + (my->sprite == 1515 ? 1517 : + (my->sprite == 1485 ? 1505 : 1506)), 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DRYAD][3][0]; // 0 + entity->focaly = limbs[DRYAD][3][1]; // 0 + entity->focalz = limbs[DRYAD][3][2]; // 2 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // right arm + entity = newEntity((my->sprite == 1485 || my->sprite == 1514) ? 1491 : 1493, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DRYAD][4][0]; // 0 + entity->focaly = limbs[DRYAD][4][1]; // 0 + entity->focalz = limbs[DRYAD][4][2]; // 1.5 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // left arm + entity = newEntity((my->sprite == 1485 || my->sprite == 1514) ? 1487 : 1489, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DRYAD][5][0]; // 0 + entity->focaly = limbs[DRYAD][5][1]; // 0 + entity->focalz = limbs[DRYAD][5][2]; // 1.5 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // world weapon + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[DRYAD][6][0]; // 1.5 + entity->focaly = limbs[DRYAD][6][1]; // 0 + entity->focalz = limbs[DRYAD][6][2]; // -.5 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + entity->pitch = .25; + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // shield + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[DRYAD][7][0]; // 2 + entity->focaly = limbs[DRYAD][7][1]; // 0 + entity->focalz = limbs[DRYAD][7][2]; // 0 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // cloak + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[DRYAD][8][0]; // 0 + entity->focaly = limbs[DRYAD][8][1]; // 0 + entity->focalz = limbs[DRYAD][8][2]; // 4 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // helmet + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->scalex = 1.01; + entity->scaley = 1.01; + entity->scalez = 1.01; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[DRYAD][9][0]; // 0 + entity->focaly = limbs[DRYAD][9][1]; // 0 + entity->focalz = limbs[DRYAD][9][2]; // -2 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // mask + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[DRYAD][10][0]; // 0 + entity->focaly = limbs[DRYAD][10][1]; // 0 + entity->focalz = limbs[DRYAD][10][2]; // .25 + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // head left + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DRYAD][11][0]; + entity->focaly = limbs[DRYAD][11][1]; + entity->focalz = limbs[DRYAD][11][2]; + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // head right + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DRYAD][13][0]; + entity->focaly = limbs[DRYAD][13][1]; + entity->focalz = limbs[DRYAD][13][2]; + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // head center + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DRYAD][15][0]; + entity->focaly = limbs[DRYAD][15][1]; + entity->focalz = limbs[DRYAD][15][2]; + entity->scalex = 1.008; + entity->scaley = 1.008; + entity->scalez = 1.008; + entity->behavior = &actMonsterDLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + if ( multiplayer == CLIENT || MONSTER_INIT ) + { + return; + } +} + +void actMonsterDLimb(Entity* my) +{ + my->actMonsterLimb(true); +} + +void monsterDDie(Entity* my) +{ + Entity* gib = spawnGib(my); + gib->skill[5] = 1; // poof + gib->sprite = my->sprite; + serverSpawnGibForClient(gib); + for ( int c = 0; c < 8; c++ ) + { + Entity* gib = spawnGib(my); + serverSpawnGibForClient(gib); + } + + my->spawnBlood(); + spawnLeafPile(my->x, my->y, local_rng.rand() % 4); + + playSoundEntity(my, 723 + local_rng.rand() % 2, 128); + + my->removeMonsterDeathNodes(); + + list_RemoveNode(my->mynode); + return; +} + +#define MONSTER_DWALKSPEED .13 + +void monsterDMoveBodyparts(Entity* my, Stat* myStats, double dist) +{ + node_t* node; + Entity* entity = nullptr, *entity2 = nullptr; + Entity* additionalLimb = nullptr; + Entity* rightbody = nullptr; + Entity* weaponarm = nullptr; + int bodypart; + bool wearingring = false; + + my->focalx = limbs[DRYAD][0][0]; + my->focaly = limbs[DRYAD][0][1]; + my->focalz = limbs[DRYAD][0][2]; + + if ( my->sprite == 1514 || my->sprite == 1515 ) + { + my->focalz += 0.5; + my->monsterSpellAnimation = MONSTER_SPELLCAST_SMALL_HUMANOID; + } + + bool debugModel = monsterDebugModels(my, &dist); + /*if ( keystatus[SDLK_n] ) + { + keystatus[SDLK_n] = 0; + my->monsterAttack = MONSTER_POSE_RANGED_WINDUP3; + }*/ + /*if ( keystatus[SDLK_n] ) + { + dist = 0.5; + } + static real_t distAccum = 0.0; + if ( dist > distAccum ) + { + distAccum = dist; + } + else + { + distAccum = std::max(dist, distAccum * 0.9); + } + dist = distAccum;*/ + + // set invisibility //TODO: isInvisible()? + if ( multiplayer != CLIENT ) + { + if ( myStats->ring != nullptr ) + if ( myStats->ring->type == RING_INVISIBILITY ) + { + wearingring = true; + } + if ( myStats->cloak != nullptr ) + if ( myStats->cloak->type == CLOAK_INVISIBILITY ) + { + wearingring = true; + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) + { + my->flags[INVISIBLE] = true; + my->flags[BLOCKSIGHT] = false; + bodypart = 0; + for (node = my->children.first; node != nullptr; node = node->next) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + bodypart++; + continue; + } + if ( bodypart >= 7 ) + { + break; + } + entity = (Entity*)node->element; + if ( !entity->flags[INVISIBLE] ) + { + entity->flags[INVISIBLE] = true; + serverUpdateEntityBodypart(my, bodypart); + } + bodypart++; + } + } + else + { + my->flags[INVISIBLE] = false; + my->flags[BLOCKSIGHT] = true; + bodypart = 0; + for (node = my->children.first; node != nullptr; node = node->next) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + bodypart++; + continue; + } + if ( bodypart >= 7 ) + { + break; + } + entity = (Entity*)node->element; + if ( entity->flags[INVISIBLE] ) + { + entity->flags[INVISIBLE] = false; + serverUpdateEntityBodypart(my, bodypart); + serverUpdateEntityFlag(my, INVISIBLE); + } + bodypart++; + } + } + + // sleeping + if ( myStats->getEffectActive(EFF_ASLEEP) ) + { + if ( my->sprite == 1514 || my->sprite == 1515 ) + { + my->z = 3.75; + } + else + { + my->z = 3.0; + } + my->pitch = PI / 4; + } + else + { + my->z = getNormalHeightMonsterD(*my); + if ( my->monsterAttack == 0 ) + { + if ( debugModel ) + { + my->pitch = my->fskill[0]; + } + else + { + my->pitch = 0; + } + } + } + my->creatureHandleLiftZ(); + } + + Entity* shieldarm = nullptr; + Entity* helmet = nullptr; + + //Move bodyparts + for (bodypart = 0, node = my->children.first; node != nullptr; node = node->next, bodypart++) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + // post-swing head animation. client doesn't need to adjust the entity pitch, server will handle. + if ( my->monsterAttack != MONSTER_POSE_RANGED_WINDUP3 && bodypart == 1 && multiplayer != CLIENT ) + { + limbAnimateToLimit(my, ANIMATE_PITCH, 0.1, 0, false, 0.0); + } + continue; + } + entity = (Entity*)node->element; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + if ( MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 && bodypart == LIMB_HUMANOID_RIGHTARM ) + { + // don't let the creatures's yaw move the casting arm + } + else + { + entity->yaw = my->yaw; + } + + if ( bodypart == LIMB_HUMANOID_RIGHTLEG || bodypart == LIMB_HUMANOID_LEFTARM ) + { + my->humanoidAnimateWalk(entity, node, bodypart, MONSTER_DWALKSPEED, dist, 0.4); + } + else if ( bodypart == LIMB_HUMANOID_LEFTLEG || bodypart == LIMB_HUMANOID_RIGHTARM || bodypart == LIMB_HUMANOID_CLOAK ) + { + // left leg, right arm, cloak. + if ( bodypart == LIMB_HUMANOID_RIGHTARM ) + { + weaponarm = entity; + if ( MONSTER_ATTACK > 0 ) + { + if ( my->monsterAttack == MONSTER_POSE_RANGED_WINDUP3 ) + { + Entity* rightbody = nullptr; + // set rightbody to left leg. + node_t* rightbodyNode = list_Node(&my->children, LIMB_HUMANOID_LEFTLEG); + if ( rightbodyNode ) + { + rightbody = (Entity*)rightbodyNode->element; + } + else + { + return; + } + + if ( my->monsterAttackTime == 0 ) + { + // init rotations + weaponarm->pitch = 0; + my->monsterArmbended = 0; + my->monsterWeaponYaw = 0; + weaponarm->roll = 0; + weaponarm->skill[1] = 0; + playSoundEntityLocal(my, 170, 32); + createParticleDot(my); + if ( multiplayer != CLIENT ) + { + myStats->setEffectActive(EFF_PARALYZED, 1); + myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 40; + } + } + if ( multiplayer != CLIENT ) + { + // move the head and weapon yaw + limbAnimateToLimit(my, ANIMATE_PITCH, -0.1, 11 * PI / 6, false, 0.0); + limbAnimateToLimit(my, ANIMATE_WEAPON_YAW, 0.05, 2 * PI / 8, false, 0.0); + } + limbAnimateToLimit(weaponarm, ANIMATE_PITCH, -0.25, 7 * PI / 4, true, 0.0); + //limbAnimateToLimit(weaponarm, ANIMATE_ROLL, -0.25, 7 * PI / 4, false, 0.0); + + if ( my->monsterAttackTime >= 8 * ANIMATE_DURATION_WINDUP / (monsterGlobalAnimationMultiplier / 10.0) ) + { + if ( multiplayer != CLIENT ) + { + my->attack(MONSTER_POSE_MAGIC_WINDUP3, 0, nullptr); + } + } + } + else if ( my->monsterAttack == MONSTER_POSE_MAGIC_WINDUP3 ) + { + if ( multiplayer != CLIENT ) + { + if ( my->monsterAttackTime == 1 ) + { + int spellID = SPELL_LIGHTNING_BOLT; + + if ( my->monsterSpecialState == MONSTER_D_SPECIAL_CAST3 ) + { + real_t oldYaw = my->yaw; + real_t dist = 100.0; + if ( Entity* target = uidToEntity(my->monsterTarget) ) + { + real_t tangent = atan2(target->y - my->y, + target->x - my->x); + my->yaw = tangent; + dist = entityDist(my, target); + } + + if ( dist < TOUCHRANGE ) + { + if ( floorMagicCreateRoots(my->x, my->y, my, 0, SPELL_ROOTS, 5 * TICKS_PER_SECOND, PARTICLE_TIMER_ACTION_ROOTS1) ) + { + spawnMagicEffectParticles(my->x, my->y, my->z, 171); + playSoundEntity(my, 171, 128); + } + } + else + { + if ( floorMagicCreateRoots(my->x, my->y, my, 0, SPELL_ROOTS, 5 * TICKS_PER_SECOND, PARTICLE_TIMER_ACTION_ROOTS_PATH) ) + { + spawnMagicEffectParticles(my->x, my->y, my->z, 171); + playSoundEntity(my, 171, 128); + } + } + my->yaw = oldYaw; + } + else + { + bool setProps = false; + CastSpellProps_t props; + if ( my->monsterSpecialState == MONSTER_D_SPECIAL_CAST2 ) + { + spellID = SPELL_KINETIC_PUSH; + setProps = props.setToMonsterCast(my, spellID); + } + if ( !setProps ) + { + switch ( local_rng.rand() % 3 ) + { + case 0: + spellID = SPELL_LIGHTNING_BOLT; + break; + case 1: + spellID = SPELL_DISRUPT_EARTH; + break; + case 2: + spellID = SPELL_SLAM; + break; + default: + break; + } + setProps = props.setToMonsterCast(my, spellID); + } + + if ( setProps ) + { + castSpell(my->getUID(), getSpellFromID(spellID), true, false, false, &props); + /*if ( spellID == SPELL_ROOTS ) + { + my->setEffect(EFF_ROOTED, true, 5 * TICKS_PER_SECOND, false); + }*/ + } + } + } + } + if ( weaponarm->pitch >= 3 * PI / 2 ) + { + my->monsterArmbended = 1; + } + + if ( weaponarm->skill[1] == 0 ) + { + // chop forwards + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, 0.4, PI / 3, false, 0.0) ) + { + weaponarm->skill[1] = 1; + } + } + else if ( weaponarm->skill[1] >= 1 ) + { + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, -0.25, 7 * PI / 4, false, 0.0) ) + { + Entity* rightbody = nullptr; + // set rightbody to left leg. + node_t* rightbodyNode = list_Node(&my->children, LIMB_HUMANOID_LEFTLEG); + if ( rightbodyNode ) + { + rightbody = (Entity*)rightbodyNode->element; + } + if ( rightbody ) + { + weaponarm->skill[0] = rightbody->skill[0]; + weaponarm->pitch = rightbody->pitch; + } + my->monsterWeaponYaw = 0; + weaponarm->roll = 0; + my->monsterArmbended = 0; + my->monsterAttack = 0; + } + } + } + else + { + my->handleWeaponArmAttack(entity); + } + } + } + else if ( bodypart == LIMB_HUMANOID_CLOAK ) + { + entity->pitch = entity->fskill[0]; + } + + my->humanoidAnimateWalk(entity, node, bodypart, MONSTER_DWALKSPEED, dist, 0.4); + + if ( bodypart == LIMB_HUMANOID_CLOAK ) + { + entity->fskill[0] = entity->pitch; + entity->roll = my->roll - fabs(entity->pitch) / 2; + entity->pitch = 0; + } + } + switch ( bodypart ) + { + // torso + case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[DRYAD][1][0]; + entity->focaly = limbs[DRYAD][1][1]; + entity->focalz = limbs[DRYAD][1][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->breastplate == nullptr + || !itemModel(myStats->breastplate, my->sprite == 1514 || my->sprite == 1515, my) ) + { + entity->sprite = + my->sprite == 1514 ? 1513 : + (my->sprite == 1515 ? 1516 : + (my->sprite == 1485 ? 1511 : 1512)); + } + else + { + entity->sprite = itemModel(myStats->breastplate, my->sprite == 1514 || my->sprite == 1515, my); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, DRYAD, LIMB_HUMANOID_TORSO); + break; + // right leg + case LIMB_HUMANOID_RIGHTLEG: + entity->focalx = limbs[DRYAD][2][0]; + entity->focaly = limbs[DRYAD][2][1]; + entity->focalz = limbs[DRYAD][2][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->shoes == nullptr ) + { + entity->sprite = + my->sprite == 1514 ? 1510 : + (my->sprite == 1515 ? 1518 : + (my->sprite == 1485 ? 1508 : 1509)); + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_RIGHT_OFFSET); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, DRYAD, LIMB_HUMANOID_RIGHTLEG); + break; + // left leg + case LIMB_HUMANOID_LEFTLEG: + entity->focalx = limbs[DRYAD][3][0]; + entity->focaly = limbs[DRYAD][3][1]; + entity->focalz = limbs[DRYAD][3][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->shoes == nullptr ) + { + entity->sprite = + my->sprite == 1514 ? 1507 : + (my->sprite == 1515 ? 1517 : + (my->sprite == 1485 ? 1505 : 1506)); + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_LEFT_OFFSET); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, DRYAD, LIMB_HUMANOID_LEFTLEG); + break; + // right arm + case LIMB_HUMANOID_RIGHTARM: + { + if ( multiplayer != CLIENT ) + { + if ( myStats->gloves == nullptr ) + { + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1491 : 1493; + } + else + { + if ( setGloveSprite(myStats, entity, SPRITE_GLOVE_RIGHT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( entity->skill[7] == 0 ) + { + if ( entity->sprite == 1491 || entity->sprite == 1493 ) + { + // these are the default arms. + // chances are they may be wrong if sent by the server, + } + else + { + // otherwise we're being sent gloves armor etc so it's probably right. + entity->skill[7] = entity->sprite; + } + } + if ( entity->skill[7] == 0 ) + { + // we set this ourselves until proper initialisation. + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1491 : 1493; + } + else + { + entity->sprite = entity->skill[7]; + } + } + + node_t* weaponNode = list_Node(&my->children, 7); + if ( weaponNode ) + { + Entity* weapon = (Entity*)weaponNode->element; + if ( MONSTER_ARMBENDED || (weapon->flags[INVISIBLE] && my->monsterState == MONSTER_STATE_WAIT) ) + { + // if weapon invisible and I'm not attacking, relax arm. + entity->focalx = limbs[DRYAD][4][0]; // 0 + entity->focaly = limbs[DRYAD][4][1] - 0.25; // 0 + entity->focalz = limbs[DRYAD][4][2]; // 2 + + if ( entity->sprite == 1491 + || entity->sprite == 1493 ) + { + entity->focaly += 0.25; + } + } + else + { + // else flex arm. + entity->focalx = limbs[DRYAD][4][0] + 0.75; + entity->focaly = limbs[DRYAD][4][1]; + entity->focalz = limbs[DRYAD][4][2] - 0.75; + if ( entity->sprite == 1491 || entity->sprite == 1493 ) + { + entity->sprite += 1; + } + else + { + entity->sprite += 2; + } + } + } + my->setHumanoidLimbOffset(entity, DRYAD, LIMB_HUMANOID_RIGHTARM); + entity->yaw += MONSTER_WEAPONYAW; + break; + // left arm + } + case LIMB_HUMANOID_LEFTARM: + { + if ( multiplayer != CLIENT ) + { + if ( myStats->gloves == nullptr ) + { + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1487 : 1489; + } + else + { + if ( setGloveSprite(myStats, entity, SPRITE_GLOVE_LEFT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( entity->skill[7] == 0 ) + { + if ( entity->sprite == 1487 || entity->sprite == 1489 ) + { + // these are the default arms. + // chances are they may be wrong if sent by the server, + } + else + { + // otherwise we're being sent gloves armor etc so it's probably right. + entity->skill[7] = entity->sprite; + } + } + if ( entity->skill[7] == 0 ) + { + // we set this ourselves until proper initialisation. + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1487 : 1489; + } + else + { + entity->sprite = entity->skill[7]; + } + } + + shieldarm = entity; + node_t* shieldNode = list_Node(&my->children, 8); + if ( shieldNode ) + { + Entity* shield = (Entity*)shieldNode->element; + if ( shield->flags[INVISIBLE] && my->monsterState == MONSTER_STATE_WAIT ) + { + entity->focalx = limbs[DRYAD][5][0]; // 0 + entity->focaly = limbs[DRYAD][5][1] + .25; // 0 + entity->focalz = limbs[DRYAD][5][2]; // 2 + + if ( entity->sprite == 1487 + || entity->sprite == 1489 ) + { + entity->focaly -= 0.25; + } + } + else + { + entity->focalx = limbs[DRYAD][5][0] + 0.75; + entity->focaly = limbs[DRYAD][5][1]; + entity->focalz = limbs[DRYAD][5][2] - 0.75; + if ( entity->sprite == 1487 || entity->sprite == 1489 ) + { + entity->sprite += 1; + } + else + { + entity->sprite += 2; + } + } + } + my->setHumanoidLimbOffset(entity, DRYAD, LIMB_HUMANOID_LEFTARM); + if ( my->monsterDefend && my->monsterAttack == 0 ) + { + MONSTER_SHIELDYAW = PI / 5; + } + else + { + MONSTER_SHIELDYAW = 0; + } + entity->yaw += MONSTER_SHIELDYAW; + break; + } + // weapon + case LIMB_HUMANOID_WEAPON: + if ( multiplayer != CLIENT ) + { + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->sprite = itemModel(myStats->weapon); + if ( itemCategory(myStats->weapon) == SPELLBOOK ) + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + if ( weaponarm != nullptr ) + { + my->handleHumanoidWeaponLimb(entity, weaponarm); + } + break; + // shield + case LIMB_HUMANOID_SHIELD: + if ( multiplayer != CLIENT ) + { + if ( myStats->shield == nullptr ) + { + entity->flags[INVISIBLE] = true; + entity->sprite = 0; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(myStats->shield); + if ( itemTypeIsQuiver(myStats->shield->type) ) + { + entity->handleQuiverThirdPersonModel(*myStats, my->sprite); + } + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + my->handleHumanoidShieldLimb(entity, shieldarm); + break; + // cloak + case LIMB_HUMANOID_CLOAK: + entity->focalx = limbs[DRYAD][8][0]; + entity->focaly = limbs[DRYAD][8][1]; + entity->focalz = limbs[DRYAD][8][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(myStats->cloak); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + entity->x -= cos(my->yaw); + entity->y -= sin(my->yaw); + entity->yaw += PI / 2; + break; + // helm + case LIMB_HUMANOID_HELMET: + helmet = entity; + entity->focalx = limbs[DRYAD][9][0]; // 0 + entity->focaly = limbs[DRYAD][9][1]; // 0 + entity->focalz = limbs[DRYAD][9][2]; // -2 + entity->pitch = my->pitch; + entity->roll = 0; + if ( multiplayer != CLIENT ) + { + entity->sprite = itemModel(myStats->helmet); + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + my->setHelmetLimbOffset(entity); + break; + // mask + case LIMB_HUMANOID_MASK: + entity->focalx = limbs[DRYAD][10][0]; // 0 + entity->focaly = limbs[DRYAD][10][1]; // 0 + entity->focalz = limbs[DRYAD][10][2]; // .25 + entity->pitch = my->pitch; + entity->roll = PI / 2; + if ( multiplayer != CLIENT ) + { + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( myStats->mask != nullptr ) + { + if ( myStats->mask->type == TOOL_GLASSES ) + { + entity->sprite = 165; // GlassesWorn.vox + } + else if ( myStats->mask->type == MONOCLE ) + { + entity->sprite = 1196; // monocleWorn.vox + } + else + { + entity->sprite = itemModel(myStats->mask); + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + if ( EquipmentModelOffsets.modelOffsetExists(DRYAD, entity->sprite, my->sprite) ) + { + my->setHelmetLimbOffset(entity); + my->setHelmetLimbOffsetWithMask(helmet, entity); + } + else + { + entity->focalx = limbs[DRYAD][10][0] + .35; // .35 + entity->focaly = limbs[DRYAD][10][1] - 2; // -2 + entity->focalz = limbs[DRYAD][10][2]; // .25 + } + break; + // head left + case 12: + { + entity->focalx = limbs[DRYAD][11][0]; + entity->focaly = limbs[DRYAD][11][1]; + entity->focalz = limbs[DRYAD][11][2]; + entity->x += limbs[DRYAD][12][0] * cos(my->yaw + PI / 2) + limbs[DRYAD][12][1] * cos(my->yaw); + entity->y += limbs[DRYAD][12][0] * sin(my->yaw + PI / 2) + limbs[DRYAD][12][1] * sin(my->yaw); + entity->z += limbs[DRYAD][12][2]; + entity->pitch = my->pitch; + if ( multiplayer != CLIENT ) + { + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1501 : 1502; + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + + additionalLimb = entity; + bool moving = false; + if ( dist > 0.1 ) + { + moving = true; + } + if ( entity->skill[0] == 0 ) + { + if ( moving ) + { + entity->fskill[0] += std::min(dist * MONSTER_DWALKSPEED, 0.5 * MONSTER_DWALKSPEED); // move proportional to move speed + } + else if ( my->monsterAttack != 0 ) + { + entity->fskill[0] += 0.5 * MONSTER_DWALKSPEED; // move fixed speed when attacking if stationary + } + else + { + entity->fskill[0] += 0.01; // otherwise move slow idle + } + + if ( entity->fskill[0] > PI / 3 || ((!moving || my->monsterAttack != 0) && entity->fskill[0] > PI / 5) ) + { + // switch direction if angle too great, angle is shorter if attacking or stationary + entity->skill[0] = 1; + } + } + else // reverse of the above + { + if ( moving ) + { + entity->fskill[0] -= std::min(dist * MONSTER_DWALKSPEED, 2.f * MONSTER_DWALKSPEED); + } + else if ( my->monsterAttack != 0 ) + { + entity->fskill[0] -= MONSTER_DWALKSPEED; + } + else + { + entity->fskill[0] -= 0.007; + } + + if ( entity->fskill[0] < -PI / 32 ) + { + entity->skill[0] = 0; + } + } + //entity->yaw += -entity->fskill[0] / 4; + entity->pitch -= entity->fskill[0] / 2; + entity->z -= 0.25 * sin(entity->fskill[0] * 5); + entity->roll = -entity->fskill[0] / 8; + entity->scalez = 1.00 + 0.1 * sin(entity->fskill[0]); + } + break; + // head right + case 13: + { + entity->focalx = limbs[DRYAD][13][0]; + entity->focaly = limbs[DRYAD][13][1]; + entity->focalz = limbs[DRYAD][13][2]; + entity->x += limbs[DRYAD][14][0] * cos(my->yaw + PI / 2) + limbs[DRYAD][14][1] * cos(my->yaw); + entity->y += limbs[DRYAD][14][0] * sin(my->yaw + PI / 2) + limbs[DRYAD][14][1] * sin(my->yaw); + entity->z += limbs[DRYAD][14][2]; + entity->pitch = my->pitch; + if ( multiplayer != CLIENT ) + { + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1503 : 1504; + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + + if ( additionalLimb ) // follow the yaw of the previous limb. + { + //entity->yaw -= -additionalLimb->fskill[0] / 4; + entity->pitch -= additionalLimb->fskill[0] / 2; + entity->z -= 0.25 * sin(additionalLimb->fskill[0] * 5); + entity->roll = additionalLimb->fskill[0] / 8; + entity->scalez = 1.00 + 0.1 * sin(additionalLimb->fskill[0]); + } + } + break; + // head center + case 14: + entity->focalx = limbs[DRYAD][15][0]; + entity->focaly = limbs[DRYAD][15][1]; + entity->focalz = limbs[DRYAD][15][2]; + entity->x += limbs[DRYAD][16][0] * cos(my->yaw + PI / 2) + limbs[DRYAD][16][1] * cos(my->yaw); + entity->y += limbs[DRYAD][16][0] * sin(my->yaw + PI / 2) + limbs[DRYAD][16][1] * sin(my->yaw); + entity->z += limbs[DRYAD][16][2]; + entity->pitch = my->pitch; + /*if ( keystatus[SDLK_h] ) + { + entity->fskill[0] += 0.05; + }*/ + //entity->scalex = 1.0 + std::max(0.0, 0.05 * sin(entity->fskill[0])); + //entity->scaley = 1.0 + std::max(0.0, 0.05 * sin(entity->fskill[0])); + //entity->scalez = 1.008 + 0.05 * sin(entity->fskill[0]); + entity->scalez = 1.008 + 0.05 * sin(additionalLimb->fskill[0]); + entity->pitch -= 0.05 * sin(std::max(0.0, additionalLimb->fskill[0]) / 10); + entity->z -= 0.05 * sin(additionalLimb->fskill[0]); + if ( multiplayer != CLIENT ) + { + int stage = 0; + /*if ( keystatus[SDLK_g] ) + { + keystatus[SDLK_g] = 0; + stage += 1; + if ( stage > 2 ) + { + stage = 0; + } + }*/ + if ( stage == 0 ) + { + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1495 : 1496; + } + else if ( stage == 1 ) + { + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1497 : 1498; + } + else if ( stage == 2 ) + { + entity->sprite = (my->sprite == 1485 || my->sprite == 1514) ? 1499 : 1500; + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring + || (helmet && helmet->sprite > 0 && !helmet->flags[INVISIBLE])) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + break; + } + } + // rotate shield a bit + node_t* shieldNode = list_Node(&my->children, 8); + if ( shieldNode ) + { + Entity* shieldEntity = (Entity*)shieldNode->element; + if ( shieldEntity->sprite != items[TOOL_TORCH].index && shieldEntity->sprite != items[TOOL_LANTERN].index && shieldEntity->sprite != items[TOOL_CRYSTALSHARD].index ) + { + shieldEntity->yaw -= PI / 6; + } + } + if ( MONSTER_ATTACK > 0 && MONSTER_ATTACK <= MONSTER_POSE_MAGIC_CAST3 ) + { + MONSTER_ATTACKTIME++; + } + else if ( MONSTER_ATTACK == 0 ) + { + MONSTER_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } +} + +void Entity::monsterDChooseWeapon(const Entity* target, double dist) +{ + Stat* myStats = getStats(); + if ( !myStats ) + { + return; + } + + if ( myStats->getAttribute("monster_d_type") == "watcher" ) + { + if ( monsterStrafeDirection == 0 ) + { + if ( dist < 64.0 ) + { + if ( local_rng.rand() % 3 == 0 ) + { + monsterStrafeDirection = TICKS_PER_SECOND * 5; + } + } + } + else + { + if ( monsterStrafeDirection > 0 ) + { + monsterStrafeDirection = std::max(0, monsterStrafeDirection - 1); + if ( monsterStrafeDirection == 0 ) + { + monsterStrafeDirection = -TICKS_PER_SECOND * 5; // cooldown direction + } + } + else if ( monsterStrafeDirection < 0 ) + { + monsterStrafeDirection = std::min(0, monsterStrafeDirection + 1); + } + } + } + + if ( monsterSpecialState != 0 || monsterSpecialTimer != 0 || monsterAttack != 0 ) + { + return; + } + + int roll = 4; + if ( target && target->hasRangedWeapon() && dist > TOUCHRANGE * 1.5 ) + { + roll = 2; + } + + if ( local_rng.rand() % roll == 0 + && monsterStrafeDirection <= 0 + && dist > TOUCHRANGE + && myStats && myStats->getAttribute("monster_d_type") == "watcher" ) + { + monsterSpecialState = MONSTER_D_SPECIAL_CAST1; + } + else + { + if ( myStats && myStats->weapon && myStats->weapon->type == BRANCH_STAFF ) + { + if ( dist < 48.0 ) + { + int roll = 5; + if ( target && dist < TOUCHRANGE ) + { + roll = 3; + } + if ( local_rng.rand() % roll == 0 ) + { + monsterSpecialState = MONSTER_D_SPECIAL_CAST2; // push + } + } + } + + if ( monsterSpecialState == 0 ) + { + if ( target && dist < 64.0 ) + { + int roll = 5; + if ( local_rng.rand() % roll == 0 ) + { + monsterSpecialState = MONSTER_D_SPECIAL_CAST3; + } + } + } + } +} + +//void Entity::goatmanChooseWeapon(const Entity* target, double dist) +//{ +// if ( monsterSpecialState != 0 ) +// { +// //Holding a weapon assigned from the special attack. Don't switch weapons. +// //messagePlayer() +// return; +// } +// +// //TODO: I don't like this function getting called every frame. Find a better place to put it. +// //Although if I do that, can't do this dirty little hack for the goatman's special... +// +// //TODO: If applying attack animations that will involve holding a potion for several frames while this code has a chance to run, do a check here to cancel the function if holding a potion. +// +// Stat *myStats = getStats(); +// if ( !myStats ) +// { +// return; +// } +// +// if ( myStats->weapon && (itemCategory(myStats->weapon) == SPELLBOOK) ) +// { +// return; +// } +// +// int specialRoll = -1; +// bool usePotionSpecial = false; +// +// /* +// * For the goatman's special: +// * * If specialRoll == 0, want to use a booze or healing potion (prioritize healing potion if damaged enough). +// * * If no have potion, try to use THROWN in melee. +// * * If in melee, if potion is not a healing potion, check if have any THROWN and then 50% chance to use those instead. +// */ +// +// node_t* hasPotion = nullptr; +// bool isHealingPotion = false; +// +// if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 ) +// { +// //messagePlayer(clientnum, "Cooldown done!"); +// specialRoll = local_rng.rand()%10; +// +// if ( specialRoll == 0 ) +// { +// if ( myStats->HP <= myStats->MAXHP / 3 * 2 ) +// { +// //Try to get a health potion. +// hasPotion = itemNodeInInventory(myStats, POTION_EXTRAHEALING, static_cast(-1)); +// if ( !hasPotion ) +// { +// hasPotion = itemNodeInInventory(myStats, POTION_HEALING, static_cast(-1)); +// if ( hasPotion ) +// { +// //Equip and chuck it now. +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, hasPotion, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap healing potion into hand!"); +// //Don't return, want to try equipping either a potion of booze, or one of the other weapon routes (e.h. a THROWN special if in melee or just an axe if worst comes to worst). +// } +// else +// { +// monsterSpecialState = GOATMAN_POTION; +// //monsterHitTime = 2 * HITRATE; +// return; +// } +// } +// } +// else +// { +// //Equip and chuck it now. +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, hasPotion, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap healing potion into hand!"); +// //Don't return, want to try equipping either a potion of booze, or one of the other weapon routes (e.h. a THROWN special if in melee or just an axe if worst comes to worst). +// } +// else +// { +// monsterSpecialState = GOATMAN_POTION; +// //monsterHitTime = 2 * HITRATE; +// return; +// } +// } +// } +// +// if ( !hasPotion ) +// { +// //Couldn't find a healing potion? Try for a potion of booze. +// hasPotion = itemNodeInInventory(myStats, POTION_BOOZE, static_cast(-1)); +// if ( hasPotion ) +// { +// //Equip and chuck it now. +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, hasPotion, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap healing potion into hand!"); +// //Don't return, want to try equipping either a potion of booze, or one of the other weapon routes (e.h. a THROWN special if in melee or just an axe if worst comes to worst). +// } +// else +// { +// monsterSpecialState = GOATMAN_POTION; +// //monsterHitTime = 2 * HITRATE; +// return; +// } +// } +// } +// } +// } +// +// bool inMeleeRange = monsterInMeleeRange(target, dist); +// +// if ( inMeleeRange ) +// { +// if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 && specialRoll == 0 ) +// { +// bool tryChakram = true; +// if ( hasPotion && local_rng.rand()%10 ) +// { +// tryChakram = false; +// } +// +// if ( tryChakram ) +// { +// //Grab a chakram instead. +// node_t* thrownNode = itemNodeInInventory(myStats, -1, THROWN); +// if ( thrownNode ) +// { +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, thrownNode, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap THROWN into hand! Cursed? (%d)", myStats->weapon->beatitude); +// //Don't return, make sure holding a melee weapon at least. +// } +// else +// { +// monsterSpecialState = GOATMAN_THROW; +// return; +// } +// } +// } +// } +// +// //Switch to a melee weapon if not already wielding one. Unless monster special state is overriding the AI. +// if ( !myStats->weapon || !isMeleeWeapon(*myStats->weapon) ) +// { +// node_t* weaponNode = getMeleeWeaponItemNodeInInventory(myStats); +// if ( !weaponNode ) +// { +// if ( myStats->weapon && myStats->weapon->type == MAGICSTAFF_SLOW ) +// { +// monsterUnequipSlotFromCategory(myStats, &myStats->weapon, MAGICSTAFF); +// } +// return; //Resort to fists. +// } +// +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap melee weapon into hand! Cursed? (%d)", myStats->weapon->beatitude); +// //Don't return so that monsters will at least equip ranged weapons in melee range if they don't have anything else. +// } +// else +// { +// return; +// } +// } +// else +// { +// return; +// } +// } +// +// //if ( hasPotion ) +// //{ +// // //Try to equip the potion first. If fails, then equip normal ranged. +// // bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, hasPotion, false, false); +// // if ( !swapped ) +// // { +// // printlog("Error in Entity::goatmanChooseWeapon(): failed to swap non-healing potion into hand! (non-melee block) Cursed? (%d)", myStats->weapon->beatitude); +// // } +// // else +// // { +// // monsterSpecialState = GOATMAN_POTION; +// // return; +// // } +// //} +// +// //Switch to a thrown weapon or a ranged weapon. Potions are reserved as a special attack. +// if ( !myStats->weapon || isMeleeWeapon(*myStats->weapon) ) +// { +// //First search the inventory for a THROWN weapon. +// node_t *weaponNode = nullptr; +// if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 && local_rng.rand() % 10 == 0 ) +// { +// weaponNode = itemNodeInInventory(myStats, -1, THROWN); +// if ( weaponNode ) +// { +// if ( swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, false) ) +// { +// monsterSpecialState = GOATMAN_THROW; +// return; +// } +// } +// } +// if ( !weaponNode ) +// { +// //If couldn't find any, search the inventory for a ranged weapon. +// weaponNode = getRangedWeaponItemNodeInInventory(myStats, true); +// } +// +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, false); +// return; +// } +// +// return; +//} +// +//bool Entity::goatmanCanWieldItem(const Item& item) const +//{ +// Stat* myStats = getStats(); +// if ( !myStats ) +// { +// return false; +// } +// +// if ( monsterAllyIndex >= 0 && (monsterAllyClass != ALLY_CLASS_MIXED || item.interactNPCUid == getUID()) ) +// { +// return monsterAllyEquipmentInClass(item); +// } +// +// switch ( itemCategory(&item) ) +// { +// case WEAPON: +// return true; +// case POTION: +// switch ( item.type ) +// { +// case POTION_BOOZE: +// return true; +// case POTION_HEALING: +// return true; +// default: +// return false; +// } +// break; +// case TOOL: +// if ( itemTypeIsQuiver(item.type) ) +// { +// return true; +// } +// break; +// case THROWN: +// return true; +// case ARMOR: +// { //Little baby compiler stop whining, wah wah. +// int equipType = checkEquipType(&item); +// if ( equipType == TYPE_HAT || equipType == TYPE_HELM ) +// { +// return false; //No can wear hats, because horns. +// } +// return true; //Can wear all other armor. +// } +// default: +// return false; +// } +// +// return false; +//} +// + + + + diff --git a/src/monster_demon.cpp b/src/monster_demon.cpp index a679a9fac..459df9cc9 100644 --- a/src/monster_demon.cpp +++ b/src/monster_demon.cpp @@ -278,7 +278,7 @@ void demonMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -329,6 +329,9 @@ void demonMoveBodyparts(Entity* my, Stat* myStats, double dist) bodypart++; } } + + my->z = -8.5; + my->creatureHandleLiftZ(); } //Move bodyparts @@ -653,7 +656,7 @@ void actDemonCeilingBuster(Entity* my) if ( myStats ) { // easy hack to stop the demon while he breaks stuff - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = TICKS_PER_SECOND / 2; } } diff --git a/src/monster_devil.cpp b/src/monster_devil.cpp index 614d1eb32..7a072e033 100644 --- a/src/monster_devil.cpp +++ b/src/monster_devil.cpp @@ -269,7 +269,7 @@ void devilMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; diff --git a/src/monster_duck.cpp b/src/monster_duck.cpp new file mode 100644 index 000000000..328d9a7fd --- /dev/null +++ b/src/monster_duck.cpp @@ -0,0 +1,2043 @@ +/*------------------------------------------------------------------------------- + + BARONY + File: monster_sentrybot.cpp + Desc: implements all of the kobold monster's code + + Copyright 2013-2019 (c) Turning Wheel LLC, all rights reserved. + See LICENSE for details. + +-------------------------------------------------------------------------------*/ + +#include "main.hpp" +#include "game.hpp" +#include "stat.hpp" +#include "entity.hpp" +#include "items.hpp" +#include "monster.hpp" +#include "engine/audio/sound.hpp" +#include "book.hpp" +#include "net.hpp" +#include "collision.hpp" +#include "player.hpp" +#include "magic/magic.hpp" +#include "interface/interface.hpp" +#include "prng.hpp" +#include "mod_tools.hpp" +#include "paths.hpp" + +void initDuck(Entity* my, Stat* myStats) +{ + node_t* node; + + bool spiritDuck = my && my->behavior == &actDeathGhostLimb; + + my->z = 0; + + int sprite = 2225; + int appearance = 0; + if ( myStats && myStats->getAttribute("duck_type") != "" ) + { + int duckType = std::stoi(myStats->getAttribute("duck_type")); + if ( duckType >= MAXPLAYERS && duckType < 2 * MAXPLAYERS ) + { + sprite = 2231; + appearance = 1; + } + else if ( duckType >= 2 * MAXPLAYERS && duckType < 3 * MAXPLAYERS ) + { + sprite = 2237; + appearance = 2; + } + else if ( duckType >= 3 * MAXPLAYERS && duckType < 4 * MAXPLAYERS ) + { + sprite = 2307; + appearance = 3; + } + } + + if ( !spiritDuck ) + { + my->initMonster(sprite); + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + } + if ( multiplayer != CLIENT && myStats ) + { + MONSTER_SPOTSND = -1; + MONSTER_SPOTVAR = 1; + MONSTER_IDLESND = 789; + MONSTER_IDLEVAR = 5; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT && myStats ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + } + } + + // body + Entity* entity = newEntity(appearance == 3 ? 2308 : 2226 + appearance * 6, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + //entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DUCK_SMALL][1][0]; + entity->focaly = limbs[DUCK_SMALL][1][1]; + entity->focalz = limbs[DUCK_SMALL][1][2]; + entity->behavior = &actDuckLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + if ( spiritDuck ) + { + entity->skill[2] = my->skill[2]; + entity->behavior = my->behavior; + entity->flags[GENIUS] = true; + } + my->bodyparts.push_back(entity); + + // body sit + entity = newEntity(appearance == 3 ? 2307 : 2225 + appearance * 6, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + //entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DUCK_SMALL][2][0]; + entity->focaly = limbs[DUCK_SMALL][2][1]; + entity->focalz = limbs[DUCK_SMALL][2][2]; + entity->behavior = &actDuckLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + if ( spiritDuck ) + { + entity->skill[2] = my->skill[2]; + entity->behavior = my->behavior; + entity->flags[GENIUS] = true; + } + my->bodyparts.push_back(entity); + + // wingleft + entity = newEntity(appearance == 3 ? 2309 : 2227 + appearance * 6, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + //entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DUCK_SMALL][3][0]; + entity->focaly = limbs[DUCK_SMALL][3][1]; + entity->focalz = limbs[DUCK_SMALL][3][2]; + entity->behavior = &actDuckLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + if ( spiritDuck ) + { + entity->skill[2] = my->skill[2]; + entity->behavior = my->behavior; + entity->flags[GENIUS] = true; + } + my->bodyparts.push_back(entity); + + // wingright + entity = newEntity(appearance == 3 ? 2310 : 2228 + appearance * 6, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + //entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DUCK_SMALL][4][0]; + entity->focaly = limbs[DUCK_SMALL][4][1]; + entity->focalz = limbs[DUCK_SMALL][4][2]; + entity->behavior = &actDuckLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + if ( spiritDuck ) + { + entity->skill[2] = my->skill[2]; + entity->behavior = my->behavior; + entity->flags[GENIUS] = true; + } + my->bodyparts.push_back(entity); + + // leg left + entity = newEntity(2229, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + //entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DUCK_SMALL][20][0]; + entity->focaly = limbs[DUCK_SMALL][20][1]; + entity->focalz = limbs[DUCK_SMALL][20][2]; + entity->behavior = &actDuckLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + if ( spiritDuck ) + { + entity->skill[2] = my->skill[2]; + entity->behavior = my->behavior; + entity->flags[GENIUS] = true; + } + my->bodyparts.push_back(entity); + + // leg right + entity = newEntity(2230, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + //entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[DUCK_SMALL][22][0]; + entity->focaly = limbs[DUCK_SMALL][22][1]; + entity->focalz = limbs[DUCK_SMALL][22][2]; + entity->behavior = &actDuckLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + if ( spiritDuck ) + { + entity->skill[2] = my->skill[2]; + entity->behavior = my->behavior; + entity->flags[GENIUS] = true; + } + my->bodyparts.push_back(entity); +} + +void actDuckLimb(Entity* my) +{ + my->actMonsterLimb(false); +} + +void duckSpawnFeather(int sprite, real_t x, real_t y, real_t z, Entity* my) +{ + int featherSprite = 2249; + if ( sprite == 2225 || sprite == 2226 ) + { + featherSprite = 2249; + } + else if ( sprite == 2231 || sprite == 2232 ) + { + featherSprite = 2250; + } + else if ( sprite == 2237 || sprite == 2238 ) + { + featherSprite = 2251; + } + else if ( sprite == 2307 || sprite == 2308 ) + { + featherSprite = 2314; + } + real_t yawOffset = ((local_rng.rand() % 8) / 4.0) * PI; + for ( int i = 0; i < 3; ++i ) + { + real_t leafEndZ = z - 7.5; + Entity* leaf = newEntity(featherSprite, 1, map.entities, nullptr); //Gib entity. + if ( leaf != NULL ) + { + leaf->x = x; + leaf->y = y; + leaf->z = z - i * 0.5; + leaf->fskill[6] = leaf->z; + leaf->fskill[7] = leafEndZ - leaf->fskill[6]; + leaf->vel_z = 0.0; + leaf->yaw = yawOffset + i * 2 * PI / 3; + leaf->sizex = 2; + leaf->sizey = 2; + leaf->scalex = 1.0; + leaf->scaley = 1.0; + leaf->scalez = 1.0; + leaf->fskill[4] = x; + leaf->fskill[5] = y; + leaf->fskill[9] = i * 2 * PI / 3; + leaf->parent = 0; + leaf->skill[0] = TICKS_PER_SECOND * 15; + leaf->behavior = &actLeafParticle; + leaf->flags[NOCLIP_CREATURES] = true; + leaf->flags[UPDATENEEDED] = false; + leaf->flags[NOUPDATE] = true; + leaf->flags[PASSABLE] = true; + leaf->flags[UNCLICKABLE] = true; + if ( multiplayer != CLIENT ) + { + --entity_uids; + } + leaf->setUID(-3); + if ( my ) + { + leaf->ditheringOverride = my->ditheringOverride; + leaf->mistformGLRender = my->mistformGLRender; + } + } + } +} + +void duckDie(Entity* my) +{ + //int c; + //for ( c = 0; c < 4; c++ ) + //{ + // Entity* entity = spawnGib(my); + // if ( entity ) + // { + // entity->skill[5] = 1; // poof + + // switch ( c ) + // { + // case 0: + // entity->sprite = 1408; + // break; + // case 1: + // entity->sprite = 1409; + // break; + // case 2: + // entity->sprite = 1410; + // break; + // case 3: + // entity->sprite = 1411; + // break; + // default: + // break; + // } + + // serverSpawnGibForClient(entity); + // } + //} + + //my->spawnBlood(); + + //playSoundEntity(my, 670 + local_rng.rand() % 2, 128); + + spawnPoof(my->x, my->y, 7.5, 0.5, true); + duckSpawnFeather(my->sprite, my->x, my->y, my->z, my); + serverSpawnMiscParticlesAtLocation(my->x, my->y, my->z, PARTICLE_EFFECT_DUCK_SPAWN_FEATHER, my->sprite); + + my->removeMonsterDeathNodes(); + + list_RemoveNode(my->mynode); + return; +} + +#define DUCK_BODY 2 +#define DUCK_HEAD 3 +#define DUCK_LEFTWING 4 +#define DUCK_RIGHTWING 5 +#define DUCK_LEFTLEG 6 +#define DUCK_RIGHTLEG 7 + +#define DUCK_FLOAT_X body->fskill[2] +#define DUCK_FLOAT_Y body->fskill[3] +#define DUCK_FLOAT_Z body->fskill[4] +#define DUCK_FLOAT_ATK body->fskill[5] +#define DUCK_LAST_SPECIAL_STATE body->skill[4] +#define DUCK_SPECIAL_TIMER body->skill[5] +#define DUCK_INWATER body->skill[6] +#define DUCK_INIT body->skill[7] +#define DUCK_FLOAT_ATK_DIVE body->fskill[8] +#define DUCK_FLOAT_Z_MULT body->fskill[9] +#define DUCK_DIVE_ANIM body->fskill[10] +#define DUCK_INERT_ANIM body->fskill[11] +#define DUCK_INERT_ANIM_COMPLETE body->fskill[12] +#define DUCK_WALK_CYCLE body->fskill[13] +#define DUCK_WALK_CYCLE_ANIM body->fskill[14] +#define DUCK_WALK_CYCLE_ANIM2 body->fskill[15] +#define DUCK_WALK_CYCLE2 body->fskill[16] +#define DUCK_BOB_WATER body->fskill[17] +#define DUCK_CAM_Z body->fskill[18] + +void actWaterSplash(Entity* my) +{ + if ( my->skill[0] <= 0 ) + { + list_RemoveNode(my->mynode); + return; + } + --my->skill[0]; + if ( my->sprite == 2246 ) + { + my->z = std::max(8.05, my->z - 0.1); + } + if ( my->sprite == 2247 && my->ticks > 20 ) + { + my->z += 0.1; + } + if ( my->ticks % 10 == 0 ) + { + if ( my->sprite == 2246 ) + { + my->sprite = 2247; + my->z += 0.2; + } + } +} + +void actWaterSplashParticle(Entity* my) +{ + if ( my->skill[0] <= 0 ) + { + list_RemoveNode(my->mynode); + return; + } + --my->skill[0]; + my->x += my->vel_x; + my->y += my->vel_y; + my->z += my->vel_z; + my->vel_z += 0.04; +} + +void createWaterSplash(real_t x, real_t y, int lifetime) +{ + { + Entity* splash = newEntity(2246, 1, map.entities, nullptr); //Gib entity. + real_t centerx = static_cast(x / 16) * 16.0 + 8.0; + real_t centery = static_cast(y / 16) * 16.0 + 8.0; + splash->x = std::max(-2.5, std::min(2.5, (x - centerx))) + centerx; + splash->y = std::max(-2.5, std::min(2.5, (y - centery))) + centery; + splash->z = 8.75; + splash->yaw = 0.0; + splash->skill[0] = lifetime; + splash->behavior = &actWaterSplash; + splash->flags[UPDATENEEDED] = false; + splash->flags[NOUPDATE] = true; + splash->flags[PASSABLE] = true; + splash->flags[UNCLICKABLE] = true; + if ( multiplayer != CLIENT ) + { + --entity_uids; + } + splash->setUID(-3); + } + + real_t offsetYaw = ((local_rng.rand() % 9) / 8.0) * PI / 4; + for ( int i = 0; i < 4; ++i ) + { + Entity* splashParticle = newEntity(2248, 1, map.entities, nullptr); //Gib entity. + splashParticle->yaw = offsetYaw + i * (PI / 2); + splashParticle->x = static_cast(x / 16) * 16.0 + 8.0 + 2.0 * cos(splashParticle->yaw); + splashParticle->y = static_cast(y / 16) * 16.0 + 8.0 + 2.0 * sin(splashParticle->yaw); + splashParticle->z = 7.5; + splashParticle->pitch = (local_rng.rand() % 360) * PI / 180.0; + splashParticle->roll = (local_rng.rand() % 360) * PI / 180.0; + splashParticle->vel_x = 0.25 * cos(splashParticle->yaw); + splashParticle->vel_y = 0.25 * sin(splashParticle->yaw); + splashParticle->vel_z = -0.5 + 0.4 * (local_rng.rand() % 11) / 10.0; + splashParticle->skill[0] = 50; + splashParticle->behavior = &actWaterSplashParticle; + splashParticle->flags[UPDATENEEDED] = false; + splashParticle->flags[NOUPDATE] = true; + splashParticle->flags[PASSABLE] = true; + splashParticle->flags[UNCLICKABLE] = true; + if ( multiplayer != CLIENT ) + { + --entity_uids; + } + splashParticle->setUID(-3); + } +} + +bool duckAreaQuck(Entity* my) +{ + if ( !my ) { return false; } + + Entity* caster = my; + if ( my->behavior == &actDeathGhost ) + { + caster = nullptr; + if ( my->skill[2] >= 0 && my->skill[2] < MAXPLAYERS ) + { + if ( players[my->skill[2]]->entity ) + { + caster = players[my->skill[2]]->entity; + } + } + if ( !caster ) + { + return false; + } + } + else if ( my->behavior != &actMonster ) + { + return false; + } + + bool anyTarget = false; + std::priority_queue> possibleTargets; + for ( auto node = map.creatures->first; node; node = node->next ) + { + if ( Entity* target = (Entity*)node->element ) + { + if ( target->monsterIsTargetable() && entityDist(target, my) < 2 * TOUCHRANGE ) + { + if ( caster->checkEnemy(target) || (my->behavior == &actMonster && target->getUID() == my->monsterTarget) ) + { + //if ( Entity* target = uidToEntity(monsterTarget) ) + { + if ( Stat* targetStats = target->getStats() ) + { + if ( !targetStats->getEffectActive(EFF_DISORIENTED) + && !targetStats->getEffectActive(EFF_DISTRACTED_COOLDOWN) + && target->behavior == &actMonster && target->isMobile() + && !monsterIsImmobileTurret(target, targetStats) + && !target->isBossMonster() && targetStats && !uidToEntity(targetStats->leader_uid) ) + { + //if ( /*(entity->monsterState == MONSTER_STATE_WAIT || entity->monsterTarget == 0) || */ + // (entityDist(target, this) < 2 * TOUCHRANGE /*&& (Uint32)(target->monsterLastDistractedByNoisemaker) != this->getUID()*/) ) + { + real_t tangent = atan2(target->y - my->y, target->x - my->x); + lineTraceTarget(my, my->x, my->y, tangent, 32.0, 0, false, target); + if ( hit.entity == target ) + { + possibleTargets.push(std::make_pair(-entityDist(target, my), target)); + } + } + } + } + } + } + } + } + } + + int numEffected = 0; + while ( possibleTargets.size() ) + { + Entity* target = possibleTargets.top().second; + possibleTargets.pop(); + if ( my->behavior == &actDeathGhost ) + { + if ( numEffected >= 3 ) + { + break; + } + if ( numEffected >= 2 ) + { + if ( local_rng.rand() % 2 == 0 ) + { + continue; + } + } + } + + if ( target->monsterSetPathToLocation(my->x / 16, my->y / 16, 2, + GeneratePathTypes::GENERATE_PATH_DEFAULT) && target->children.first ) + { + target->monsterLastDistractedByNoisemaker = my->getUID(); + target->monsterTarget = my->getUID(); + target->monsterState = MONSTER_STATE_HUNT; // hunt state + serverUpdateEntitySkill(target, 0); + + + if ( my->behavior == &actDeathGhost ) + { + if ( target->setEffect(EFF_DISORIENTED, true, 2 * TICKS_PER_SECOND, false) ) + { + anyTarget = true; + ++numEffected; + target->setEffect(EFF_DISTRACTED_COOLDOWN, true, TICKS_PER_SECOND * 2 + 25, false); + spawnFloatingSpriteMisc(134, target->x + (-4 + local_rng.rand() % 9) + cos(target->yaw) * 2, + target->y + (-4 + local_rng.rand() % 9) + sin(target->yaw) * 2, target->z + local_rng.rand() % 4); + } + } + else + { + if ( target->setEffect(EFF_DISORIENTED, true, 2 * TICKS_PER_SECOND, false) ) + { + anyTarget = true; + ++numEffected; + target->setEffect(EFF_DISTRACTED_COOLDOWN, true, TICKS_PER_SECOND * 2 + 25, false); + if ( my->behavior == &actMonster ) + { + if ( Stat* myStats = my->getStats() ) + { + int owner = achievementObserver.checkUidIsFromPlayer(myStats->leader_uid); + if ( owner >= 0 ) + { + Compendium_t::Events_t::eventUpdate(owner, Compendium_t::CPDM_SPELL_TARGETS, TOOL_DUCK, 1); + } + } + } + } + } + } + } + + if ( anyTarget || my->behavior == &actDeathGhost ) + { + playSoundEntity(my, 784 + local_rng.rand() % 2, 128); + spawnDamageGib(my, 198, DamageGib::DMG_STRONGEST, DamageGibDisplayType::DMG_GIB_SPRITE, true); + } + + return anyTarget; +} + +void duckAnimate(Entity* my, Stat* myStats, double dist) +{ + node_t* node; + Entity* entity = nullptr; + int bodypart; + + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + my->flags[PASSABLE] = true; + my->flags[NOCLIP_CREATURES] = true; + + my->sizex = 4; + my->sizey = 4; + + my->focalx = limbs[DUCK_SMALL][0][0]; + my->focaly = limbs[DUCK_SMALL][0][1]; + my->focalz = limbs[DUCK_SMALL][0][2]; + + bool spiritDuck = my->behavior == &actDeathGhostLimb; + if ( spiritDuck ) + { + my->z = limbs[DUCK_SMALL][5][2]; + } + if ( multiplayer != CLIENT ) + { + my->z = limbs[DUCK_SMALL][5][2]; + if ( myStats && !myStats->getEffectActive(EFF_LEVITATING) ) + { + myStats->setEffectActive(EFF_LEVITATING, 1); + myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; + } + + if ( myStats ) + { + if ( !my->isMobile() ) + { + my->monsterRotate(); + } + + my->creatureHandleLiftZ(); + + if ( myStats->getAttribute("duck_time") != "" ) + { + int lifetime = std::stoi(myStats->getAttribute("duck_time")); + bool ghostActive = false; + if ( Entity* leader = uidToEntity(myStats->leader_uid) ) + { + if ( leader->behavior == &actPlayer && players[leader->skill[2]]->ghost.isActive() ) + { + ghostActive = true; + } + } + if ( my->monsterSpecialState != DUCK_DIVE && !ghostActive ) + { + --lifetime; + if ( !uidToEntity(myStats->leader_uid) ) + { + --lifetime; + } + } + lifetime = std::max(0, lifetime); + myStats->setAttribute("duck_time", std::to_string(lifetime)); + if ( lifetime <= 0 ) + { + if ( my->monsterSpecialState != DUCK_RETURN ) + { + my->monsterSpecialState = DUCK_RETURN; + serverUpdateEntitySkill(my, 33); + myStats->setEffectActive(EFF_STUNNED, 1); + myStats->EFFECTS_TIMERS[EFF_STUNNED] = 0; + my->flags[NOCLIP_WALLS] = true; + playSoundEntity(my, 794 + local_rng.rand() % 2, 128); + //playSoundEntity(my, 786 + local_rng.rand() % 3, 128); + } + } + } + } + } + + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) && myStats ) + { + if ( keystatus[SDLK_KP_5] ) + { + keystatus[SDLK_KP_5] = 0; + static real_t dir = 0.0; + my->yaw = dir; + my->monsterLookDir = my->yaw; + if ( keystatus[SDLK_LSHIFT] ) + { + dir += PI / 2; + } + else + { + dir += 0.1; + } + + createWaterSplash(my->x, my->y, 30); + playSoundEntityLocal(my, 136, 64); + } + if ( keystatus[SDLK_g] ) + { + keystatus[SDLK_g] = 0; + if ( my->monsterSpecialState == 0 ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + my->monsterSpecialState = DUCK_DIVE; + } + else + { + my->monsterSpecialState = DUCK_INERT; + } + } + else if ( my->monsterSpecialState == 2 ) + { + if ( keystatus[SDLK_LSHIFT] ) + { + my->monsterSpecialState = 0; + } + else + { + my->monsterSpecialState = DUCK_DIVE; + } + } + else + { + if ( keystatus[SDLK_LSHIFT] ) + { + my->monsterSpecialState = 0; + } + else + { + my->monsterSpecialState = DUCK_INERT; + } + } + //my->monsterSpecialState = 1; + } + if ( keystatus[SDLK_h] ) + { + keystatus[SDLK_h] = 0; + myStats->setEffectValueUnsafe(EFF_STUNNED, myStats->getEffectActive(EFF_STUNNED) ? 0 : 1); + myStats->EFFECTS_TIMERS[EFF_STUNNED] = myStats->getEffectActive(EFF_STUNNED) ? -1 : 0; + } + if ( keystatus[SDLK_j] ) + { + keystatus[SDLK_j] = 0; + //MONSTER_ATTACK = local_rng.rand() % 2 ? MONSTER_POSE_MELEE_WINDUP2 : MONSTER_POSE_MELEE_WINDUP3; + //MONSTER_ATTACKTIME = 0; + my->attack(local_rng.rand() % 2 ? MONSTER_POSE_MELEE_WINDUP2 : MONSTER_POSE_MELEE_WINDUP3, 0, nullptr); + } + } + + Entity* body = nullptr; + Entity* head = nullptr; + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, ++bodypart ) + { + if ( bodypart < DUCK_BODY ) + { + continue; + } + entity = (Entity*)node->element; + if ( bodypart == DUCK_HEAD ) + { + head = entity; + } + else if ( bodypart == DUCK_BODY ) + { + body = entity; + } + } + + bool inWater = false; + bool waterTile = false; + bool noFloor = false; + bool lavaTile = false; + bool safeTile = false; + real_t inertHeight = 7.5; + int mapx = my->x / 16; + int mapy = my->y / 16; + if ( body ) + { + if ( mapx >= 0 && mapx < map.width && mapy >= 0 && mapy < map.height ) + { + int mapIndex = (mapy)*MAPLAYERS + (mapx)*MAPLAYERS * map.height; + if ( !map.tiles[mapIndex] ) + { + noFloor = true; + } + if ( map.tiles[mapIndex] && lavatiles[map.tiles[mapIndex]] ) + { + lavaTile = true; + } + if ( !map.tiles[OBSTACLELAYER + mapIndex] && map.tiles[mapIndex] + && swimmingtiles[map.tiles[mapIndex]] ) + { + waterTile = true; + inertHeight += 0.6; + } + if ( (!lavaTile && !noFloor) || swimmingtiles[map.tiles[mapIndex]] ) + { + safeTile = true; + } + } + } + + if ( multiplayer != CLIENT ) + { + if ( !waterTile && my->monsterSpecialState == DUCK_DIVE ) + { + my->monsterSpecialState = 0; + serverUpdateEntitySkill(my, 33); + } + if ( (lavaTile || noFloor) && !waterTile ) + { + if ( my->monsterSpecialState == DUCK_DIVE || my->monsterSpecialState == DUCK_INERT ) + { + my->monsterSpecialState = 0; + serverUpdateEntitySkill(my, 33); + } + } + + if ( spiritDuck && my->monsterSpecialState == DUCK_INERT ) + { + if ( my->skill[2] >= 0 && my->skill[2] < MAXPLAYERS ) + { + if ( players[my->skill[2]]->ghost.isActive() ) + { + if ( players[my->skill[2]]->ghost.my && players[my->skill[2]]->ghost.my->skill[11] == 1 ) // high profile + { + my->monsterSpecialState = 0; + serverUpdateEntitySkill(my, 33); + } + } + } + } + + if ( safeTile && body && my->isMobile() ) + { + if ( my->monsterSpecialTimer == 0 ) + { + my->monsterSpecialTimer = spiritDuck ? 0 : 2 * TICKS_PER_SECOND; + if ( my->monsterSpecialState == DUCK_INERT && waterTile && !spiritDuck ) + { + if ( DUCK_INERT_ANIM_COMPLETE >= 0.95 ) + { + if ( my->monsterTarget == 0 || !uidToEntity(my->monsterTarget) ) + { + if ( !spiritDuck ) + { + if ( Entity* leader = uidToEntity(myStats->leader_uid) ) + { + if ( leader->behavior == &actPlayer && players[leader->skill[2]]->mechanics.numFishingCaught < 3 ) + { + my->monsterSpecialState = DUCK_DIVE; + serverUpdateEntitySkill(my, 33); + playSoundEntity(my, 794 + local_rng.rand() % 2, 128); + playSoundEntity(my, 786 + local_rng.rand() % 3, 128); + } + } + } + } + } + } + if ( my->monsterSpecialState == 0 ) + { + if ( spiritDuck ) + { + if ( my->skill[2] >= 0 && my->skill[2] < MAXPLAYERS ) + { + if ( players[my->skill[2]]->ghost.isActive() ) + { + if ( players[my->skill[2]]->ghost.my && players[my->skill[2]]->ghost.my->skill[11] == 0 ) // low profile + { + my->monsterSpecialState = DUCK_INERT; + serverUpdateEntitySkill(my, 33); + } + } + } + } + else + { + my->monsterSpecialTimer += 3 * TICKS_PER_SECOND; + my->monsterSpecialState = DUCK_INERT; + serverUpdateEntitySkill(my, 33); + } + } + } + else + { + if ( spiritDuck ) + { + if ( my->monsterSpecialTimer > 0 ) + { + --my->monsterSpecialTimer; + } + } + } + } + + if ( !spiritDuck && my->ticks % 15 == 1 ) + { + if ( my ) + { + for ( auto node = map.creatures->first; node; node = node->next ) + { + if ( Entity* entity = (Entity*)node->element ) + { + if ( entity != my ) + { + if ( entity->behavior == &actMonster && entityDist(my, entity) < 1.5 * TOUCHRANGE ) + { + entity->setEffect(EFF_DUCKED, true, 0.5 * TICKS_PER_SECOND, false, false, true, true); + } + } + } + } + } + } + } + + if ( body ) + { + if ( waterTile ) + { + if ( (my->monsterSpecialState == DUCK_INERT || my->monsterSpecialState == DUCK_RETURN) && abs(DUCK_FLOAT_ATK) < 0.05 + && DUCK_INERT_ANIM_COMPLETE >= 0.01 && abs(DUCK_FLOAT_ATK_DIVE - inertHeight) < 0.01 ) + { + inWater = true; + } + } + + DUCK_WALK_CYCLE *= 0.8; + DUCK_WALK_CYCLE2 *= 0.8; + if ( dist > 0.05 && !DUCK_INWATER ) + { + if ( my->monsterSpecialState == DUCK_RETURN ) + { + DUCK_WALK_CYCLE = 1.0; + } + else + { + DUCK_WALK_CYCLE2 = 1.0; + } + } + + if ( my->monsterSpecialState != DUCK_LAST_SPECIAL_STATE ) + { + DUCK_SPECIAL_TIMER = 0; + } + DUCK_LAST_SPECIAL_STATE = my->monsterSpecialState; + if ( my->monsterSpecialState ) + { + ++DUCK_SPECIAL_TIMER; + } + else + { + DUCK_SPECIAL_TIMER = 0; + } + } + + int appearance = 0; + if ( my->sprite == 2231 ) + { + appearance = 1; + } + else if ( my->sprite == 2237 ) + { + appearance = 2; + } + else if ( my->sprite == 2307 ) + { + appearance = 3; + } + + if ( spiritDuck ) + { + my->ditheringOverride = 6; + my->mistformGLRender = 1.0; + } + + //Move bodyparts + Entity* leftWing = nullptr; + Entity* leftLeg = nullptr; + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, ++bodypart ) + { + if ( bodypart < DUCK_BODY ) + { + continue; + } + + entity = (Entity*)node->element; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + entity->ditheringDisabled = true; + entity->yaw = my->yaw; + + if ( spiritDuck ) + { + entity->ditheringOverride = my->ditheringOverride; + entity->mistformGLRender = my->mistformGLRender; + } + + real_t dodgeSpinDir = DUCK_FLOAT_ATK >= 0.0 ? (PI / 8) : -PI / 8; + entity->yaw -= dodgeSpinDir * sin((PI / 2) * abs(DUCK_FLOAT_ATK)); + + if ( bodypart == DUCK_HEAD ) + { + if ( body && (my->monsterSpecialState == DUCK_INERT || my->monsterSpecialState == DUCK_RETURN) && abs(DUCK_FLOAT_ATK) < 0.05 + && DUCK_INERT_ANIM_COMPLETE >= 0.01 && abs(DUCK_FLOAT_ATK_DIVE - inertHeight) < 0.01 ) + { + entity->flags[INVISIBLE] = false; + } + else + { + entity->flags[INVISIBLE] = true; + } + entity->fskill[0] = fmod(entity->fskill[0], 2 * PI); + while ( entity->fskill[0] >= PI ) + { + entity->fskill[0] -= 2 * PI; + } + while ( entity->fskill[0] < -PI ) + { + entity->fskill[0] += 2 * PI; + } + entity->fskill[1] = fmod(entity->fskill[1], 2 * PI); + while ( entity->fskill[1] >= PI ) + { + entity->fskill[1] -= 2 * PI; + } + while ( entity->fskill[1] < -PI ) + { + entity->fskill[1] += 2 * PI; + } + } + else if ( bodypart == DUCK_BODY ) + { + body = entity; + entity->fskill[0] = fmod(entity->fskill[0], 2 * PI); + while ( entity->fskill[0] >= PI ) + { + entity->fskill[0] -= 2 * PI; + } + while ( entity->fskill[0] < -PI ) + { + entity->fskill[0] += 2 * PI; + } + + if ( MONSTER_ATTACK == 0 ) + { + DUCK_FLOAT_ATK *= 0.8; + } + + if ( my->monsterSpecialState == DUCK_DIVE ) + { + DUCK_INERT_ANIM *= 0.95; + DUCK_INERT_ANIM_COMPLETE *= 0.95; + } + else if ( my->monsterSpecialState == DUCK_INERT || my->monsterSpecialState == DUCK_RETURN ) + { + body->fskill[0] *= 0.8; + DUCK_DIVE_ANIM *= 0.8; + } + else + { + DUCK_FLOAT_ATK_DIVE *= 0.95; + DUCK_FLOAT_Z_MULT *= 0.95; + body->fskill[0] *= 0.95; + DUCK_DIVE_ANIM *= 0.95; + DUCK_INERT_ANIM *= 0.95; + DUCK_INERT_ANIM_COMPLETE *= 0.95; + } + + if ( MONSTER_ATTACK > 0 && (my->monsterSpecialState == DUCK_DIVE || my->monsterSpecialState == DUCK_RETURN) ) + { + MONSTER_ATTACK = 0; + MONSTER_ATTACKTIME = 0; + } + if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP2 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + DUCK_FLOAT_ATK = std::max(DUCK_FLOAT_ATK, 0.0); + if ( multiplayer != CLIENT && myStats ) + { + myStats->setEffectActive(EFF_STUNNED, 1); + myStats->EFFECTS_TIMERS[EFF_STUNNED] = 50; + + if ( Entity* leader = uidToEntity(myStats->leader_uid) ) + { + if ( leader->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdate(leader->skill[2], Compendium_t::CPDM_DUCK_DODGE, TOOL_DUCK, 1); + } + } + } + playSoundEntityLocal(my, 794 + local_rng.rand() % 2, 128); + playSoundEntityLocal(my, 786 + local_rng.rand() % 3, 128); + } + DUCK_FLOAT_ATK += std::max(0.1, 1.0 / 10.0); + DUCK_FLOAT_ATK = std::min(1.0, DUCK_FLOAT_ATK); + + if ( MONSTER_ATTACKTIME >= 35 ) + { + MONSTER_ATTACK = 0; + } + } + else if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP3 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + DUCK_FLOAT_ATK = std::min(DUCK_FLOAT_ATK, 0.0); + if ( multiplayer != CLIENT && myStats ) + { + myStats->setEffectActive(EFF_STUNNED, 1); + myStats->EFFECTS_TIMERS[EFF_STUNNED] = 50; + + if ( Entity* leader = uidToEntity(myStats->leader_uid) ) + { + if ( leader->behavior == &actPlayer ) + { + Compendium_t::Events_t::eventUpdate(leader->skill[2], Compendium_t::CPDM_DUCK_DODGE, TOOL_DUCK, 1); + } + } + } + playSoundEntityLocal(my, 794 + local_rng.rand() % 2, 128); + playSoundEntityLocal(my, 786 + local_rng.rand() % 3, 128); + } + DUCK_FLOAT_ATK -= std::max(0.1, 1.0 / 10.0); + DUCK_FLOAT_ATK = std::max(-1.0, DUCK_FLOAT_ATK); + + if ( MONSTER_ATTACKTIME >= 35 ) + { + MONSTER_ATTACK = 0; + } + } + + if ( my->monsterSpecialState == DUCK_INERT || my->monsterSpecialState == DUCK_RETURN ) + { + real_t start = -5; + real_t end = 15.5; + real_t midpoint = start + (end - start) / 2; + + DUCK_FLOAT_Z_MULT = std::min(1.0, DUCK_FLOAT_Z_MULT + 0.025); + + real_t ratio = 1.0 - cos((PI / 2) * std::min(DUCK_SPECIAL_TIMER, 35) / (real_t)35); + DUCK_INERT_ANIM = ratio; + real_t floatHeight = inertHeight - 2.0 * abs(DUCK_FLOAT_ATK); + + if ( DUCK_INERT_ANIM >= 0.95 ) + { + if ( abs(DUCK_FLOAT_ATK_DIVE - floatHeight) < 0.01 ) + { + DUCK_INERT_ANIM_COMPLETE += std::max(0.01, DUCK_INERT_ANIM_COMPLETE / 10.0); + DUCK_INERT_ANIM_COMPLETE = std::min(1.0, DUCK_INERT_ANIM_COMPLETE) * (1.0 - abs(DUCK_FLOAT_ATK)); + entity->fskill[1] += 0.2 * abs(DUCK_FLOAT_ATK); + + if ( my->monsterSpecialState == DUCK_RETURN ) + { + if ( my->ticks % 4 == 0 ) + { + spawnPoof(entity->x, entity->y, 7.5, 0.25, false); + } + } + } + else + { + entity->fskill[1] += 0.2; + } + } + else + { + entity->fskill[1] += 0.1; + entity->fskill[1] += 0.1 * abs(DUCK_FLOAT_ATK); + } + + if ( DUCK_FLOAT_ATK_DIVE > floatHeight ) + { + DUCK_FLOAT_ATK_DIVE = std::max(floatHeight, DUCK_FLOAT_ATK_DIVE - 0.25); + } + else + { + DUCK_FLOAT_ATK_DIVE = std::min(floatHeight, DUCK_FLOAT_ATK_DIVE + 0.1); + } + + if ( DUCK_WALK_CYCLE ) + { + DUCK_WALK_CYCLE_ANIM += 0.3; + } + if ( DUCK_WALK_CYCLE2 ) + { + DUCK_WALK_CYCLE_ANIM2 += 0.35; + } + while ( DUCK_WALK_CYCLE_ANIM >= 2 * PI ) + { + DUCK_WALK_CYCLE_ANIM -= 2 * PI; + } + while ( DUCK_WALK_CYCLE_ANIM2 >= 2 * PI ) + { + DUCK_WALK_CYCLE_ANIM2 -= 2 * PI; + } + + if ( DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 >= 0.95 && (abs(fmod(DUCK_WALK_CYCLE_ANIM2, PI) < 0.35)) ) + { + playSoundEntityLocal(my, 779 + local_rng.rand() % 5, 12); + } + else if ( DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE >= 0.95 && (abs(fmod(DUCK_WALK_CYCLE_ANIM, PI) < 0.3)) ) + { + playSoundEntityLocal(my, 779 + local_rng.rand() % 5, 32); + } + } + else if ( my->monsterSpecialState == DUCK_DIVE ) + { + int interval = 15; + int interval2 = 50; + int interval3 = 150; + if ( DUCK_SPECIAL_TIMER < interval ) + { + if ( entity->fskill[0] < 0.01 ) + { + entity->fskill[0] = std::max(-PI / 8, entity->fskill[0] - PI / 64); + } + else + { + entity->fskill[0] *= 0.75; + } + entity->fskill[1] += 0.1; + + + entity->skill[1] = 0; + DUCK_DIVE_ANIM *= 0.8; // decay if previous dive + if ( DUCK_FLOAT_ATK_DIVE >= 5.0 ) + { + DUCK_FLOAT_ATK_DIVE -= (DUCK_FLOAT_ATK_DIVE - 5.0) * (DUCK_SPECIAL_TIMER / (real_t)interval); + DUCK_FLOAT_ATK_DIVE = std::max(5.0, DUCK_FLOAT_ATK_DIVE); + } + else + { + DUCK_FLOAT_ATK_DIVE = 5 * sin((DUCK_SPECIAL_TIMER / (real_t)(4 * interval)) * 2 * PI); + } + } + else if ( DUCK_SPECIAL_TIMER >= interval && (DUCK_SPECIAL_TIMER - interval) <= interval2 ) + { + if ( DUCK_SPECIAL_TIMER == interval ) + { + entity->fskill[0] = -PI / 8; + } + int currentTick = (DUCK_SPECIAL_TIMER - interval); + + entity->fskill[1] += 0.1; + + DUCK_FLOAT_ATK_DIVE = 5 * (1 - 2 * sin((PI / 2) * std::min(interval2, currentTick) / (real_t)interval2)); + } + else if ( (DUCK_SPECIAL_TIMER - interval - interval2) >= 0 ) + { + int currentTick = (DUCK_SPECIAL_TIMER - interval - interval2); + entity->fskill[1] += 0.1; + DUCK_FLOAT_Z_MULT = std::min(1.0, DUCK_FLOAT_Z_MULT + 0.025); + + real_t ratio = 1.0 - cos((PI / 2) * std::min(currentTick, 35) / (real_t)35); + DUCK_DIVE_ANIM = ratio; + entity->fskill[0] = std::min(6 * PI / 8, - PI / 8 + ratio * 6 * PI / 4); + + real_t start = -5; + real_t end = 15.5; + real_t midpoint = start + (end - start) / 2; + + real_t ratio2 = 1.0 - cos((PI / 2) * std::min(std::max(0, currentTick - 5), 25) / (real_t)25); + DUCK_FLOAT_ATK_DIVE = midpoint - ((end - start) / 2) * sin(PI / 2 - PI * ratio2); + if ( currentTick >= 22 ) + { + inWater = true; + } + + int huntInterval = 150; + /*if ( currentTick == 22 && spiritDuck && !waterTile ) + { + if ( multiplayer != CLIENT ) + { + if ( lavaTile ) + { + my->monsterSpecialState = 0; + serverUpdateEntitySkill(my, 33); + } + else if ( !noFloor ) + { + Entity* spellTimer = createParticleTimer(nullptr, TICKS_PER_SECOND, -1); + spellTimer->x = my->x; + spellTimer->y = my->y; + spellTimer->z = 0.0; + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_TRAP_SABOTAGED; + serverSpawnMiscParticlesAtLocation(spellTimer->x, spellTimer->y, spellTimer->z, PARTICLE_EFFECT_SABOTAGE_TRAP, 0); + my->monsterSpecialState = 0; + serverUpdateEntitySkill(my, 33); + playSoundEntity(my, 807, 128); + createSpellExplosionArea(SPELL_PROJECT_SPIRIT, my->skill[2] >= 0 && my->skill[2] < MAXPLAYERS ? players[my->skill[2]]->entity : my, my->x, my->y, 7.5, 8.0, 5, nullptr); + } + } + }*/ + if ( DUCK_DIVE_ANIM >= 0.975 && currentTick >= huntInterval ) + { + int interval4 = 300; + int raiseLowerInterval = 10; + const real_t diveDepth = 5.0; + const real_t bobDepth = 0.25; + if ( (currentTick - huntInterval) % interval4 < raiseLowerInterval ) + { + DUCK_FLOAT_ATK_DIVE -= diveDepth * sin(PI / 2 * ((currentTick - huntInterval) % interval4) / (real_t)(raiseLowerInterval)); + } + else if ( (currentTick - huntInterval) % interval4 < interval4 / 2 ) + { + if ( (currentTick - huntInterval) % interval4 == raiseLowerInterval ) + { + if ( waterTile ) + { + createWaterSplash(my->x, my->y, 30); + playSoundEntityLocal(my, 136, 64); + } + } + DUCK_FLOAT_ATK_DIVE -= diveDepth; + } + else if ( (currentTick - huntInterval) % interval4 < (interval4 / 2 + raiseLowerInterval) ) + { + DUCK_FLOAT_ATK_DIVE += -diveDepth + diveDepth * sin(PI / 2 * ((currentTick - huntInterval - interval4 / 2) % interval4) / (real_t)(raiseLowerInterval)); + } + + DUCK_FLOAT_ATK_DIVE += bobDepth * sin((currentTick % TICKS_PER_SECOND / (real_t)TICKS_PER_SECOND) * 2 * PI); + + if ( currentTick > huntInterval && (currentTick % (2 * huntInterval) == huntInterval / 2) ) + { + if ( spiritDuck && !waterTile ) + { + my->monsterSpecialState = 0; + serverUpdateEntitySkill(my, 33); + } + else if ( multiplayer != CLIENT ) + { + int bless = 0; + if ( myStats ) + { + if ( myStats->getAttribute("duck_bless") != "" ) + { + bless = std::stoi(myStats->getAttribute("duck_bless")); + } + } + int chance = std::max(1, 5 - bless); + if ( local_rng.rand() % chance == 0 ) + { + Item* item = nullptr; + if ( bless > 0 && local_rng.rand() % 3 == 0 ) + { + int charge = std::min(ENCHANTED_FEATHER_MAX_DURABILITY - 1, 25 + local_rng.rand() % (bless * 25)); + item = newItem(ENCHANTED_FEATHER, EXCELLENT, 0, 1, charge, false, nullptr); + --bless; + myStats->setAttribute("duck_bless", std::to_string(bless)); + } + else + { + ItemType type = FOOD_FISH; + if ( local_rng.rand() % 20 == 0 ) + { + switch ( local_rng.rand() % 6 ) + { + case 0: + case 1: + type = LEATHER_BOOTS; + break; + case 2: + case 3: + type = IRON_BOOTS; + break; + case 4: + type = LEATHER_BOOTS_SPEED; + break; + case 5: + type = IRON_BOOTS_WATERWALKING; + break; + default: + break; + } + } + item = newItem(FOOD_FISH, static_cast(DECREPIT + local_rng.rand() % 4), -1 + local_rng.rand() % 3, 1, local_rng.rand(), false, nullptr); + if ( abs(bless) > 0 ) + { + item->beatitude = bless; + } + + if ( Entity* leader = uidToEntity(myStats->leader_uid) ) + { + if ( leader->behavior == &actPlayer ) + { + players[leader->skill[2]]->mechanics.numFishingCaught++; + if ( item->type == FOOD_FISH ) + { + Compendium_t::Events_t::eventUpdate(leader->skill[2], Compendium_t::CPDM_DUCK_CAUGHT, TOOL_DUCK, 1); + } + } + } + } + if ( Entity* dropped = dropItemMonster(item, my, myStats, 1) ) + { + + } + else + { + free(item); + } + playSoundEntity(my, 789 + local_rng.rand() % 5, 128); + my->monsterSpecialState = 0; + serverUpdateEntitySkill(my, 33); + } + else if ( currentTick >= (2 * huntInterval) * 3 ) + { + my->monsterSpecialState = 0; + serverUpdateEntitySkill(my, 33); + } + } + } + } + + } + } + } + else if ( bodypart == DUCK_LEFTWING ) + { + entity->fskill[1] = fmod(entity->fskill[1], 2 * PI); + while ( entity->fskill[1] >= PI ) + { + entity->fskill[1] -= 2 * PI; + } + while ( entity->fskill[1] < -PI ) + { + entity->fskill[1] += 2 * PI; + } + entity->fskill[0] = fmod(entity->fskill[0], 2 * PI); + while ( entity->fskill[0] >= PI ) + { + entity->fskill[0] -= 2 * PI; + } + while ( entity->fskill[0] < -PI ) + { + entity->fskill[0] += 2 * PI; + } + } + + switch ( bodypart ) + { + case DUCK_BODY: + { + if ( appearance == 3 ) + { + entity->sprite = 2308; + } + else + { + entity->sprite = 2226 + appearance * 6; + } + + if ( head ) + { + entity->flags[INVISIBLE] = !head->flags[INVISIBLE]; + } + + entity->x += limbs[DUCK_SMALL][6][0] * cos(entity->yaw); + entity->y += limbs[DUCK_SMALL][6][1] * sin(entity->yaw); + entity->z += limbs[DUCK_SMALL][6][2]; + entity->focalx = limbs[DUCK_SMALL][1][0]; + entity->focaly = limbs[DUCK_SMALL][1][1]; + entity->focalz = limbs[DUCK_SMALL][1][2]; + + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_6] ) + { + entity->fskill[0] -= 0.05; + } + else if ( keystatus[SDLK_KP_4] ) + { + entity->fskill[0] += 0.05; + } + } + entity->pitch = entity->fskill[0]; + entity->pitch += DUCK_WALK_CYCLE * PI / 8; + if ( my->monsterState != MONSTER_STATE_WAIT ) + { + entity->pitch += DUCK_WALK_CYCLE2 * PI / 16; + } + entity->pitch -= abs(DUCK_FLOAT_ATK) * PI / 16; + + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_PLUS] ) + { + keystatus[SDLK_KP_PLUS] = 0; + entity->skill[0] = entity->skill[0] == 0 ? 1 : 0; + } + } + entity->roll = DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE * 0.25 * (sin(DUCK_WALK_CYCLE_ANIM)); + + if ( my->monsterSpecialState == DUCK_DIVE || my->monsterSpecialState == DUCK_INERT || my->monsterSpecialState == DUCK_RETURN ) + { + //entity->fskill[1] += 0.25; + } + else if ( entity->skill[0] == 0 ) + { + entity->fskill[1] += 0.1; + entity->fskill[1] += 0.05 * abs(DUCK_FLOAT_ATK); + } + + { + DUCK_FLOAT_X = limbs[DUCK_SMALL][10][0] * sin(body->fskill[1] * limbs[DUCK_SMALL][11][0]) * cos(entity->yaw + PI / 2) * (1.0 - DUCK_FLOAT_Z_MULT); + DUCK_FLOAT_Y = limbs[DUCK_SMALL][10][1] * sin(body->fskill[1] * limbs[DUCK_SMALL][11][1]) * sin(entity->yaw + PI / 2) * (1.0 - DUCK_FLOAT_Z_MULT); + DUCK_FLOAT_Z = limbs[DUCK_SMALL][10][2] * sin(body->fskill[1] * limbs[DUCK_SMALL][11][2]) * (1.0 - DUCK_FLOAT_Z_MULT) * (1.0 - abs(DUCK_FLOAT_ATK)); + + DUCK_FLOAT_Z += DUCK_FLOAT_ATK_DIVE; + DUCK_FLOAT_Z -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE * 0.5 * abs(sin(DUCK_WALK_CYCLE_ANIM)); + } + + if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP2 || MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP3 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + spawnPoof(entity->x + DUCK_FLOAT_X, entity->y + DUCK_FLOAT_Y, entity->z + DUCK_FLOAT_Z, 0.5); + } + } + + real_t floatAtkZ = -2.0 * sin((PI / 2) * abs(DUCK_FLOAT_ATK)); + DUCK_FLOAT_X += 4.0 * sin((PI / 2) * abs(DUCK_FLOAT_ATK)) * cos(entity->yaw + (PI - dodgeSpinDir)); + DUCK_FLOAT_Y += 4.0 * sin((PI / 2) * abs(DUCK_FLOAT_ATK)) * sin(entity->yaw + (PI - dodgeSpinDir)); + DUCK_FLOAT_Z += floatAtkZ; + + if ( DUCK_INWATER ) + { + DUCK_BOB_WATER += 0.025; + DUCK_FLOAT_Z += 0.125 * (1.0 + sin(DUCK_BOB_WATER * 2 * PI + PI / 2)); + entity->pitch += (PI / 64) * (1.0 + sin(DUCK_BOB_WATER * 2 * PI)); + } + else + { + DUCK_BOB_WATER = 0.0; + } + + entity->x += DUCK_FLOAT_X; + entity->y += DUCK_FLOAT_Y; + entity->z += DUCK_FLOAT_Z; + + DUCK_CAM_Z = entity->z; + + DUCK_CAM_Z -= (limbs[DUCK_SMALL][10][2]) * sin(body->fskill[1] * limbs[DUCK_SMALL][11][2]) * (1.0 - DUCK_FLOAT_Z_MULT) * (1.0 - abs(DUCK_FLOAT_ATK)); + DUCK_CAM_Z += (limbs[DUCK_SMALL][10][2] / 5.0) * sin(body->fskill[1] * limbs[DUCK_SMALL][11][2] * 0.25) * (1.0 - DUCK_FLOAT_Z_MULT) * (1.0 - abs(DUCK_FLOAT_ATK)); + break; + } + case DUCK_HEAD: + if ( appearance == 3 ) + { + entity->sprite = 2307; + } + else + { + entity->sprite = 2225 + appearance * 6; + } + + entity->x += limbs[DUCK_SMALL][7][0] * cos(entity->yaw); + entity->y += limbs[DUCK_SMALL][7][1] * sin(entity->yaw); + entity->z += limbs[DUCK_SMALL][7][2]; + entity->focalx = limbs[DUCK_SMALL][2][0]; + entity->focaly = limbs[DUCK_SMALL][2][1]; + entity->focalz = limbs[DUCK_SMALL][2][2]; + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_7] ) + { + entity->fskill[0] -= 0.05; + } + else if ( keystatus[SDLK_KP_9] ) + { + entity->fskill[0] += 0.05; + } + } + + entity->fskill[0] = body->pitch; + entity->pitch = entity->fskill[0]; + + if ( body ) + { + if ( spiritDuck ) + { + if ( my->skill[2] >= 0 && my->skill[2] < MAXPLAYERS ) + { + if ( players[my->skill[2]]->ghost.isActive() ) + { + if ( players[my->skill[2]]->ghost.my ) + { + real_t pitch = players[my->skill[2]]->ghost.my->pitch; + while ( pitch >= PI ) + { + pitch -= 2 * PI; + } + entity->fskill[1] = -pitch / (PI / 3); + } + } + } + } + else + { + + if ( my->monsterState == MONSTER_STATE_ATTACK ) + { + entity->fskill[1] += 0.1; + entity->fskill[1] = std::min(1.0, entity->fskill[1]); + } + else + { + entity->fskill[1] -= 0.1; + entity->fskill[1] = std::max(0.0, entity->fskill[1]); + } + } + entity->pitch -= sin(entity->fskill[1] * PI / 2) * PI / 8; + entity->roll = body->roll; + entity->roll += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * 0.125 * (sin(DUCK_WALK_CYCLE_ANIM2 + PI + PI / 4)); + entity->x += DUCK_FLOAT_X; + entity->y += DUCK_FLOAT_Y; + entity->z += DUCK_FLOAT_Z; + + entity->x += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * 1.5 * sin(entity->roll) * cos(entity->yaw + PI / 2); + entity->y += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * 1.5 * sin(entity->roll) * sin(entity->yaw + PI / 2); + entity->z -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * (0.2 + 0.2 * (sin(2 * DUCK_WALK_CYCLE_ANIM2 + PI / 2))); + } + break; + case DUCK_LEFTWING: + if ( appearance == 3 ) + { + entity->sprite = 2309; + } + else + { + entity->sprite = 2227 + appearance * 6; + } + + entity->x += limbs[DUCK_SMALL][8][0] * cos(entity->yaw) + limbs[DUCK_SMALL][8][1] * cos(entity->yaw + PI / 2); + entity->y += limbs[DUCK_SMALL][8][0] * sin(entity->yaw) + limbs[DUCK_SMALL][8][1] * sin(entity->yaw + PI / 2); + entity->z += limbs[DUCK_SMALL][8][2]; + entity->focalx = limbs[DUCK_SMALL][3][0]; + entity->focaly = limbs[DUCK_SMALL][3][1]; + entity->focalz = limbs[DUCK_SMALL][3][2]; + + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_1] ) + { + entity->fskill[0] -= 0.05; + } + else if ( keystatus[SDLK_KP_3] ) + { + entity->fskill[0] += 0.05; + } + if ( keystatus[SDLK_KP_0] ) + { + entity->fskill[1] -= 0.05; + } + else if ( keystatus[SDLK_KP_ENTER] ) + { + entity->fskill[1] += 0.05; + } + + if ( keystatus[SDLK_KP_MINUS] ) + { + keystatus[SDLK_KP_MINUS] = 0; + entity->skill[3] = entity->skill[3] == 0 ? 1 : 0; + } + } + + if ( (entity->skill[3] == 0) ) + { + real_t minRot = -1.3; + real_t maxRot = 0.8; + + if ( my->monsterSpecialState == DUCK_INERT || my->monsterSpecialState == DUCK_RETURN ) + { + minRot = 0.0; + maxRot = 0.8; + } + + real_t midpoint = minRot + (maxRot - minRot) / 2; + entity->fskill[1] = midpoint + ((maxRot - minRot) / 2) * sin(PI / 2 + body->fskill[1] * limbs[DUCK_SMALL][11][2]); + } + + entity->pitch = entity->fskill[0] * (1.0 - DUCK_INERT_ANIM) * (1.0 - DUCK_DIVE_ANIM); + entity->pitch += (DUCK_DIVE_ANIM)*PI; + entity->roll = entity->fskill[1] * std::max(abs(DUCK_FLOAT_ATK), (1.0 - DUCK_DIVE_ANIM) * (1.0 - DUCK_INERT_ANIM_COMPLETE)); + + entity->pitch -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE * (PI / 2); + entity->roll += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE * (PI / 2); + leftWing = entity; + + if ( head->flags[INVISIBLE] ) + { + entity->scalex += 0.1; + entity->scalex = std::min(1.0, entity->scalex); + } + else + { + entity->scalex -= 0.1; + entity->scalex = std::max(0.0, entity->scalex); + } + entity->scaley = entity->scalex; + entity->scalez = entity->scalex; + + entity->x += (1.0 - entity->scalex) * limbs[DUCK_SMALL][24][1] * cos(entity->yaw + PI / 2); + entity->y += (1.0 - entity->scalex) * limbs[DUCK_SMALL][24][1] * sin(entity->yaw + PI / 2); + entity->z += (1.0 - entity->scalex) * limbs[DUCK_SMALL][24][2]; + + if ( body ) + { + entity->x += DUCK_FLOAT_X; + entity->y += DUCK_FLOAT_Y; + entity->z += DUCK_FLOAT_Z; + + entity->x += limbs[DUCK_SMALL][12][0] * sin(body->pitch) * cos(entity->yaw); + entity->y += limbs[DUCK_SMALL][12][1] * sin(body->pitch) * sin(entity->yaw); + entity->z += limbs[DUCK_SMALL][12][2] * sin(body->pitch); + } + break; + case DUCK_RIGHTWING: + if ( appearance == 3 ) + { + entity->sprite = 2310; + } + else + { + entity->sprite = 2228 + appearance * 6; + } + /*if ( body ) + { + entity->flags[INVISIBLE] = body->flags[INVISIBLE]; + }*/ + + entity->x += limbs[DUCK_SMALL][9][0] * cos(entity->yaw) + limbs[DUCK_SMALL][9][1] * cos(entity->yaw + PI / 2); + entity->y += limbs[DUCK_SMALL][9][0] * sin(entity->yaw) + limbs[DUCK_SMALL][9][1] * sin(entity->yaw + PI / 2); + entity->z += limbs[DUCK_SMALL][9][2]; + entity->focalx = limbs[DUCK_SMALL][4][0]; + entity->focaly = limbs[DUCK_SMALL][4][1]; + entity->focalz = limbs[DUCK_SMALL][4][2]; + if ( leftWing ) + { + entity->fskill[0] = leftWing->fskill[0]; + entity->fskill[1] = -leftWing->fskill[1]; + } + + entity->pitch = entity->fskill[0] * (1.0 - DUCK_INERT_ANIM) * (1.0 - DUCK_DIVE_ANIM); + entity->pitch += (DUCK_DIVE_ANIM)*PI; + entity->roll = entity->fskill[1] * std::max(abs(DUCK_FLOAT_ATK), (1.0 - DUCK_DIVE_ANIM) * (1.0 - DUCK_INERT_ANIM_COMPLETE)); + + entity->pitch -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE * (PI / 2); + entity->roll -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE * (PI / 2); + + entity->scalex = leftWing->scalex; + entity->scaley = leftWing->scalex; + entity->scalez = leftWing->scalex; + + entity->x += (1.0 - entity->scalex) * limbs[DUCK_SMALL][24][0] * cos(entity->yaw + PI / 2); + entity->y += (1.0 - entity->scalex) * limbs[DUCK_SMALL][24][0] * sin(entity->yaw + PI / 2); + entity->z += (1.0 - entity->scalex) * limbs[DUCK_SMALL][24][2]; + + if ( body ) + { + entity->x += DUCK_FLOAT_X; + entity->y += DUCK_FLOAT_Y; + entity->z += DUCK_FLOAT_Z; + + entity->x += limbs[DUCK_SMALL][12][0] * sin(body->pitch) * cos(entity->yaw); + entity->y += limbs[DUCK_SMALL][12][1] * sin(body->pitch) * sin(entity->yaw); + entity->z += limbs[DUCK_SMALL][12][2] * sin(body->pitch); + + } + break; + case DUCK_LEFTLEG: + { + entity->focalx = limbs[DUCK_SMALL][20][0]; + entity->focaly = limbs[DUCK_SMALL][20][1]; + entity->focalz = limbs[DUCK_SMALL][20][2]; + bool webFoot = (head && !head->flags[INVISIBLE]) && (my->monsterSpecialState != DUCK_RETURN); + if ( webFoot ) + { + entity->focalz -= 0.25; + entity->focaly += 0.25; + entity->z += 0.25; + } + + entity->sprite = 2229; + if ( my->sprite == 2225 || my->sprite == 2226 ) + { + if ( webFoot ) + { + entity->sprite = 2243; + } + else + { + entity->sprite = 2229; + } + } + else if ( my->sprite == 2231 || my->sprite == 2232 ) + { + if ( webFoot ) + { + entity->sprite = 2244; + } + else + { + entity->sprite = 2235; + } + } + else if ( my->sprite == 2237 || my->sprite == 2238 ) + { + if ( webFoot ) + { + entity->sprite = 2245; + } + else + { + entity->sprite = 2241; + } + } + else if ( my->sprite == 2307 || my->sprite == 2308 ) + { + if ( webFoot ) + { + entity->sprite = 2313; + } + else + { + entity->sprite = 2311; + } + } + if ( body && head ) + { + entity->flags[INVISIBLE] = body->flags[INVISIBLE] && head->flags[INVISIBLE]; + } + + entity->x += limbs[DUCK_SMALL][19][0] * cos(entity->yaw) + limbs[DUCK_SMALL][19][1] * cos(entity->yaw + PI / 2); + entity->y += limbs[DUCK_SMALL][19][0] * sin(entity->yaw) + limbs[DUCK_SMALL][19][1] * sin(entity->yaw + PI / 2); + entity->z += limbs[DUCK_SMALL][19][2]; + + if ( body ) + { + entity->x += DUCK_FLOAT_X; + entity->y += DUCK_FLOAT_Y; + entity->z += DUCK_FLOAT_Z; + + entity->x += limbs[DUCK_SMALL][23][0] * sin(body->pitch) * cos(body->yaw); + entity->y += limbs[DUCK_SMALL][23][1] * sin(body->pitch) * sin(body->yaw); + entity->z += limbs[DUCK_SMALL][23][2] * cos(body->pitch); + + entity->roll = body->roll; + } + + leftLeg = entity; + + entity->fskill[0] = -leftWing->roll * (1.0 - DUCK_INERT_ANIM_COMPLETE); + if ( my->monsterSpecialState == DUCK_DIVE ) + { + real_t minRot = -1.3; + real_t maxRot = 0.8; + real_t midpoint = minRot + (maxRot - minRot) / 2; + entity->fskill[0] = midpoint + ((maxRot - minRot) / 2) * sin(PI / 2 + ((ticks % 15) / 7.5) * PI); + } + entity->pitch = entity->fskill[0] + entity->fskill[1] + body->pitch; + //entity->pitch += (PI / 4) * DUCK_INERT_ANIM_COMPLETE; + entity->pitch += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE * (PI / 4) * sin(DUCK_WALK_CYCLE_ANIM); + + entity->x += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * (0.5 + 0.5 * sin(DUCK_WALK_CYCLE_ANIM2 + PI)) * cos(entity->yaw); + entity->y += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * (0.5 + 0.5 * sin(DUCK_WALK_CYCLE_ANIM2 + PI)) * sin(entity->yaw); + entity->z -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * 0.4 * std::max(0.0, sin(DUCK_WALK_CYCLE_ANIM2 + PI + PI / 2)); + entity->pitch -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * (PI / 4) * std::max(0.0, sin(DUCK_WALK_CYCLE_ANIM2 + PI + PI / 2)); + break; + } + case DUCK_RIGHTLEG: + { + entity->focalx = limbs[DUCK_SMALL][22][0]; + entity->focaly = limbs[DUCK_SMALL][22][1]; + entity->focalz = limbs[DUCK_SMALL][22][2]; + + bool webFoot = (head && !head->flags[INVISIBLE]) && (my->monsterSpecialState != DUCK_RETURN); + if ( webFoot ) + { + entity->focalz -= 0.25; + entity->focaly -= 0.25; + entity->z += 0.25; + } + + entity->sprite = 2230; + if ( my->sprite == 2225 || my->sprite == 2226 ) + { + if ( webFoot ) + { + entity->sprite = 2243; + } + else + { + entity->sprite = 2230; + } + } + else if ( my->sprite == 2231 || my->sprite == 2232 ) + { + if ( webFoot ) + { + entity->sprite = 2244; + } + else + { + entity->sprite = 2236; + } + } + else if ( my->sprite == 2237 || my->sprite == 2238 ) + { + if ( webFoot ) + { + entity->sprite = 2245; + } + else + { + entity->sprite = 2242; + } + } + else if ( my->sprite == 2307 || my->sprite == 2308 ) + { + if ( webFoot ) + { + entity->sprite = 2313; + } + else + { + entity->sprite = 2312; + } + } + if ( body && head ) + { + entity->flags[INVISIBLE] = body->flags[INVISIBLE] && head->flags[INVISIBLE]; + } + + entity->x += limbs[DUCK_SMALL][21][0] * cos(entity->yaw) + limbs[DUCK_SMALL][21][1] * cos(entity->yaw + PI / 2); + entity->y += limbs[DUCK_SMALL][21][0] * sin(entity->yaw) + limbs[DUCK_SMALL][21][1] * sin(entity->yaw + PI / 2); + entity->z += limbs[DUCK_SMALL][21][2]; + + if ( body ) + { + entity->x += DUCK_FLOAT_X; + entity->y += DUCK_FLOAT_Y; + entity->z += DUCK_FLOAT_Z; + + entity->x += limbs[DUCK_SMALL][23][0] * sin(body->pitch) * cos(entity->yaw); + entity->y += limbs[DUCK_SMALL][23][1] * sin(body->pitch) * sin(entity->yaw); + entity->z += limbs[DUCK_SMALL][23][2] * cos(body->pitch); + + entity->roll = body->roll; + } + + if ( leftLeg && body ) + { + if ( my->monsterSpecialState == 0 ) + { + entity->pitch = leftLeg->fskill[0]; // swing same + } + else + { + entity->pitch = -leftLeg->fskill[0]; + } + entity->pitch += leftLeg->fskill[1] + body->pitch; + //entity->pitch += (PI / 4) * DUCK_INERT_ANIM_COMPLETE; + entity->pitch -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE * (PI / 4) * sin(DUCK_WALK_CYCLE_ANIM * 2); + + entity->x += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * (0.5 + 0.5 * sin(DUCK_WALK_CYCLE_ANIM2)) * cos(entity->yaw); + entity->y += DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * (0.5 + 0.5 * sin(DUCK_WALK_CYCLE_ANIM2)) * sin(entity->yaw); + entity->z -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * 0.4 * std::max(0.0, sin(DUCK_WALK_CYCLE_ANIM2 + PI / 2)); + entity->pitch -= DUCK_INERT_ANIM_COMPLETE * DUCK_WALK_CYCLE2 * (PI / 4) * std::max(0.0, sin(DUCK_WALK_CYCLE_ANIM2 + PI / 2)); + } + break; + } + default: + break; + } + } + + if ( body ) + { + if ( (inWater ? 1 : 0) != DUCK_INWATER ) + { + if ( waterTile ) + { + createWaterSplash(my->x, my->y, 30); + playSoundEntityLocal(my, 136, 64); + } + } + + if ( !DUCK_INIT ) // spawn feathers + { + DUCK_INIT = 1; + duckSpawnFeather(my->sprite, my->x, my->y, my->z, my); + } + DUCK_INWATER = inWater; + } + + if ( MONSTER_ATTACK > 0 ) + { + MONSTER_ATTACKTIME++; + } + else if ( MONSTER_ATTACK == 0 ) + { + MONSTER_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } +} \ No newline at end of file diff --git a/src/monster_g.cpp b/src/monster_g.cpp new file mode 100644 index 000000000..1743ad5d4 --- /dev/null +++ b/src/monster_g.cpp @@ -0,0 +1,1651 @@ +/*------------------------------------------------------------------------------- + + BARONY + File: monster_gnome.cpp + Desc: implements all of the gnome monster's code + + Copyright 2013-2016 (c) Turning Wheel LLC, all rights reserved. + See LICENSE for details. + +-------------------------------------------------------------------------------*/ + +#include "main.hpp" +#include "game.hpp" +#include "stat.hpp" +#include "entity.hpp" +#include "items.hpp" +#include "monster.hpp" +#include "engine/audio/sound.hpp" +#include "book.hpp" +#include "net.hpp" +#include "collision.hpp" +#include "player.hpp" +#include "prng.hpp" +#include "mod_tools.hpp" + +real_t getNormalHeightMonsterG(Entity& my) +{ + return 1.5; +} + +enum MonsterGVariant +{ + NONE, + SAPPER, + SKIRMISHER, + BERSERKER +}; + +void initMonsterG(Entity* my, Stat* myStats) +{ + int c; + node_t* node; + + my->flags[BURNABLE] = true; + my->initMonster(1569); + my->z = getNormalHeightMonsterG(*my); + + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = 736; + MONSTER_SPOTVAR = 3; + MONSTER_IDLESND = 730; + MONSTER_IDLEVAR = 3; + } + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( myStats->sex == FEMALE ) + { + my->sprite = 1570; + } + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // boss variants + + // random effects + /*if ( rng.rand() % 8 == 0 ) + { + myStats->setEffectActive(EFF_ASLEEP, 1); + myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 1800; + }*/ + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + MonsterGVariant variant = NONE; + + Item* item = nullptr; + if ( myStats->getAttribute("monster_g_type") == "" ) + { + switch ( rng.rand() % 3 ) + { + case 0: + myStats->setAttribute("monster_g_type", "sapper"); + variant = SAPPER; + if ( defaultItems >= 1 ) + { + item = newItem(GREASE_BALL, SERVICABLE, 0, rng.rand() % 2 + 2, rng.rand(), false, &myStats->inventory); + item->isDroppable = rng.rand() % 5 == 0; + } + if ( defaultItems >= 2 ) + { + if ( rng.rand() % 4 == 0 ) + { + item = newItem(POTION_FIRESTORM, SERVICABLE, 0, rng.rand() % 2 + 1, rng.rand(), false, &myStats->inventory); + item->isDroppable = rng.rand() % 5 == 0; + } + item = newItem(POTION_SICKNESS, SERVICABLE, -2, rng.rand() % 2 + 1, rng.rand(), false, &myStats->inventory); + item->isDroppable = rng.rand() % 5 == 0; + } + break; + case 1: + myStats->setAttribute("monster_g_type", "skirmisher"); + variant = SKIRMISHER; + if ( defaultItems >= 1 ) + { + item = newItem(BOLAS, SERVICABLE, 0, rng.rand() % 2 + 2, rng.rand(), false, &myStats->inventory); + item->isDroppable = rng.rand() % 5 == 0; + } + if ( defaultItems >= 2 ) + { + item = newItem(GREASE_BALL, SERVICABLE, 0, rng.rand() % 2 + 2, rng.rand(), false, &myStats->inventory); + item->isDroppable = rng.rand() % 5 == 0; + } + break; + case 2: + myStats->setAttribute("monster_g_type", "berserker"); + variant = BERSERKER; + break; + default: + break; + } + } + + if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + { + if ( variant == SKIRMISHER ) + { + if ( rng.rand() % 2 ) + { + myStats->weapon = newItem(STEEL_SWORD, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + else + { + myStats->weapon = newItem(STEEL_AXE, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + else if ( variant == SAPPER ) + { + if ( rng.rand() % 5 == 0 ) + { + myStats->weapon = newItem(STEEL_FLAIL, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + else + { + myStats->weapon = newItem(STEEL_MACE, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + + } + else if ( variant == BERSERKER ) + { + if ( rng.rand() % 2 ) + { + myStats->weapon = newItem(STEEL_AXE, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + } + + if ( myStats->shield == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] == 1 ) + { + if ( variant == SAPPER ) + { + if ( rng.rand() % 2 ) + { + } + else + { + if ( rng.rand() % 2 ) + { + myStats->shield = newItem(IRON_SHIELD, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + else + { + myStats->shield = newItem(WOODEN_SHIELD, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + } + } + + if ( myStats->helmet == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_HELM] == 1 ) + { + if ( variant == SKIRMISHER ) + { + myStats->helmet = newItem(HAT_HOOD, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + + if ( myStats->shoes == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_BOOTS] == 1 ) + { + if ( variant == SKIRMISHER ) + { + if ( rng.rand() % 2 ) + { + myStats->shoes = newItem(LEATHER_BOOTS, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + else if ( variant == SAPPER ) + { + if ( rng.rand() % 2 ) + { + myStats->shoes = newItem(LEATHER_BOOTS, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + } + + if ( myStats->gloves == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_GLOVES] == 1 ) + { + if ( variant == SKIRMISHER ) + { + if ( rng.rand() % 2 ) + { + myStats->gloves = newItem(GLOVES, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + else if ( variant == SAPPER ) + { + if ( rng.rand() % 2 ) + { + myStats->gloves = newItem(GLOVES, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + else if ( variant == BERSERKER ) + { + if ( !myStats->weapon ) + { + if ( rng.rand() % 2 ) + { + myStats->gloves = newItem(IRON_KNUCKLES, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + else if ( rng.rand() % 3 > 0 ) + { + myStats->gloves = newItem(BRASS_KNUCKLES, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + else + { + myStats->gloves = newItem(SPIKED_GAUNTLETS, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + } + } + + if ( myStats->cloak == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_CLOAK] == 1 ) + { + if ( variant == SKIRMISHER ) + { + if ( rng.rand() % 2 ) + { + myStats->cloak = newItem(CLOAK, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + } + + if ( myStats->breastplate == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] == 1 ) + { + if ( variant == BERSERKER ) + { + if ( rng.rand() % 2 ) + { + myStats->breastplate = newItem(TUNIC, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + } + } + } + + if ( myStats->amulet == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_AMULET] == 1 ) + { + if ( variant == SAPPER ) + { + if ( rng.rand() % 10 == 0 ) + { + myStats->amulet = newItem(AMULET_BURNINGRESIST, static_cast(WORN + rng.rand() % 2), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + myStats->amulet->isDroppable = rng.rand() % 5 == 0; + } + else + { + myStats->amulet = newItem(AMULET_BURNINGRESIST, static_cast(WORN + rng.rand() % 2), -1, 1, rng.rand(), false, nullptr); + myStats->amulet->isDroppable = rng.rand() % 10 == 0; + } + } + } + } + } + + // torso + Entity* entity = newEntity(my->sprite == 1569 ? 1583 : 1584, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[GREMLIN][1][0]; // 0 + entity->focaly = limbs[GREMLIN][1][1]; // 0 + entity->focalz = limbs[GREMLIN][1][2]; // 0 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // right leg + entity = newEntity(my->sprite == 1569 ? 1580 : 1582, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[GREMLIN][2][0]; // .25 + entity->focaly = limbs[GREMLIN][2][1]; // 0 + entity->focalz = limbs[GREMLIN][2][2]; // 1.5 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // left leg + entity = newEntity(my->sprite == 1569 ? 1579 : 1581, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[GREMLIN][3][0]; // .25 + entity->focaly = limbs[GREMLIN][3][1]; // 0 + entity->focalz = limbs[GREMLIN][3][2]; // 1.5 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // right arm + entity = newEntity(my->sprite == 1569 ? 1573 : 1577, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[GREMLIN][4][0]; // 0 + entity->focaly = limbs[GREMLIN][4][1]; // 0 + entity->focalz = limbs[GREMLIN][4][2]; // 2 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // left arm + entity = newEntity(my->sprite == 1569 ? 1571 : 1575, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[GREMLIN][5][0]; // 0 + entity->focaly = limbs[GREMLIN][5][1]; // 0 + entity->focalz = limbs[GREMLIN][5][2]; // 2 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // world weapon + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[GREMLIN][6][0]; // 2 + entity->focaly = limbs[GREMLIN][6][1]; // 0 + entity->focalz = limbs[GREMLIN][6][2]; // -.5 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + entity->pitch = .25; + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // shield + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[GREMLIN][7][0]; // 0 + entity->focaly = limbs[GREMLIN][7][1]; // 0 + entity->focalz = limbs[GREMLIN][7][2]; // 1.5 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // cloak + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->scalex = 1.01; + entity->scaley = 1.01; + entity->scalez = 1.01; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[GREMLIN][8][0]; // 0 + entity->focaly = limbs[GREMLIN][8][1]; // 0 + entity->focalz = limbs[GREMLIN][8][2]; // 4 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // helmet + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->scalex = 1.01; + entity->scaley = 1.01; + entity->scalez = 1.01; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[GREMLIN][9][0]; // 0 + entity->focaly = limbs[GREMLIN][9][1]; // 0 + entity->focalz = limbs[GREMLIN][9][2]; // -2 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // mask + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[GREMLIN][10][0]; // 0 + entity->focaly = limbs[GREMLIN][10][1]; // 0 + entity->focalz = limbs[GREMLIN][10][2]; // .25 + entity->behavior = &actGnomeLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + if ( multiplayer == CLIENT || MONSTER_INIT ) + { + return; + } +} + +void actMonsterGLimb(Entity* my) +{ + my->actMonsterLimb(true); +} + +void monsterGDie(Entity* my) +{ + if ( !my ) { return; } + for ( int c = 0; c < 10; c++ ) + { + Entity* entity = spawnGib(my); + if ( entity ) + { + if (c < 6) { + entity->sprite = 295 + c; + entity->skill[5] = 1; // poof + } + serverSpawnGibForClient(entity); + } + } + + playSoundEntity(my, 733 + local_rng.rand() % 3, 128); + + my->spawnBlood(); + + my->removeMonsterDeathNodes(); + + list_RemoveNode(my->mynode); + return; +} + +#define MONSTER_GWALKSPEED .13 + +void monsterGMoveBodyparts(Entity* my, Stat* myStats, double dist) +{ + node_t* node; + Entity* entity = nullptr; + Entity* entity2 = nullptr; + Entity* rightbody = nullptr; + Entity* weaponarm = nullptr; + int bodypart; + bool wearingring = false; + + my->focalx = limbs[GREMLIN][0][0]; + my->focaly = limbs[GREMLIN][0][1]; + my->focalz = limbs[GREMLIN][0][2]; + /*if ( my->sprite == 1430 ) + { + my->focalx -= 0.26; + } + else if ( my->sprite == 295 ) + { + my->focalx -= 0.25; + my->focalz -= 0.25; + }*/ + + bool debugModel = monsterDebugModels(my, &dist); + + // set invisibility //TODO: isInvisible()? + if ( multiplayer != CLIENT ) + { + if ( myStats->ring != nullptr ) + if ( myStats->ring->type == RING_INVISIBILITY ) + { + wearingring = true; + } + if ( myStats->cloak != nullptr ) + if ( myStats->cloak->type == CLOAK_INVISIBILITY ) + { + wearingring = true; + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) + { + my->flags[INVISIBLE] = true; + my->flags[BLOCKSIGHT] = false; + bodypart = 0; + for (node = my->children.first; node != nullptr; node = node->next) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + ++bodypart; + continue; + } + if ( bodypart >= LIMB_HUMANOID_WEAPON ) + { + break; + } + entity = (Entity*)node->element; + if ( !entity->flags[INVISIBLE] ) + { + entity->flags[INVISIBLE] = true; + serverUpdateEntityBodypart(my, bodypart); + } + bodypart++; + } + } + else + { + my->flags[INVISIBLE] = false; + my->flags[BLOCKSIGHT] = true; + bodypart = 0; + for (node = my->children.first; node != nullptr; node = node->next) + { + if ( bodypart < 2 ) + { + bodypart++; + continue; + } + if ( bodypart >= 7 ) + { + break; + } + entity = (Entity*)node->element; + if ( entity->flags[INVISIBLE] ) + { + entity->flags[INVISIBLE] = false; + serverUpdateEntityBodypart(my, bodypart); + serverUpdateEntityFlag(my, INVISIBLE); + } + bodypart++; + } + } + + // sleeping + if ( myStats->getEffectActive(EFF_ASLEEP) ) + { + my->z = 4; + my->pitch = PI / 4; + } + else + { + my->z = getNormalHeightMonsterG(*my); + if ( my->monsterAttack == 0 ) + { + if ( debugModel ) + { + my->pitch = my->fskill[0]; + if ( my->fskill[1] > 0.0 ) + { + my->fskill[1] = std::max(0.0, my->fskill[1] - 0.05); + my->z += -3.0 * sqrt(sin(PI * my->fskill[1])); + } + } + else + { + my->pitch = 0; + } + } + } + my->creatureHandleLiftZ(); + } + + Entity* shieldarm = nullptr; + Entity* helmet = nullptr; + Entity* torso = nullptr; + + //Move bodyparts + for (bodypart = 0, node = my->children.first; node != nullptr; node = node->next, bodypart++) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + // post-swing head animation. client doesn't need to adjust the entity pitch, server will handle. + if ( my->monsterAttack != MONSTER_POSE_RANGED_WINDUP3 && my->monsterAttack != MONSTER_POSE_SPECIAL_WINDUP1 + && bodypart == 1 && multiplayer != CLIENT ) + { + limbAnimateToLimit(my, ANIMATE_PITCH, 0.1, 0, false, 0.0); + } + continue; + } + entity = (Entity*)node->element; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + if ( MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 && bodypart == LIMB_HUMANOID_RIGHTARM ) + { + // don't let the creatures's yaw move the casting arm + } + else + { + entity->yaw = my->yaw; + } + if ( bodypart == LIMB_HUMANOID_RIGHTLEG || bodypart == LIMB_HUMANOID_LEFTARM ) + { + my->humanoidAnimateWalk(entity, node, bodypart, MONSTER_GWALKSPEED, dist, 0.4); + } + else if ( bodypart == LIMB_HUMANOID_LEFTLEG || bodypart == LIMB_HUMANOID_RIGHTARM || bodypart == LIMB_HUMANOID_CLOAK ) + { + // left leg, right arm, cloak. + if ( bodypart == LIMB_HUMANOID_RIGHTARM ) + { + weaponarm = entity; + if ( my->monsterAttack > 0 ) + { + if ( my->monsterAttack == MONSTER_POSE_RANGED_WINDUP3 + || my->monsterAttack == MONSTER_POSE_SPECIAL_WINDUP1 ) + { + Entity* rightbody = nullptr; + // set rightbody to left leg. + node_t* rightbodyNode = list_Node(&my->children, LIMB_HUMANOID_LEFTLEG); + if ( rightbodyNode ) + { + rightbody = (Entity*)rightbodyNode->element; + } + else + { + return; + } + + if ( my->monsterAttackTime == 0 ) + { + // init rotations + weaponarm->pitch = 0; + my->monsterArmbended = 0; + my->monsterWeaponYaw = 0; + weaponarm->roll = 0; + weaponarm->skill[1] = 0; + playSoundEntityLocal(my, 736 + local_rng.rand() % 3, 128); + createParticleDot(my); + if ( multiplayer != CLIENT ) + { + my->setEffect(EFF_STUNNED, true, 40, false); + } + } + if ( multiplayer != CLIENT ) + { + // move the head and weapon yaw + limbAnimateToLimit(my, ANIMATE_PITCH, -0.1, 11 * PI / 6, false, 0.0); + limbAnimateToLimit(my, ANIMATE_WEAPON_YAW, 0.05, 2 * PI / 8, false, 0.0); + } + limbAnimateToLimit(weaponarm, ANIMATE_PITCH, -0.25, 7 * PI / 4, true, 0.0); + //limbAnimateToLimit(weaponarm, ANIMATE_ROLL, -0.25, 7 * PI / 4, false, 0.0); + + if ( my->monsterAttackTime >= 4 * ANIMATE_DURATION_WINDUP / (monsterGlobalAnimationMultiplier / 10.0) ) + { + if ( multiplayer != CLIENT ) + { + if ( my->monsterAttack == MONSTER_POSE_SPECIAL_WINDUP1 ) + { + my->attack(MONSTER_POSE_MAGIC_WINDUP3, 0, nullptr); + } + else + { + my->attack(MONSTER_POSE_MELEE_WINDUP1, 0, nullptr); + } + } + } + } + else if ( my->monsterAttack == MONSTER_POSE_MAGIC_WINDUP3 ) + { + if ( multiplayer != CLIENT ) + { + if ( my->monsterAttackTime == 1 ) + { + int spellID = SPELL_SPEED; + if ( my->monsterSpecialState == MONSTER_G_SPECIAL_CAST1 ) + { + castSpell(my->getUID(), getSpellFromID(spellID), true, false); + } + } + } + if ( weaponarm->pitch >= 3 * PI / 2 ) + { + my->monsterArmbended = 1; + } + + if ( weaponarm->skill[1] == 0 ) + { + // chop forwards + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, 0.4, PI / 3, false, 0.0) ) + { + weaponarm->skill[1] = 1; + } + } + else if ( weaponarm->skill[1] >= 1 ) + { + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, -0.25, 7 * PI / 4, false, 0.0) ) + { + Entity* rightbody = nullptr; + // set rightbody to left leg. + node_t* rightbodyNode = list_Node(&my->children, LIMB_HUMANOID_LEFTLEG); + if ( rightbodyNode ) + { + rightbody = (Entity*)rightbodyNode->element; + } + if ( rightbody ) + { + weaponarm->skill[0] = rightbody->skill[0]; + weaponarm->pitch = rightbody->pitch; + } + my->monsterWeaponYaw = 0; + weaponarm->roll = 0; + my->monsterArmbended = 0; + my->monsterAttack = 0; + } + } + } + else + { + if ( multiplayer != CLIENT ) + { + if ( my->monsterAttack == MONSTER_POSE_MAGIC_WINDUP1 && + my->monsterSpecialState == MONSTER_G_SPECIAL_CAST1 + && my->monsterAttackTime == 0 ) + { + my->setEffect(EFF_STUNNED, true, 50, false); + } + } + my->handleWeaponArmAttack(entity); + } + } + } + else if ( bodypart == LIMB_HUMANOID_CLOAK ) + { + entity->pitch = entity->fskill[0]; + } + + my->humanoidAnimateWalk(entity, node, bodypart, MONSTER_GWALKSPEED, dist, 0.4); + + if ( bodypart == LIMB_HUMANOID_CLOAK ) + { + entity->fskill[0] = entity->pitch; + entity->roll = my->roll - fabs(entity->pitch) / 2; + entity->pitch = 0; + } + } + switch ( bodypart ) + { + // torso + case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[GREMLIN][1][0]; + entity->focaly = limbs[GREMLIN][1][1]; + entity->focalz = limbs[GREMLIN][1][2]; + torso = entity; + if ( multiplayer != CLIENT ) + { + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, true, my) ) + { + entity->sprite = my->sprite == 1569 ? 1583 : 1584; + } + else + { + entity->sprite = itemModel(myStats->breastplate, true, my); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, GREMLIN, LIMB_HUMANOID_TORSO); + break; + // right leg + case LIMB_HUMANOID_RIGHTLEG: + entity->focalx = limbs[GREMLIN][2][0]; + entity->focaly = limbs[GREMLIN][2][1]; + entity->focalz = limbs[GREMLIN][2][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->shoes == nullptr ) + { + entity->sprite = my->sprite == 1569 ? 1580 : 1582; + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_RIGHT_OFFSET); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, GREMLIN, LIMB_HUMANOID_RIGHTLEG); + break; + // left leg + case LIMB_HUMANOID_LEFTLEG: + entity->focalx = limbs[GREMLIN][3][0]; + entity->focaly = limbs[GREMLIN][3][1]; + entity->focalz = limbs[GREMLIN][3][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->shoes == nullptr ) + { + entity->sprite = my->sprite == 1569 ? 1579 : 1581; + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_LEFT_OFFSET); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, GREMLIN, LIMB_HUMANOID_LEFTLEG); + break; + // right arm + case LIMB_HUMANOID_RIGHTARM: + { + if ( multiplayer != CLIENT ) + { + if ( myStats->gloves == nullptr ) + { + entity->sprite = my->sprite == 1569 ? 1573 : 1577; + } + else + { + if ( setGloveSprite(myStats, entity, SPRITE_GLOVE_RIGHT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( entity->skill[7] == 0 ) + { + if ( entity->sprite == 1573 || entity->sprite == 1577 ) + { + // these are the default arms. + // chances are they may be wrong if sent by the server, + } + else + { + // otherwise we're being sent gloves armor etc so it's probably right. + entity->skill[7] = entity->sprite; + } + } + if ( entity->skill[7] == 0 ) + { + // we set this ourselves until proper initialisation. + entity->sprite = my->sprite == 1569 ? 1573 : 1577; + } + else + { + entity->sprite = entity->skill[7]; + } + } + + node_t* weaponNode = list_Node(&my->children, 7); + bool bentArm = false; + if ( weaponNode ) + { + Entity* weapon = (Entity*)weaponNode->element; + if ( my->monsterArmbended || (weapon->flags[INVISIBLE] && my->monsterState == MONSTER_STATE_WAIT) ) + { + entity->focalx = limbs[GREMLIN][4][0]; // 0 + entity->focaly = limbs[GREMLIN][4][1]; // 0 + entity->focalz = limbs[GREMLIN][4][2]; // 2 + } + else + { + entity->focalx = limbs[GREMLIN][4][0] + 1; // 1 + entity->focaly = limbs[GREMLIN][4][1] + 0.25; // 0 + entity->focalz = limbs[GREMLIN][4][2] - 0.75; // 1 + if ( entity->sprite == 1573 || entity->sprite == 1577 ) + { + entity->sprite += 1; + } + else + { + entity->sprite += 2; + } + } + } + my->setHumanoidLimbOffset(entity, GREMLIN, LIMB_HUMANOID_RIGHTARM); + entity->yaw += MONSTER_WEAPONYAW; + break; + // left arm + } + case LIMB_HUMANOID_LEFTARM: + { + if ( multiplayer != CLIENT ) + { + if ( myStats->gloves == nullptr ) + { + entity->sprite = my->sprite == 1569 ? 1571 : 1575; + } + else + { + if ( setGloveSprite(myStats, entity, SPRITE_GLOVE_LEFT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( entity->skill[7] == 0 ) + { + if ( entity->sprite == 1571 || entity->sprite == 1575 ) + { + // these are the default arms. + // chances are they may be wrong if sent by the server, + } + else + { + // otherwise we're being sent gloves armor etc so it's probably right. + entity->skill[7] = entity->sprite; + } + } + if ( entity->skill[7] == 0 ) + { + // we set this ourselves until proper initialisation. + entity->sprite = my->sprite == 1569 ? 1571 : 1575; + } + else + { + entity->sprite = entity->skill[7]; + } + } + + shieldarm = entity; + node_t* shieldNode = list_Node(&my->children, 8); + if ( shieldNode ) + { + Entity* shield = (Entity*)shieldNode->element; + if ( shield->flags[INVISIBLE] && my->monsterState == MONSTER_STATE_WAIT ) + { + entity->focalx = limbs[GREMLIN][5][0]; // 0 + entity->focaly = limbs[GREMLIN][5][1]; // 0 + entity->focalz = limbs[GREMLIN][5][2]; // 2 + } + else + { + entity->focalx = limbs[GREMLIN][5][0] + 1; // 1 + entity->focaly = limbs[GREMLIN][5][1] - 0.25; // 0 + entity->focalz = limbs[GREMLIN][5][2] - 0.75; // 1 + if ( entity->sprite == 1571 || entity->sprite == 1575 ) + { + entity->sprite += 1; + } + else + { + entity->sprite += 2; + } + } + } + my->setHumanoidLimbOffset(entity, GREMLIN, LIMB_HUMANOID_LEFTARM); + if ( my->monsterDefend && my->monsterAttack == 0 ) + { + MONSTER_SHIELDYAW = PI / 5; + } + else + { + MONSTER_SHIELDYAW = 0; + } + entity->yaw += MONSTER_SHIELDYAW; + break; + } + // weapon + case LIMB_HUMANOID_WEAPON: + if ( multiplayer != CLIENT ) + { + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->sprite = itemModel(myStats->weapon); + if ( itemCategory(myStats->weapon) == SPELLBOOK ) + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + if ( weaponarm != nullptr ) + { + my->handleHumanoidWeaponLimb(entity, weaponarm); + } + break; + // shield + case LIMB_HUMANOID_SHIELD: + if ( multiplayer != CLIENT ) + { + if ( myStats->shield == nullptr ) + { + entity->flags[INVISIBLE] = true; + entity->sprite = 0; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(myStats->shield); + if ( itemTypeIsQuiver(myStats->shield->type) ) + { + entity->handleQuiverThirdPersonModel(*myStats); + } + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + my->handleHumanoidShieldLimb(entity, shieldarm); + break; + // cloak + case LIMB_HUMANOID_CLOAK: + entity->focalx = limbs[GREMLIN][8][0]; + entity->focaly = limbs[GREMLIN][8][1]; + entity->focalz = limbs[GREMLIN][8][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(myStats->cloak); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + //if ( torso->sprite != 1583 && torso->sprite != 1584 ) + //{ + // // push back for larger armors + //} + entity->x -= cos(my->yaw) * 1.0; + entity->y -= sin(my->yaw) * 1.0; + entity->yaw += PI / 2; + my->setHumanoidLimbOffset(entity, GREMLIN, LIMB_HUMANOID_CLOAK); + break; + // helm + case LIMB_HUMANOID_HELMET: + helmet = entity; + entity->focalx = limbs[GREMLIN][9][0]; // 0 + entity->focaly = limbs[GREMLIN][9][1]; // 0 + entity->focalz = limbs[GREMLIN][9][2]; // -2 + entity->pitch = my->pitch; + entity->roll = 0; + if ( multiplayer != CLIENT ) + { + entity->sprite = itemModel(myStats->helmet); + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + my->setHelmetLimbOffset(entity); + break; + // mask + case LIMB_HUMANOID_MASK: + entity->focalx = limbs[GREMLIN][10][0]; // 0 + entity->focaly = limbs[GREMLIN][10][1]; // 0 + entity->focalz = limbs[GREMLIN][10][2]; // .25 + entity->pitch = my->pitch; + entity->roll = PI / 2; + if ( multiplayer != CLIENT ) + { + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( myStats->mask != nullptr ) + { + if ( myStats->mask->type == TOOL_GLASSES ) + { + entity->sprite = 165; // GlassesWorn.vox + } + else if ( myStats->mask->type == MONOCLE ) + { + entity->sprite = 1196; // monocleWorn.vox + } + else + { + entity->sprite = itemModel(myStats->mask); + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + + if ( entity->sprite == items[MASK_SHAMAN].index ) + { + entity->roll = 0; + my->setHelmetLimbOffset(entity); + my->setHelmetLimbOffsetWithMask(helmet, entity); + } + else if ( EquipmentModelOffsets.modelOffsetExists(GREMLIN, entity->sprite, my->sprite) ) + { + my->setHelmetLimbOffset(entity); + my->setHelmetLimbOffsetWithMask(helmet, entity); + } + else + { + entity->focalx = limbs[GREMLIN][10][0] + .35; // .35 + entity->focaly = limbs[GREMLIN][10][1] - 2; // -2 + entity->focalz = limbs[GREMLIN][10][2]; // .25 + } + break; + default: + break; + } + } + // rotate shield a bit + node_t* shieldNode = list_Node(&my->children, 8); + if ( shieldNode ) + { + Entity* shieldEntity = (Entity*)shieldNode->element; + if ( shieldEntity->sprite != items[TOOL_TORCH].index && shieldEntity->sprite != items[TOOL_LANTERN].index && shieldEntity->sprite != items[TOOL_CRYSTALSHARD].index ) + { + shieldEntity->yaw -= PI / 6; + } + } + if ( MONSTER_ATTACK > 0 && MONSTER_ATTACK <= MONSTER_POSE_MAGIC_CAST3 ) + { + MONSTER_ATTACKTIME++; + } + else if ( MONSTER_ATTACK == 0 ) + { + MONSTER_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } +} + +void Entity::monsterGChooseWeapon(const Entity* target, double dist) +{ + if ( monsterSpecialState != 0 ) + { + //Holding a weapon assigned from the special attack. Don't switch weapons. + //messagePlayer() + return; + } + + Stat* myStats = getStats(); + if ( !myStats ) + { + return; + } + + if ( myStats->weapon && (itemCategory(myStats->weapon) == SPELLBOOK) ) + { + return; + } + + bool inMeleeRange = monsterInMeleeRange(target, dist); + + //if ( inMeleeRange ) + //{ + // //if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 && local_rng.rand() % 10 == 0 ) + // //{ + // // bool tryThrow = true; + // // if ( tryThrow ) + // // { + // // node_t* thrownNode = itemNodeInInventory(myStats, -1, THROWN); + // // if ( thrownNode ) + // // { + // // bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, thrownNode, false, true); + // // if ( !swapped ) + // // { + // // //Don't return, make sure holding a melee weapon at least. + // // } + // // else + // // { + // // monsterSpecialState = MONSTER_M_SPECIAL_THROW; + // // return; + // // } + // // } + // // } + // //} + + // //Switch to a melee weapon if not already wielding one. Unless monster special state is overriding the AI. + // if ( !myStats->weapon || !isMeleeWeapon(*myStats->weapon) ) + // { + // node_t* weaponNode = getMeleeWeaponItemNodeInInventory(myStats); + // if ( !weaponNode ) + // { + // /*if ( myStats->weapon && myStats->weapon->type == MAGICSTAFF_SLOW ) + // { + // monsterUnequipSlotFromCategory(myStats, &myStats->weapon, MAGICSTAFF); + // }*/ + // return; //Resort to fists. + // } + + // bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, true); + // if ( !swapped ) + // { + // //Don't return so that monsters will at least equip ranged weapons in melee range if they don't have anything else. + // } + // else + // { + // return; + // } + // } + // else + // { + // return; + // } + //} + + if ( myStats->getAttribute("monster_g_type") == "berserker" ) + { + int roll = 10; + if ( myStats->getEffectActive(EFF_SLOW) || myStats->HP <= myStats->MAXHP / 2 ) + { + roll = 3; + } + if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 + && local_rng.rand() % roll == 0 && !myStats->getEffectActive(EFF_FAST) ) + { + if ( dist >= TOUCHRANGE ) + { + monsterSpecialState = MONSTER_G_SPECIAL_CAST1; + } + } + return; + } + + //Switch to a thrown weapon or a ranged weapon + if ( dist < 80.0 ) + { + int tiles = dist / 16; + //First search the inventory for a THROWN weapon. + node_t* weaponNode = nullptr; + int roll = 10; + if ( tiles <= 3 ) + { + roll = 5; + } + if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 + && local_rng.rand() % roll == 0 ) + { + Stat* targetStats = target ? target->getStats() : nullptr; + if ( (dist > STRIKERANGE) || (targetStats && targetStats->getEffectActive(EFF_MAGIC_GREASE)) ) + { + if ( (targetStats && targetStats->getEffectActive(EFF_MAGIC_GREASE)) ) + { + // first item + weaponNode = itemNodeInInventory(myStats, -1, POTION); + } + else + { + weaponNode = itemNodeInInventory(myStats, -1, POTION, true); // random + } + } + if ( !weaponNode || local_rng.rand() % 4 == 0 ) + { + weaponNode = itemNodeInInventory(myStats, -1, THROWN, true); + } + if ( weaponNode ) + { + if ( swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, true) ) + { + monsterSpecialState = MONSTER_G_SPECIAL_THROW; + return; + } + } + } + //if ( !weaponNode ) + //{ + // //If couldn't find any, search the inventory for a ranged weapon. + // weaponNode = getRangedWeaponItemNodeInInventory(myStats, true); + //} + + bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, true); + return; + } + + return; +} \ No newline at end of file diff --git a/src/monster_ghoul.cpp b/src/monster_ghoul.cpp index b58412cbb..7e2741790 100644 --- a/src/monster_ghoul.cpp +++ b/src/monster_ghoul.cpp @@ -76,7 +76,7 @@ void initGhoul(Entity* my, Stat* myStats) myStats->PER = 10; if ( rng.rand() % 2 == 0 ) { - myStats->EFFECTS[EFF_VAMPIRICAURA] = true; + myStats->setEffectActive(EFF_VAMPIRICAURA, 1); myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] = -1; } } @@ -133,7 +133,7 @@ void initGhoul(Entity* my, Stat* myStats) { if ( !strncmp(map.name, "Bram's Castle", 13) ) { - myStats->EFFECTS[EFF_VAMPIRICAURA] = true; + myStats->setEffectActive(EFF_VAMPIRICAURA, 1); myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] = -1; } } @@ -321,7 +321,7 @@ void ghoulMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -372,6 +372,12 @@ void ghoulMoveBodyparts(Entity* my, Stat* myStats, double dist) bodypart++; } } + + if ( my->z <= -0.25 ) + { + my->z = -0.25; + my->creatureHandleLiftZ(); + } } //Move bodyparts diff --git a/src/monster_gnome.cpp b/src/monster_gnome.cpp index 8e8251b5e..3565d3180 100644 --- a/src/monster_gnome.cpp +++ b/src/monster_gnome.cpp @@ -210,9 +210,9 @@ void initGnome(Entity* my, Stat* myStats) // boss variants // random effects - if ( rng.rand() % 8 == 0 ) + if ( rng.rand() % 8 == 0 && myStats->getAttribute("spawn_no_sleep") == "" ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 1800; } @@ -253,10 +253,34 @@ void initGnome(Entity* my, Stat* myStats) case 2: if ( rng.rand() % 10 == 0 ) { - if ( rng.rand() % 2 == 0 ) + if ( rng.rand() % 5 == 0 ) { newItem(MASK_PIPE, SERVICABLE, -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); } + else if ( rng.rand() % 2 == 0 ) + { + if ( gnome_type == "gnome2" || gnome_type == "gnome2F" ) + { + newItem(BANDIT_BREASTPIECE, static_cast(1 + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + } + else + { + switch ( rng.rand() % 3 ) + { + case 0: + newItem(HAT_FELT, static_cast(1 + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + break; + case 1: + newItem(LOAFERS, static_cast(1 + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + break; + case 2: + newItem(TUNIC_BLOUSE, static_cast(1 + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + break; + default: + break; + } + } + } else { int i = 1 + rng.rand() % 4; @@ -902,6 +926,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) int bodypart; bool wearingring = false; + bool debugModel = monsterDebugModels(my, &dist); GnomeVariant gnomeVariant = GNOME_DEFAULT; if ( myStats ) { @@ -913,264 +938,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) { gnomeVariant = GNOME_THIEF_MELEE; } -//#ifndef NDEBUG -// static bool forceWalk = false; -// if ( keystatus[SDLK_KP_5] ) -// { -// keystatus[SDLK_KP_5] = 0; -// forceWalk = !forceWalk; -// } -// if ( keystatus[SDLK_KP_6] ) -// { -// myStats->EFFECTS[EFF_STUNNED] = !myStats->EFFECTS[EFF_STUNNED]; -// } -// if ( forceWalk ) -// { -// dist = 0.15; -// } -// -// if ( keystatus[SDLK_9] ) -// { -// keystatus[SDLK_9] = 0; -// if ( myStats->helmet ) -// { -// myStats->helmet->appearance++; -// } -// } -// if ( keystatus[SDLK_0] ) -// { -// keystatus[SDLK_0] = 0; -// if ( myStats->mask ) -// { -// myStats->mask->appearance++; -// } -// } -// if ( keystatus[SDLK_6] ) -// { -// keystatus[SDLK_6] = 0; -// if ( my->sprite == 295 ) -// { -// my->sprite = 1426; -// } -// else if ( my->sprite == 1426 ) -// { -// my->sprite = 1430; -// } -// else -// { -// my->sprite = 295; -// } -// } -// if ( keystatus[SDLK_7] ) -// { -// keystatus[SDLK_7] = 0; -// if ( myStats->shoes ) -// { -// while ( true ) -// { -// int type = myStats->shoes->type; -// type++; -// if ( type >= NUMITEMS ) -// { -// if ( myStats->shoes->node ) -// { -// list_RemoveNode(myStats->shoes->node); -// } -// else -// { -// free(myStats->shoes); -// } -// myStats->shoes = nullptr; -// break; -// } -// myStats->shoes->type = (ItemType)type; -// if ( items[myStats->shoes->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BOOTS ) -// { -// break; -// } -// } -// } -// else -// { -// myStats->shoes = newItem(LEATHER_BOOTS, EXCELLENT, 0, 1, 0, true, nullptr); -// } -// } -// if ( keystatus[SDLK_8] ) -// { -// keystatus[SDLK_8] = 0; -// if ( myStats->gloves ) -// { -// while ( true ) -// { -// int type = myStats->gloves->type; -// type++; -// if ( type >= NUMITEMS ) -// { -// if ( myStats->gloves->node ) -// { -// list_RemoveNode(myStats->gloves->node); -// } -// else -// { -// free(myStats->gloves); -// } -// myStats->gloves = nullptr; -// break; -// } -// myStats->gloves->type = (ItemType)type; -// if ( items[myStats->gloves->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_GLOVES ) -// { -// break; -// } -// } -// } -// else -// { -// myStats->gloves = newItem(GLOVES, EXCELLENT, 0, 1, 0, true, nullptr); -// } -// } -// if ( keystatus[SDLK_g] ) -// { -// keystatus[SDLK_g] = 0; -// if ( myStats->helmet ) -// { -// if ( keystatus[SDLK_LSHIFT] ) -// { -// while ( true ) -// { -// int type = myStats->helmet->type; -// type--; -// if ( type < 0 ) -// { -// type = NUMITEMS - 1; -// } -// myStats->helmet->type = (ItemType)type; -// if ( items[myStats->helmet->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_HELM ) -// { -// break; -// } -// } -// } -// else -// { -// while ( true ) -// { -// int type = myStats->helmet->type; -// type++; -// if ( type >= NUMITEMS ) -// { -// type = 0; -// } -// myStats->helmet->type = (ItemType)type; -// if ( items[myStats->helmet->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_HELM ) -// { -// break; -// } -// } -// } -// } -// else -// { -// myStats->helmet = newItem(LEATHER_HELM, EXCELLENT, 0, 1, 0, true, nullptr); -// } -// } -// if ( keystatus[SDLK_j] ) -// { -// keystatus[SDLK_j] = 0; -// if ( myStats->breastplate ) -// { -// if ( keystatus[SDLK_LSHIFT] ) -// { -// while ( true ) -// { -// int type = myStats->breastplate->type; -// type--; -// if ( type < 0 ) -// { -// type = NUMITEMS - 1; -// } -// myStats->breastplate->type = (ItemType)type; -// if ( items[myStats->breastplate->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BREASTPLATE ) -// { -// break; -// } -// } -// } -// else -// { -// while ( true ) -// { -// int type = myStats->breastplate->type; -// type++; -// if ( type >= NUMITEMS ) -// { -// if ( myStats->breastplate->node ) -// { -// list_RemoveNode(myStats->breastplate->node); -// } -// else -// { -// free(myStats->breastplate); -// } -// myStats->breastplate = nullptr; -// break; -// } -// myStats->breastplate->type = (ItemType)type; -// if ( items[myStats->breastplate->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BREASTPLATE ) -// { -// break; -// } -// } -// } -// } -// else -// { -// myStats->breastplate = newItem(LEATHER_BREASTPIECE, EXCELLENT, 0, 1, 0, true, nullptr); -// } -// } -// if ( keystatus[SDLK_h] ) -// { -// keystatus[SDLK_h] = 0; -// if ( myStats->mask ) -// { -// if ( keystatus[SDLK_LSHIFT] ) -// { -// while ( true ) -// { -// int type = myStats->mask->type; -// type--; -// if ( type < 0 ) -// { -// type = NUMITEMS - 1; -// } -// myStats->mask->type = (ItemType)type; -// if ( items[myStats->mask->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_MASK ) -// { -// break; -// } -// } -// } -// else -// { -// while ( true ) -// { -// int type = myStats->mask->type; -// type++; -// if ( type >= NUMITEMS ) -// { -// type = 0; -// } -// myStats->mask->type = (ItemType)type; -// if ( items[myStats->mask->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_MASK ) -// { -// break; -// } -// } -// } -// } -// } -//#endif -} + } my->focalx = limbs[GNOME][0][0]; my->focaly = limbs[GNOME][0][1]; @@ -1198,7 +966,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -1251,20 +1019,22 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 4; my->pitch = PI / 4; } else { - my->z = 2.75; + my->z = 2.25; my->pitch = 0; } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; Entity* helmet = nullptr; + Entity* torso = nullptr; std::string gnome_type = my->sprite == 1426 ? "gnome2" : my->sprite == 1430 ? "gnome2F" : ""; @@ -1279,6 +1049,12 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->x = my->x; entity->y = my->y; entity->z = my->z; + + if ( bodypart <= LIMB_HUMANOID_CLOAK ) + { + entity->z += 0.5; + } + if ( MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 && bodypart == LIMB_HUMANOID_RIGHTARM ) { // don't let the creatures's yaw move the casting arm @@ -1320,12 +1096,16 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; entity->focalx = limbs[GNOME][1][0]; entity->focaly = limbs[GNOME][1][1]; entity->focalz = limbs[GNOME][1][2]; + torso = entity; if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, true, my) ) { if ( gnomeVariant == GNOME_THIEF_MELEE ) { @@ -1342,7 +1122,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) } else { - entity->sprite = itemModel(myStats->breastplate, true); + entity->sprite = itemModel(myStats->breastplate, true, my); } if ( multiplayer == SERVER ) { @@ -1662,7 +1442,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1735,7 +1515,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1782,7 +1562,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->focalz = limbs[GNOME][8][2]; if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1825,16 +1605,14 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->flags[INVISIBLE] = true; } } - entity->x -= cos(my->yaw) * 1.5; - entity->y -= sin(my->yaw) * 1.5; - entity->yaw += PI / 2; - - if ( entity->sprite != 1427 && entity->sprite != 1431 && entity->sprite != 296 ) + if ( torso->sprite != 1427 && torso->sprite != 1431 && torso->sprite != 296 ) { // push back for larger armors entity->x -= cos(my->yaw) * 1.0; entity->y -= sin(my->yaw) * 1.0; } + entity->yaw += PI / 2; + my->setHumanoidLimbOffset(entity, GNOME, LIMB_HUMANOID_CLOAK); break; // helm case LIMB_HUMANOID_HELMET: @@ -1847,7 +1625,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1900,7 +1678,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1964,7 +1742,7 @@ void gnomeMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(GNOME, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(GNOME, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); diff --git a/src/monster_goatman.cpp b/src/monster_goatman.cpp index 5f350de94..11b921e63 100644 --- a/src/monster_goatman.cpp +++ b/src/monster_goatman.cpp @@ -773,7 +773,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -826,7 +826,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 2.5; my->pitch = PI / 4; @@ -839,6 +839,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) my->pitch = 0; } } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; @@ -905,7 +906,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) weaponarm->skill[1] = 0; if ( multiplayer != CLIENT ) { - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 40; } } @@ -950,9 +951,15 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[GOATMAN][1][0]; + entity->focaly = limbs[GOATMAN][1][1]; + entity->focalz = limbs[GOATMAN][1][2]; if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) { entity->sprite = my->sprite == 1025 ? 1028 : @@ -960,7 +967,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); } if ( multiplayer == SERVER ) { @@ -1125,7 +1132,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1198,7 +1205,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1242,7 +1249,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1300,7 +1307,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1353,17 +1360,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1428,7 +1425,7 @@ void goatmanMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(GOATMAN, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(GOATMAN, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); diff --git a/src/monster_goblin.cpp b/src/monster_goblin.cpp index 5c3147a2d..9ce0e489f 100644 --- a/src/monster_goblin.cpp +++ b/src/monster_goblin.cpp @@ -99,9 +99,9 @@ void initGoblin(Entity* my, Stat* myStats) } // random effects - if ( rng.rand() % 8 == 0 ) + if ( rng.rand() % 8 == 0 && myStats->getAttribute("spawn_no_sleep") == "" ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 1800; } @@ -564,7 +564,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -617,7 +617,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 2.5; my->pitch = PI / 4; @@ -630,6 +630,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) my->pitch = 0; } } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; @@ -688,9 +689,15 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[GOBLIN][1][0]; + entity->focaly = limbs[GOBLIN][1][1]; + entity->focalz = limbs[GOBLIN][1][2]; if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) { const int torso_sprite = my->sprite == 1035 ? 1038 : (my->sprite == 1039 ? 1042 : 183); @@ -698,7 +705,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); } if ( multiplayer == SERVER ) { @@ -867,7 +874,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -940,7 +947,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -984,7 +991,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1042,7 +1049,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1095,7 +1102,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1161,7 +1168,7 @@ void goblinMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(GOBLIN, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(GOBLIN, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); diff --git a/src/monster_human.cpp b/src/monster_human.cpp index 4d1312be7..1ab5a10cc 100644 --- a/src/monster_human.cpp +++ b/src/monster_human.cpp @@ -80,7 +80,7 @@ void initHuman(Entity* my, Stat* myStats) specialMonsterType = rng.rand() % 10; } } - switch ( rng.rand() % 10 ) + switch ( specialMonsterType ) { case 0: // red riding hood @@ -366,9 +366,9 @@ void initHuman(Entity* my, Stat* myStats) } // random effects - if ( rng.rand() % 10 == 0 && strcmp(myStats->name, "scriptNPC") && myStats->MISC_FLAGS[STAT_FLAG_NPC] == 0 ) + if ( rng.rand() % 10 == 0 && strcmp(myStats->name, "scriptNPC") && myStats->MISC_FLAGS[STAT_FLAG_NPC] == 0 && myStats->getAttribute("spawn_no_sleep") == "" ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 1800; } @@ -423,6 +423,8 @@ void initHuman(Entity* my, Stat* myStats) break; } } + myStats->HP += HP_MOD; + myStats->MAXHP += HP_MOD; myStats->EXP -= 100; } } @@ -993,7 +995,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -1046,7 +1048,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 1.5; my->pitch = PI / 4; @@ -1066,6 +1068,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->z -= 1; // floating } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; @@ -1130,9 +1133,15 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[HUMAN][1][0]; + entity->focaly = limbs[HUMAN][1][1]; + entity->focalz = limbs[HUMAN][1][2]; if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) { switch ( myStats->stat_appearance / 6 ) { @@ -1149,7 +1158,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); } if ( multiplayer == SERVER ) { @@ -1200,9 +1209,10 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) } } - entity->x -= .25 * cos(my->yaw); + /*entity->x -= .25 * cos(my->yaw); entity->y -= .25 * sin(my->yaw); - entity->z += 2.5; + entity->z += 2.5;*/ + my->setHumanoidLimbOffset(entity, HUMAN, LIMB_HUMANOID_TORSO); break; // right leg case LIMB_HUMANOID_RIGHTLEG: @@ -1463,14 +1473,8 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) } } - entity->x += 2.5 * cos(my->yaw + PI / 2) - .20 * cos(my->yaw); - entity->y += 2.5 * sin(my->yaw + PI / 2) - .20 * sin(my->yaw); - entity->z += 1.5; + my->setHumanoidLimbOffset(entity, HUMAN, LIMB_HUMANOID_RIGHTARM); entity->yaw += MONSTER_WEAPONYAW; - if ( my->z >= 1.4 && my->z <= 1.6 ) - { - entity->pitch = 0; - } break; } // left arm @@ -1570,13 +1574,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->sprite += 2; } } - entity->x -= 2.5 * cos(my->yaw + PI / 2) + .20 * cos(my->yaw); - entity->y -= 2.5 * sin(my->yaw + PI / 2) + .20 * sin(my->yaw); - entity->z += 1.5; - if ( my->z >= 1.4 && my->z <= 1.6 ) - { - entity->pitch = 0; - } + my->setHumanoidLimbOffset(entity, HUMAN, LIMB_HUMANOID_LEFTARM); if ( my->monsterDefend && my->monsterAttack == 0 ) { MONSTER_SHIELDYAW = PI / 5; @@ -1592,7 +1590,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1665,7 +1663,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1709,7 +1707,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1767,7 +1765,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1820,17 +1818,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1896,7 +1884,7 @@ void humanMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(HUMAN, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(HUMAN, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); diff --git a/src/monster_imp.cpp b/src/monster_imp.cpp index 00abf9e74..377de1f50 100644 --- a/src/monster_imp.cpp +++ b/src/monster_imp.cpp @@ -56,12 +56,12 @@ void initImp(Entity* my, Stat* myStats) // boss variants // random effects - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; - if ( rng.rand() % 4 == 0 && strncmp(map.name, "Hell Boss", 9) ) + if ( rng.rand() % 4 == 0 && strncmp(map.name, "Hell Boss", 9) && myStats->getAttribute("spawn_no_sleep") == "" ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 3600; } @@ -284,7 +284,7 @@ void impMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -337,7 +337,7 @@ void impMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->pitch = PI / 4; } @@ -346,8 +346,11 @@ void impMoveBodyparts(Entity* my, Stat* myStats, double dist) my->pitch = 0; } + my->z = -4.5; + my->creatureHandleLiftZ(); + // imps are always flying - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; } diff --git a/src/monster_incubus.cpp b/src/monster_incubus.cpp index 4dd0473aa..f64e2f866 100644 --- a/src/monster_incubus.cpp +++ b/src/monster_incubus.cpp @@ -83,60 +83,6 @@ void initIncubus(Entity* my, Stat* myStats) myStats->GOLD = 0; myStats->weapon = newItem(TOOL_WHIP, EXCELLENT, 0, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, true, nullptr); - /*if ( parentStats->shield ) - { - myStats->shield = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->shield, parentStats->shield); - myStats->shield->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - } - if ( parentStats->helmet ) - { - myStats->helmet = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->helmet, parentStats->helmet); - myStats->helmet->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - } - if ( parentStats->breastplate ) - { - myStats->breastplate = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->breastplate, parentStats->breastplate); - myStats->breastplate->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - } - if ( parentStats->shoes ) - { - myStats->shoes = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->shoes, parentStats->shoes); - myStats->shoes->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - } - if ( parentStats->gloves ) - { - myStats->gloves = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->gloves, parentStats->gloves); - myStats->gloves->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - } - if ( parentStats->cloak ) - { - myStats->cloak = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->cloak, parentStats->cloak); - myStats->cloak->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - } - if ( parentStats->ring ) - { - myStats->ring = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->ring, parentStats->ring); - myStats->ring->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - } - if ( parentStats->amulet ) - { - myStats->amulet = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->amulet, parentStats->amulet); - myStats->amulet->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - } - if ( parentStats->mask ) - { - myStats->mask = newItem(BRONZE_SWORD, EXCELLENT, 0, 1, 0, true, nullptr); - copyItem(myStats->mask, parentStats->mask); - myStats->mask->appearance = MONSTER_ITEM_UNDROPPABLE_APPEARANCE; - }*/ } } } @@ -186,13 +132,21 @@ void initIncubus(Entity* my, Stat* myStats) newItem(SPELLBOOK_STEAL_WEAPON, DECREPIT, 0, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); newItem(SPELLBOOK_CHARM_MONSTER, DECREPIT, 0, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); - if ( rng.rand() % 4 == 0 ) // 1 in 4 + if ( myStats->getAttribute("special_npc") == "johann" ) { - newItem(POTION_CONFUSION, SERVICABLE, 0, 0 + rng.rand() % 3, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + //newItem(POTION_CONFUSION, SERVICABLE, 0, 10 + rng.rand() % 3, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + //newItem(POTION_THUNDERSTORM, SERVICABLE, 0, 10 + rng.rand() % 3, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); } - else // 3 in 4 + else { - newItem(POTION_BOOZE, SERVICABLE, 0, 1 + rng.rand() % 3, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + if ( rng.rand() % 4 == 0 ) // 1 in 4 + { + newItem(POTION_CONFUSION, SERVICABLE, 0, 0 + rng.rand() % 3, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + } + else // 3 in 4 + { + newItem(POTION_BOOZE, SERVICABLE, 0, 1 + rng.rand() % 3, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + } } @@ -312,7 +266,7 @@ void initIncubus(Entity* my, Stat* myStats) } // torso - Entity* entity = newEntity(446, 1, map.entities, nullptr); //Limb entity. + Entity* entity = newEntity(my->sprite == 445 ? 446 : 1827, 1, map.entities, nullptr); //Limb entity. entity->sizex = 4; entity->sizey = 4; entity->skill[2] = my->getUID(); @@ -331,7 +285,7 @@ void initIncubus(Entity* my, Stat* myStats) my->bodyparts.push_back(entity); // right leg - entity = newEntity(450, 1, map.entities, nullptr); //Limb entity. + entity = newEntity(my->sprite == 445 ? 450 : 1831, 1, map.entities, nullptr); //Limb entity. entity->sizex = 4; entity->sizey = 4; entity->skill[2] = my->getUID(); @@ -350,7 +304,7 @@ void initIncubus(Entity* my, Stat* myStats) my->bodyparts.push_back(entity); // left leg - entity = newEntity(449, 1, map.entities, nullptr); //Limb entity. + entity = newEntity(my->sprite == 445 ? 449 : 1830, 1, map.entities, nullptr); //Limb entity. entity->sizex = 4; entity->sizey = 4; entity->skill[2] = my->getUID(); @@ -369,7 +323,7 @@ void initIncubus(Entity* my, Stat* myStats) my->bodyparts.push_back(entity); // right arm - entity = newEntity(448, 1, map.entities, nullptr); //595 //Limb entity. + entity = newEntity(my->sprite == 445 ? 448 : 1829, 1, map.entities, nullptr); //595 //Limb entity. entity->sizex = 4; entity->sizey = 4; entity->skill[2] = my->getUID(); @@ -388,7 +342,7 @@ void initIncubus(Entity* my, Stat* myStats) my->bodyparts.push_back(entity); // left arm - entity = newEntity(447, 1, map.entities, nullptr); //447 //Limb entity. + entity = newEntity(my->sprite == 445 ? 447 : 1828, 1, map.entities, nullptr); //447 //Limb entity. entity->sizex = 4; entity->sizey = 4; entity->skill[2] = my->getUID(); @@ -542,8 +496,8 @@ void incubusDie(Entity* my) { Entity* gib = spawnGib(my); if (c < 6) { - gib->sprite = 445 + c; - gib->skill[5] = 1; // poof + gib->sprite = my->sprite + c; + gib->skill[5] = 1; // poof } serverSpawnGibForClient(gib); } @@ -586,7 +540,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) wearingring = true; } } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -652,7 +606,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 1.5; } @@ -660,6 +614,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->z = -1; } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; @@ -833,7 +788,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { // set overshoot for head, freeze incubus in place my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); if ( my->monsterAttack == MONSTER_POSE_INCUBUS_TAUNT ) { myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 250; @@ -943,7 +898,14 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->focalx = limbs[INCUBUS][1][0]; + entity->focaly = limbs[INCUBUS][1][1]; + entity->focalz = limbs[INCUBUS][1][2]; + entity->sprite = my->sprite == 445 ? 446 : 1827; my->setHumanoidLimbOffset(entity, INCUBUS, LIMB_HUMANOID_TORSO); + entity->scalex = 0.975; + entity->scaley = 0.975; + entity->scalez = 0.975; break; // right leg case LIMB_HUMANOID_RIGHTLEG: @@ -951,7 +913,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { if ( myStats->shoes == nullptr ) { - entity->sprite = 450; + entity->sprite = my->sprite == 445 ? 450 : 1831; } else { @@ -987,7 +949,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { if ( myStats->shoes == nullptr ) { - entity->sprite = 449; + entity->sprite = my->sprite == 445 ? 449 : 1830; } else { @@ -1030,7 +992,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->focalx = limbs[INCUBUS][4][0] - 0.25; // 0 entity->focaly = limbs[INCUBUS][4][1] - 0.25; // 0 entity->focalz = limbs[INCUBUS][4][2]; // 2 - entity->sprite = 448; + entity->sprite = my->sprite == 445 ? 448 : 1829; if ( my->monsterAttack == 0 ) { entity->roll = -PI / 32; @@ -1042,7 +1004,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->focalx = limbs[INCUBUS][4][0]; entity->focaly = limbs[INCUBUS][4][1]; entity->focalz = limbs[INCUBUS][4][2]; - entity->sprite = 595; + entity->sprite = my->sprite == 445 ? 595 : 1833; } } my->setHumanoidLimbOffset(entity, INCUBUS, LIMB_HUMANOID_RIGHTARM); @@ -1063,7 +1025,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->focalx = limbs[INCUBUS][5][0] - 0.25; // 0 entity->focaly = limbs[INCUBUS][5][1] + 0.25; // 0 entity->focalz = limbs[INCUBUS][5][2]; // 2 - entity->sprite = 447; + entity->sprite = my->sprite == 445 ? 447 : 1828; if ( my->monsterAttack == 0 ) { entity->roll = PI / 32; @@ -1075,7 +1037,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->focalx = limbs[INCUBUS][5][0]; entity->focaly = limbs[INCUBUS][5][1]; entity->focalz = limbs[INCUBUS][5][2]; - entity->sprite = 594; + entity->sprite = my->sprite == 445 ? 594 : 1832; if ( my->monsterSpecialState == INCUBUS_STEAL ) { entity->yaw -= MONSTER_WEAPONYAW; @@ -1099,7 +1061,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1174,7 +1136,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1218,7 +1180,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1276,7 +1238,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1329,17 +1291,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1404,7 +1356,7 @@ void incubusMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(INCUBUS, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(INCUBUS, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); @@ -1513,7 +1465,7 @@ void Entity::incubusChooseWeapon(const Entity* target, double dist) bool tryCharm = true; bool trySteal = true; - if ( targetStats->EFFECTS[EFF_PACIFY] ) + if ( targetStats->getEffectActive(EFF_PACIFY) ) { tryCharm = false; } @@ -1533,9 +1485,9 @@ void Entity::incubusChooseWeapon(const Entity* target, double dist) bonusFromHP += 1; // +extra 2.5% chance if on lower health } - int requiredRoll = (1 + bonusFromHP + (targetStats->EFFECTS[EFF_CONFUSED] ? 4 : 0) - + (targetStats->EFFECTS[EFF_DRUNK] ? 2 : 0) - + (targetStats->EFFECTS[EFF_PACIFY] ? 2 : 0)); // +2.5% base, + extra if target is inebriated + int requiredRoll = (1 + bonusFromHP + (targetStats->getEffectActive(EFF_CONFUSED) ? 4 : 0) + + (targetStats->getEffectActive(EFF_DRUNK) ? 2 : 0) + + (targetStats->getEffectActive(EFF_PACIFY) ? 2 : 0)); // +2.5% base, + extra if target is inebriated if ( trySteal && tryCharm ) { @@ -1599,7 +1551,14 @@ void Entity::incubusChooseWeapon(const Entity* target, double dist) if ( specialRoll < (2 + bonusFromHP) ) // +5% base { node_t* node = nullptr; - node = itemNodeInInventory(myStats, -1, POTION); + if ( myStats->getAttribute("special_npc") == "johann" ) + { + node = itemNodeInInventory(myStats, -1, POTION, true); + } + else + { + node = itemNodeInInventory(myStats, -1, POTION); + } if ( node != nullptr ) { swapMonsterWeaponWithInventoryItem(this, myStats, node, true, true); diff --git a/src/monster_insectoid.cpp b/src/monster_insectoid.cpp index 409b946b4..7a3f6d29e 100644 --- a/src/monster_insectoid.cpp +++ b/src/monster_insectoid.cpp @@ -84,9 +84,9 @@ void initInsectoid(Entity* my, Stat* myStats) // boss variants // random effects - if ( rng.rand() % 8 == 0 ) + if ( rng.rand() % 8 == 0 && myStats->getAttribute("spawn_no_sleep") == "" ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 1800; } @@ -744,7 +744,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -797,7 +797,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 2.5; my->pitch = PI / 4; @@ -810,6 +810,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) my->pitch = 0; } } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; @@ -964,7 +965,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) playSoundEntityLocal(my, MONSTER_SPOTSND + 1, 128); if ( multiplayer != CLIENT ) { - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 60; } } @@ -1010,15 +1011,21 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) // torso case LIMB_HUMANOID_TORSO: torso = entity; + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[INSECTOID][1][0]; + entity->focaly = limbs[INSECTOID][1][1]; + entity->focalz = limbs[INSECTOID][1][2]; if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) { entity->sprite = my->sprite == 1057 ? 1060 : 458; } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); entity->scalex = 0.9; // shrink the width of the breastplate entity->scaley = 0.9; @@ -1095,6 +1102,9 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) } } } + entity->focalx = limbs[INSECTOID][2][0]; + entity->focaly = limbs[INSECTOID][2][1]; + entity->focalz = limbs[INSECTOID][2][2]; my->setHumanoidLimbOffset(entity, INSECTOID, LIMB_HUMANOID_RIGHTLEG); break; // left leg @@ -1131,6 +1141,9 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) } } } + entity->focalx = limbs[INSECTOID][3][0]; + entity->focaly = limbs[INSECTOID][3][1]; + entity->focalz = limbs[INSECTOID][3][2]; my->setHumanoidLimbOffset(entity, INSECTOID, LIMB_HUMANOID_LEFTLEG); break; // right arm @@ -1200,7 +1213,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1273,7 +1286,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1317,7 +1330,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1375,7 +1388,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1428,17 +1441,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1503,7 +1506,7 @@ void insectoidMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(INSECTOID, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(INSECTOID, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); diff --git a/src/monster_kobold.cpp b/src/monster_kobold.cpp index b247e1bc5..8490462b4 100644 --- a/src/monster_kobold.cpp +++ b/src/monster_kobold.cpp @@ -58,9 +58,9 @@ void initKobold(Entity* my, Stat* myStats) // boss variants // random effects - if ( rng.rand() % 8 == 0 ) + if ( rng.rand() % 8 == 0 && myStats->getAttribute("spawn_no_sleep") == "" ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 1800; } @@ -520,7 +520,7 @@ void koboldMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -573,7 +573,7 @@ void koboldMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 4; my->pitch = PI / 4; @@ -583,6 +583,7 @@ void koboldMoveBodyparts(Entity* my, Stat* myStats, double dist) my->z = 2.25; my->pitch = 0; } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; @@ -811,7 +812,7 @@ void koboldMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -884,7 +885,7 @@ void koboldMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -995,7 +996,7 @@ void koboldMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1052,7 +1053,7 @@ void koboldMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == NULL || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == NULL || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } diff --git a/src/monster_lich.cpp b/src/monster_lich.cpp index db81dc857..2523f568b 100644 --- a/src/monster_lich.cpp +++ b/src/monster_lich.cpp @@ -75,7 +75,7 @@ void initLich(Entity* my, Stat* myStats) // boss variants // random effects - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; // generates equipment and weapons if available from editor @@ -294,7 +294,7 @@ void lichAnimate(Entity* my, double dist) if ( multiplayer != CLIENT ) { Stat* myStats = my->getStats(); - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; diff --git a/src/monster_lichfire.cpp b/src/monster_lichfire.cpp index b2bb183d1..3e10de208 100644 --- a/src/monster_lichfire.cpp +++ b/src/monster_lichfire.cpp @@ -76,7 +76,7 @@ void initLichFire(Entity* my, Stat* myStats) // boss variants // random effects - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; // generates equipment and weapons if available from editor @@ -315,7 +315,7 @@ void lichFireAnimate(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -709,12 +709,12 @@ void lichFireAnimate(Entity* my, Stat* myStats, double dist) { my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; // lich can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 50; } else { - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 25; } } @@ -796,7 +796,7 @@ void lichFireAnimate(Entity* my, Stat* myStats, double dist) else if ( my->monsterAttack == MONSTER_POSE_MELEE_WINDUP3 ) { int windupDuration = (my->monsterState == MONSTER_STATE_LICH_CASTSPELLS) ? 20 : 40; - if ( multiplayer != CLIENT && myStats->EFFECTS[EFF_VAMPIRICAURA] ) + if ( multiplayer != CLIENT && myStats->getEffectActive(EFF_VAMPIRICAURA) ) { windupDuration = 20; } @@ -811,7 +811,7 @@ void lichFireAnimate(Entity* my, Stat* myStats, double dist) { my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; // // lich can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = windupDuration; } } @@ -876,7 +876,7 @@ void lichFireAnimate(Entity* my, Stat* myStats, double dist) playSoundEntityLocal(my, 170, 32); if ( multiplayer != CLIENT ) { - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 20; } } @@ -951,12 +951,12 @@ void lichFireAnimate(Entity* my, Stat* myStats, double dist) if ( local_rng.rand() % 2 ) { if ( my->monsterLichAllyStatus == LICH_ALLY_DEAD - && !myStats->EFFECTS[EFF_VAMPIRICAURA] + && !myStats->getEffectActive(EFF_VAMPIRICAURA) && my->monsterState != MONSTER_STATE_LICH_CASTSPELLS ) { createParticleDropRising(my, 600, 0.7); serverSpawnMiscParticles(my, PARTICLE_EFFECT_VAMPIRIC_AURA, 600); - myStats->EFFECTS[EFF_VAMPIRICAURA] = true; + myStats->setEffectActive(EFF_VAMPIRICAURA, 1); myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] = 600; } else @@ -1033,7 +1033,7 @@ void lichFireAnimate(Entity* my, Stat* myStats, double dist) // set sprites, invisibility check etc. if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } diff --git a/src/monster_lichice.cpp b/src/monster_lichice.cpp index 959df3a5a..38158f201 100644 --- a/src/monster_lichice.cpp +++ b/src/monster_lichice.cpp @@ -76,7 +76,7 @@ void initLichIce(Entity* my, Stat* myStats) // boss variants // random effects - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; // generates equipment and weapons if available from editor @@ -315,7 +315,7 @@ void lichIceAnimate(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -719,7 +719,7 @@ void lichIceAnimate(Entity* my, Stat* myStats, double dist) { my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; // lich can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 50; } } @@ -758,7 +758,7 @@ void lichIceAnimate(Entity* my, Stat* myStats, double dist) { my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; // // lich can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = windupDuration; } } @@ -816,12 +816,12 @@ void lichIceAnimate(Entity* my, Stat* myStats, double dist) { my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; // lich can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 50; } else { - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 25; } } @@ -894,7 +894,7 @@ void lichIceAnimate(Entity* my, Stat* myStats, double dist) { my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; // lich can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 50; } } @@ -953,7 +953,7 @@ void lichIceAnimate(Entity* my, Stat* myStats, double dist) { my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; // lich can't be paralyzed, use EFF_STUNNED instead. - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 80; } } @@ -1000,7 +1000,7 @@ void lichIceAnimate(Entity* my, Stat* myStats, double dist) playSoundEntityLocal(my, 170, 32); if ( multiplayer != CLIENT ) { - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = 20; } } @@ -1143,7 +1143,7 @@ void lichIceAnimate(Entity* my, Stat* myStats, double dist) // set sprites, invisibility check etc. if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } diff --git a/src/monster_m.cpp b/src/monster_m.cpp new file mode 100644 index 000000000..d3b82afc8 --- /dev/null +++ b/src/monster_m.cpp @@ -0,0 +1,2339 @@ +/*------------------------------------------------------------------------------- + + BARONY + File: monster_goatman.cpp + Desc: implements all of the goatman monster's code + + Copyright 2013-2016 (c) Turning Wheel LLC, all rights reserved. + See LICENSE for details. + +-------------------------------------------------------------------------------*/ + +#include "main.hpp" +#include "game.hpp" +#include "stat.hpp" +#include "entity.hpp" +#include "items.hpp" +#include "monster.hpp" +#include "engine/audio/sound.hpp" +#include "net.hpp" +#include "collision.hpp" +#include "player.hpp" +#include "prng.hpp" +#include "scores.hpp" +#include "mod_tools.hpp" + +real_t getNormalHeightMonsterM(Entity& my) +{ + if ( my.sprite == 1520 ) + { + return 1.5; + } + return -0.5; +} + +void initMonsterM(Entity* my, Stat* myStats) +{ + if ( !my ) + { + return; + } + node_t* node; + bool spawnedBoss = false; + + my->flags[BURNABLE] = true; + my->initMonster(1519); + my->z = getNormalHeightMonsterM(*my); + + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = 747; + MONSTER_SPOTVAR = 2; + MONSTER_IDLESND = 742; + MONSTER_IDLEVAR = 3; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( myStats->sex == FEMALE ) + { + my->sprite = 1520; + } + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + + // boss variants + //const bool boss = + // rng.rand() % 50 == 0 && + // !my->flags[USERFLAG2] && + // !myStats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS]; + //if ( (boss || (*cvar_summonBosses && conductGameChallenges[CONDUCT_CHEATS_ENABLED])) && myStats->leader_uid == 0 ) + //{ + // myStats->setAttribute("special_npc", "gharbad"); + // strcpy(myStats->name, MonsterMata_t::getSpecialNPCName(*myStats).c_str()); + // my->sprite = MonsterMata_t::getSpecialNPCBaseModel(*myStats); + // myStats->sex = MALE; + // myStats->STR += 10; + // myStats->DEX += 2; + // myStats->MAXHP += 75; + // myStats->HP = myStats->MAXHP; + // myStats->OLDHP = myStats->MAXHP; + // myStats->CHR = -1; + // spawnedBoss = true; + // //TODO: Boss stats + + // //Spawn in potions. + // int end = rng.rand()%NUM_GOATMAN_BOSS_GHARBAD_POTIONS + 5; + // for ( int i = 0; i < end; ++i ) + // { + // switch ( rng.rand()%10 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // case 8: + // newItem(POTION_BOOZE, EXCELLENT, 0, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + // break; + // case 9: + // newItem(POTION_HEALING, EXCELLENT, 0, 1, rng.rand(), false, &myStats->inventory); + // break; + // default: + // printlog("Tried to spawn goatman boss \"Gharbad\" invalid potion."); + // break; + // } + // } + + // newItem(CRYSTAL_SHURIKEN, EXCELLENT, 1 + rng.rand()%1, rng.rand()%NUM_GOATMAN_BOSS_GHARBAD_THROWN_WEAPONS + 2, rng.rand(), true, &myStats->inventory); + //} + + // random effects + /*if ( rng.rand() % 8 == 0 ) + { + my->setEffect(EFF_ASLEEP, true, 1800 + rng.rand() % 1800, false); + }*/ + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + if ( myStats->getAttribute("monster_m_type") == "" ) + { + if ( rng.rand() % 2 == 0 ) + { + myStats->setAttribute("monster_m_type", "duster"); + } + else + { + myStats->setAttribute("monster_m_type", "discanter"); + } + } + + if ( myStats->getAttribute("monster_m_type") == "duster" ) + { + newItem(DUST_BALL, SERVICABLE, 0, rng.rand() % 2 + 2, rng.rand(), false, &myStats->inventory); + } + + // give weapon + if ( myStats->getAttribute("monster_m_type") == "duster" ) + { + if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + { + switch ( rng.rand() % 10 ) + { + case 0: + case 1: + case 2: + case 3: + case 4: + myStats->weapon = newItem(IRON_AXE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + case 5: + case 6: + case 7: + case 8: + case 9: + myStats->weapon = newItem(IRON_MACE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + default: + break; + } + } + } + else if ( myStats->getAttribute("monster_m_type") == "discanter" ) + { + if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + { + switch ( rng.rand() % 10 ) + { + case 0: + case 1: + case 2: + case 3: + case 4: + myStats->weapon = newItem(QUARTERSTAFF, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + case 5: + case 6: + case 7: + case 8: + case 9: + myStats->weapon = newItem(QUARTERSTAFF, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + default: + break; + } + } + } + + //bool isShaman = false; + //if ( rng.rand() % 2 && !spawnedBoss && !minion ) + //{ + // isShaman = true; + // if ( myStats->leader_uid == 0 && !my->flags[USERFLAG2] && rng.rand() % 2 == 0 ) + // { + // Entity* entity = summonMonster(GOATMAN, my->x, my->y); + // if ( entity ) + // { + // entity->parent = my->getUID(); + // if ( Stat* followerStats = entity->getStats() ) + // { + // followerStats->leader_uid = entity->parent; + // } + // entity->seedEntityRNG(rng.getU32()); + // } + // if ( rng.rand() % 5 == 0 ) + // { + // // summon second ally randomly. + // entity = summonMonster(GOATMAN, my->x, my->y); + // if ( entity ) + // { + // entity->parent = my->getUID(); + // if ( Stat* followerStats = entity->getStats() ) + // { + // followerStats->leader_uid = entity->parent; + // } + // entity->seedEntityRNG(rng.getU32()); + // } + // } + // } + //} + //else + //{ + // myStats->DEX += 1; // more speed for brawlers. + //} + + // generate the default inventory items for the monster, provided the editor sprite allowed enough default slots + //switch ( defaultItems ) + //{ + // case 6: + // case 5: + // case 4: + // case 3: + // case 2: + // case 1: + // if ( isShaman && rng.rand() % 10 == 0 ) + // { + // switch ( rng.rand() % 4 ) + // { + // case 0: + // newItem(SPELLBOOK_SLOW, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 1: + // newItem(SPELLBOOK_FIREBALL, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 2: + // newItem(SPELLBOOK_COLD, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 3: + // newItem(SPELLBOOK_FORCEBOLT, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // } + // } + // break; + // default: + // break; + //} + + + ////Give weapons. + //if ( !spawnedBoss ) + //{ + // if ( !isShaman && rng.rand() % 3 > 0 ) + // { + // newItem(STEEL_CHAKRAM, SERVICABLE, 0, rng.rand()%NUM_GOATMAN_THROWN_WEAPONS + 1, rng.rand(), false, &myStats->inventory); + // } + // int numpotions = rng.rand() % NUM_GOATMAN_POTIONS + 2; + // if ( rng.rand() % 3 == 0 ) + // { + // int numhealpotion = rng.rand() % 2 + 1; + // newItem(POTION_HEALING, static_cast(rng.rand() % 3 + DECREPIT), 0, numhealpotion, rng.rand(), false, &myStats->inventory); + // numpotions -= numhealpotion; + // } + // if ( rng.rand() % 4 > 0 ) + // { + // // undroppable + // newItem(POTION_BOOZE, static_cast(rng.rand() % 3 + DECREPIT), 0, numpotions, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + // } + //} + + //if ( isShaman ) + //{ + // //give shield + // if ( myStats->shield == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] == 1 ) + // { + // // give shield + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // myStats->shield = newItem(TOOL_CRYSTALSHARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 8: + // myStats->shield = newItem(MIRROR_SHIELD, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shield = newItem(TOOL_LANTERN, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give cloak + // if ( myStats->cloak == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_CLOAK] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // break; + // default: + // myStats->cloak = newItem(CLOAK, WORN, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give helmet + // if ( myStats->helmet == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_HELM] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // myStats->helmet = newItem(HAT_HOOD, WORN, -1 + rng.rand() % 3, 1, 0, false, nullptr); + // break; + // default: + // myStats->helmet = newItem(HAT_WIZARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give armor + // if ( myStats->breastplate == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // myStats->breastplate = newItem(WIZARD_DOUBLET, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 2: + // myStats->breastplate = newItem(LEATHER_BREASTPIECE, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // break; + // } + // } + // // give booties + // if ( myStats->shoes == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_BOOTS] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // myStats->shoes = newItem(IRON_BOOTS, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->shoes = newItem(CRYSTAL_BOOTS, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shoes = newItem(STEEL_BOOTS, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give weapon + // if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + // { + // switch ( rng.rand() % 12 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // myStats->weapon = newItem(MAGICSTAFF_COLD, static_cast(rng.rand() % 2 + SERVICABLE), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 5: + // case 6: + // case 7: + // case 8: + // myStats->weapon = newItem(MAGICSTAFF_FIRE, static_cast(rng.rand() % 2 + SERVICABLE), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 9: + // switch ( rng.rand() % 4 ) + // { + // case 0: + // myStats->weapon = newItem(SPELLBOOK_SLOW, static_cast(rng.rand() % 2 + DECREPIT), -1 + rng.rand() % 3, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, nullptr); + // break; + // case 1: + // myStats->weapon = newItem(SPELLBOOK_FIREBALL, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, nullptr); + // break; + // case 2: + // myStats->weapon = newItem(SPELLBOOK_COLD, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, nullptr); + // break; + // case 3: + // myStats->weapon = newItem(SPELLBOOK_FORCEBOLT, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, nullptr); + // break; + // } + // break; + // default: + // myStats->weapon = newItem(MAGICSTAFF_SLOW, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + //} + //else + //{ + // ////give shield + // //if ( myStats->shield == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] == 1 ) + // //{ + // // switch ( rng.rand() % 20 ) + // // { + // // case 0: + // // case 1: + // // case 2: + // // case 3: + // // case 4: + // // case 5: + // // case 6: + // // case 7: + // // myStats->shield = newItem(TOOL_CRYSTALSHARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // case 8: + // // myStats->shield = newItem(MIRROR_SHIELD, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // default: + // // myStats->shield = newItem(TOOL_LANTERN, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // } + // //} + // // give cloak + // /*if ( myStats->cloak == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_CLOAK] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // break; + // default: + // myStats->cloak = newItem(CLOAK, WORN, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // }*/ + // //// give helmet + // //if ( myStats->helmet == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_HELM] == 1 ) + // //{ + // // switch ( rng.rand() % 10 ) + // // { + // // case 0: + // // case 1: + // // myStats->helmet = newItem(HAT_HOOD, WORN, -1 + rng.rand() % 3, 1, 0, false, nullptr); + // // break; + // // default: + // // myStats->helmet = newItem(HAT_WIZARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, 0, false, nullptr); + // // break; + // // } + // //} + // // give armor + // if ( myStats->breastplate == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // myStats->breastplate = newItem(STEEL_BREASTPIECE, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 3: + // case 4: + // myStats->breastplate = newItem(LEATHER_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 5: + // case 6: + // myStats->breastplate = newItem(IRON_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->breastplate = newItem(CRYSTAL_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // break; + // } + // } + // // give booties + // if ( myStats->shoes == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_BOOTS] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // myStats->shoes = newItem(IRON_BOOTS, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->shoes = newItem(CRYSTAL_BOOTS, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shoes = newItem(STEEL_BOOTS, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give weapon + // if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // myStats->weapon = newItem(STEEL_AXE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 18: + // myStats->weapon = newItem(CRYSTAL_BATTLEAXE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->weapon = newItem(CRYSTAL_MACE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->weapon = newItem(STEEL_MACE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + //} + } + } + + // torso + const int torso_sprite = my->sprite == 1519 ? 1534 : 1535; + Entity* entity = newEntity(torso_sprite, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->scalex = 1.01; + entity->scaley = 1.01; + entity->scalez = 1.01; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MYCONID][1][0]; // 0 + entity->focaly = limbs[MYCONID][1][1]; // 0 + entity->focalz = limbs[MYCONID][1][2]; // 0 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // right leg + entity = newEntity(my->sprite == 1519 ? 1532 : 1533, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MYCONID][2][0]; // 0 + entity->focaly = limbs[MYCONID][2][1]; // 0 + entity->focalz = limbs[MYCONID][2][2]; // 2 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // left leg + entity = newEntity(my->sprite == 1519 ? 1530 : 1531, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MYCONID][3][0]; // 0 + entity->focaly = limbs[MYCONID][3][1]; // 0 + entity->focalz = limbs[MYCONID][3][2]; // 2 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // right arm + entity = newEntity(1523, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MYCONID][4][0]; // 0 + entity->focaly = limbs[MYCONID][4][1]; // 0 + entity->focalz = limbs[MYCONID][4][2]; // 1.5 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // left arm + entity = newEntity(1521, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MYCONID][5][0]; // 0 + entity->focaly = limbs[MYCONID][5][1]; // 0 + entity->focalz = limbs[MYCONID][5][2]; // 1.5 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // world weapon + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[MYCONID][6][0]; // 1.5 + entity->focaly = limbs[MYCONID][6][1]; // 0 + entity->focalz = limbs[MYCONID][6][2]; // -.5 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + entity->pitch = .25; + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // shield + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[MYCONID][7][0]; // 2 + entity->focaly = limbs[MYCONID][7][1]; // 0 + entity->focalz = limbs[MYCONID][7][2]; // 0 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // cloak + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[MYCONID][8][0]; // 0 + entity->focaly = limbs[MYCONID][8][1]; // 0 + entity->focalz = limbs[MYCONID][8][2]; // 4 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // helmet + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->scalex = 1.01; + entity->scaley = 1.01; + entity->scalez = 1.01; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[MYCONID][9][0]; // 0 + entity->focaly = limbs[MYCONID][9][1]; // 0 + entity->focalz = limbs[MYCONID][9][2]; // -2 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // mask + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[MYCONID][10][0]; // 0 + entity->focaly = limbs[MYCONID][10][1]; // 0 + entity->focalz = limbs[MYCONID][10][2]; // .25 + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // head left + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MYCONID][11][0]; + entity->focaly = limbs[MYCONID][11][1]; + entity->focalz = limbs[MYCONID][11][2]; + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // head right + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MYCONID][13][0]; + entity->focaly = limbs[MYCONID][13][1]; + entity->focalz = limbs[MYCONID][13][2]; + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // head center + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MYCONID][15][0]; + entity->focaly = limbs[MYCONID][15][1]; + entity->focalz = limbs[MYCONID][15][2]; + entity->scalex = 1.008; + entity->scaley = 1.008; + entity->scalez = 1.008; + entity->behavior = &actMonsterMLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + if ( multiplayer == CLIENT || MONSTER_INIT ) + { + return; + } +} + +void actMonsterMLimb(Entity* my) +{ + my->actMonsterLimb(true); +} + +void monsterMDie(Entity* my) +{ + Entity* gib = spawnGib(my); + gib->skill[5] = 1; // poof + gib->sprite = my->sprite; + serverSpawnGibForClient(gib); + for ( int c = 0; c < 8; c++ ) + { + Entity* gib = spawnGib(my); + serverSpawnGibForClient(gib); + } + + my->spawnBlood(); + + playSoundEntity(my, 745 + local_rng.rand() % 2, 128); + + my->removeMonsterDeathNodes(); + + list_RemoveNode(my->mynode); + return; +} + +#define MONSTER_MWALKSPEED .13 + +void monsterMMoveBodyparts(Entity* my, Stat* myStats, double dist) +{ + node_t* node; + Entity* entity = nullptr, * entity2 = nullptr; + Entity* additionalLimb = nullptr; + Entity* rightbody = nullptr; + Entity* weaponarm = nullptr; + int bodypart; + bool wearingring = false; + + my->focalx = limbs[MYCONID][0][0]; + my->focaly = limbs[MYCONID][0][1]; + my->focalz = limbs[MYCONID][0][2]; + + /*if ( keystatus[SDLK_j] ) + { + keystatus[SDLK_j] = 0; + my->fskill[1] = 1.0; + + my->monsterAttack = MONSTER_POSE_MAGIC_WINDUP3; + }*/ + + if ( my->sprite == 1520 ) + { + my->focalz += 0.5; + my->monsterSpellAnimation = MONSTER_SPELLCAST_SMALL_HUMANOID; + } + + bool debugModel = monsterDebugModels(my, &dist); + + // set invisibility //TODO: isInvisible()? + if ( multiplayer != CLIENT ) + { + if ( myStats->ring != nullptr ) + if ( myStats->ring->type == RING_INVISIBILITY ) + { + wearingring = true; + } + if ( myStats->cloak != nullptr ) + if ( myStats->cloak->type == CLOAK_INVISIBILITY ) + { + wearingring = true; + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) + { + my->flags[INVISIBLE] = true; + my->flags[BLOCKSIGHT] = false; + bodypart = 0; + for ( node = my->children.first; node != nullptr; node = node->next ) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + bodypart++; + continue; + } + if ( bodypart >= 7 ) + { + break; + } + entity = (Entity*)node->element; + if ( !entity->flags[INVISIBLE] ) + { + entity->flags[INVISIBLE] = true; + serverUpdateEntityBodypart(my, bodypart); + } + bodypart++; + } + } + else + { + my->flags[INVISIBLE] = false; + my->flags[BLOCKSIGHT] = true; + bodypart = 0; + for ( node = my->children.first; node != nullptr; node = node->next ) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + bodypart++; + continue; + } + if ( bodypart >= 7 ) + { + break; + } + entity = (Entity*)node->element; + if ( entity->flags[INVISIBLE] ) + { + entity->flags[INVISIBLE] = false; + serverUpdateEntityBodypart(my, bodypart); + serverUpdateEntityFlag(my, INVISIBLE); + } + bodypart++; + } + } + + // sleeping + if ( myStats->getEffectActive(EFF_ASLEEP) ) + { + if ( my->sprite == 1520 ) + { + my->z = 3.75; + } + else + { + my->z = 3.0; + } + my->pitch = PI / 4; + } + else + { + my->z = getNormalHeightMonsterM(*my); + + if ( my->monsterAttack == 0 ) + { + if ( debugModel ) + { + my->pitch = my->fskill[0]; + if ( my->fskill[1] > 0.0 ) // jumpy + { + my->fskill[1] = std::max(0.0, my->fskill[1] - 0.05); + my->z += -3.0 * sqrt(sin(PI * my->fskill[1])); + } + } + else + { + my->pitch = 0; + } + } + } + my->creatureHandleLiftZ(); + } + + Entity* shieldarm = nullptr; + Entity* helmet = nullptr; + + //Move bodyparts + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, bodypart++ ) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + // post-swing head animation. client doesn't need to adjust the entity pitch, server will handle. + if ( my->monsterAttack != MONSTER_POSE_RANGED_WINDUP3 && my->monsterAttack != MONSTER_POSE_SPECIAL_WINDUP1 + && bodypart == 1 && multiplayer != CLIENT ) + { + limbAnimateToLimit(my, ANIMATE_PITCH, 0.1, 0, false, 0.0); + } + continue; + } + entity = (Entity*)node->element; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + if ( MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 && bodypart == LIMB_HUMANOID_RIGHTARM ) + { + // don't let the creatures's yaw move the casting arm + } + else + { + entity->yaw = my->yaw; + } + + if ( bodypart == LIMB_HUMANOID_RIGHTLEG || bodypart == LIMB_HUMANOID_LEFTARM ) + { + my->humanoidAnimateWalk(entity, node, bodypart, MONSTER_MWALKSPEED, dist, 0.4); + } + else if ( bodypart == LIMB_HUMANOID_LEFTLEG || bodypart == LIMB_HUMANOID_RIGHTARM || bodypart == LIMB_HUMANOID_CLOAK ) + { + // left leg, right arm, cloak. + if ( bodypart == LIMB_HUMANOID_RIGHTARM ) + { + weaponarm = entity; + if ( MONSTER_ATTACK > 0 ) + { + if ( my->monsterAttack == MONSTER_POSE_RANGED_WINDUP3 + || my->monsterAttack == MONSTER_POSE_SPECIAL_WINDUP1 ) + { + Entity* rightbody = nullptr; + // set rightbody to left leg. + node_t* rightbodyNode = list_Node(&my->children, LIMB_HUMANOID_LEFTLEG); + if ( rightbodyNode ) + { + rightbody = (Entity*)rightbodyNode->element; + } + else + { + return; + } + + if ( my->monsterAttackTime == 0 ) + { + // init rotations + weaponarm->pitch = 0; + my->monsterArmbended = 0; + my->monsterWeaponYaw = 0; + weaponarm->roll = 0; + weaponarm->skill[1] = 0; + playSoundEntityLocal(my, 747, 128); + createParticleDot(my); + if ( multiplayer != CLIENT ) + { + myStats->setEffectActive(EFF_PARALYZED, 1); + myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 40; + } + } + if ( multiplayer != CLIENT ) + { + // move the head and weapon yaw + limbAnimateToLimit(my, ANIMATE_PITCH, -0.1, 11 * PI / 6, false, 0.0); + limbAnimateToLimit(my, ANIMATE_WEAPON_YAW, 0.05, 2 * PI / 8, false, 0.0); + } + limbAnimateToLimit(weaponarm, ANIMATE_PITCH, -0.25, 7 * PI / 4, true, 0.0); + //limbAnimateToLimit(weaponarm, ANIMATE_ROLL, -0.25, 7 * PI / 4, false, 0.0); + + if ( my->monsterAttackTime >= 4 * ANIMATE_DURATION_WINDUP / (monsterGlobalAnimationMultiplier / 10.0) ) + { + if ( multiplayer != CLIENT ) + { + if ( my->monsterAttack == MONSTER_POSE_SPECIAL_WINDUP1 ) + { + my->attack(MONSTER_POSE_MAGIC_WINDUP3, 0, nullptr); + } + else + { + my->attack(MONSTER_POSE_MELEE_WINDUP1, 0, nullptr); + } + } + } + } + else if ( my->monsterAttack == MONSTER_POSE_MAGIC_WINDUP3 ) + { + if ( multiplayer != CLIENT ) + { + if ( my->monsterAttackTime == 1 ) + { + int spellID = SPELL_SPORE_BOMB; + if ( my->monsterSpecialState == MONSTER_M_SPECIAL_CAST3 ) + { + real_t oldYaw = my->yaw; + real_t dist = 100.0; + if ( Entity* target = uidToEntity(my->monsterTarget) ) + { + real_t tangent = atan2(target->y - my->y, + target->x - my->x); + my->yaw = tangent; + dist = entityDist(my, target); + } + + castSpell(my->getUID(), getSpellFromID(spellID), true, false); + + playSoundEntity(my, 171, 128); + my->yaw = oldYaw; + } + else + { + real_t dist = 100.0; + real_t yawDiff = 0.0; + if ( Entity* target = uidToEntity(my->monsterTarget) ) + { + yawDiff = my->yawDifferenceFromEntity(target); + dist = entityDist(my, target); + } + bool setProps = false; + CastSpellProps_t props; + if ( my->monsterSpecialState == MONSTER_M_SPECIAL_CAST1 + && (dist > TOUCHRANGE || (yawDiff >= 0 && yawDiff < PI)) ) + { + spellID = SPELL_TELEKINESIS; + setProps = props.setToMonsterCast(my, spellID); + } + if ( !setProps ) + { + spellID = SPELL_SPORES; + } + + if ( setProps ) + { + castSpell(my->getUID(), getSpellFromID(spellID), true, false, false, &props); + } + else + { + castSpell(my->getUID(), getSpellFromID(spellID), true, false); + + if ( spellID == SPELL_SPORES ) + { + for ( int x = -1; x <= 1; ++x ) + { + for ( int y = -1; y <= 1; ++y ) + { + floorMagicCreateSpores(my, my->x + x * 16.0, my->y + y * 16.0, my, 0, SPELL_SPORES); + } + } + } + } + } + } + } + if ( weaponarm->pitch >= 3 * PI / 2 ) + { + my->monsterArmbended = 1; + } + + if ( weaponarm->skill[1] == 0 ) + { + // chop forwards + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, 0.4, PI / 3, false, 0.0) ) + { + weaponarm->skill[1] = 1; + } + } + else if ( weaponarm->skill[1] >= 1 ) + { + if ( limbAnimateToLimit(weaponarm, ANIMATE_PITCH, -0.25, 7 * PI / 4, false, 0.0) ) + { + Entity* rightbody = nullptr; + // set rightbody to left leg. + node_t* rightbodyNode = list_Node(&my->children, LIMB_HUMANOID_LEFTLEG); + if ( rightbodyNode ) + { + rightbody = (Entity*)rightbodyNode->element; + } + if ( rightbody ) + { + weaponarm->skill[0] = rightbody->skill[0]; + weaponarm->pitch = rightbody->pitch; + } + my->monsterWeaponYaw = 0; + weaponarm->roll = 0; + my->monsterArmbended = 0; + my->monsterAttack = 0; + } + } + } + else + { + if ( multiplayer != CLIENT ) + { + if ( my->monsterAttack == MONSTER_POSE_MAGIC_WINDUP1 && + my->monsterSpecialState == MONSTER_M_SPECIAL_CAST1 + && my->monsterAttackTime == 0 ) + { + my->setEffect(EFF_ROOTED, true, 50, false, true, false, false); + } + } + my->handleWeaponArmAttack(entity); + } + } + } + else if ( bodypart == LIMB_HUMANOID_CLOAK ) + { + entity->pitch = entity->fskill[0]; + } + + my->humanoidAnimateWalk(entity, node, bodypart, MONSTER_MWALKSPEED, dist, 0.4); + + if ( bodypart == LIMB_HUMANOID_CLOAK ) + { + entity->fskill[0] = entity->pitch; + entity->roll = my->roll - fabs(entity->pitch) / 2; + entity->pitch = 0; + } + } + switch ( bodypart ) + { + // torso + case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[MYCONID][1][0]; + entity->focaly = limbs[MYCONID][1][1]; + entity->focalz = limbs[MYCONID][1][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, my->sprite == 1520, my) ) + { + entity->sprite = + my->sprite == 1519 ? 1534 : 1535; + } + else + { + entity->sprite = itemModel(myStats->breastplate, my->sprite == 1520, my); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, MYCONID, LIMB_HUMANOID_TORSO); + break; + // right leg + case LIMB_HUMANOID_RIGHTLEG: + entity->focalx = limbs[MYCONID][2][0]; + entity->focaly = limbs[MYCONID][2][1]; + entity->focalz = limbs[MYCONID][2][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->shoes == nullptr ) + { + entity->sprite = + my->sprite == 1519 ? 1532 : 1533; + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_RIGHT_OFFSET); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, MYCONID, LIMB_HUMANOID_RIGHTLEG); + break; + // left leg + case LIMB_HUMANOID_LEFTLEG: + entity->focalx = limbs[MYCONID][3][0]; + entity->focaly = limbs[MYCONID][3][1]; + entity->focalz = limbs[MYCONID][3][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->shoes == nullptr ) + { + entity->sprite = + my->sprite == 1519 ? 1530 : 1531; + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_LEFT_OFFSET); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, MYCONID, LIMB_HUMANOID_LEFTLEG); + break; + // right arm + case LIMB_HUMANOID_RIGHTARM: + { + if ( multiplayer != CLIENT ) + { + if ( myStats->gloves == nullptr ) + { + entity->sprite = 1523; + } + else + { + if ( setGloveSprite(myStats, entity, SPRITE_GLOVE_RIGHT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( entity->skill[7] == 0 ) + { + if ( entity->sprite == 1523 ) + { + // these are the default arms. + // chances are they may be wrong if sent by the server, + } + else + { + // otherwise we're being sent gloves armor etc so it's probably right. + entity->skill[7] = entity->sprite; + } + } + if ( entity->skill[7] == 0 ) + { + // we set this ourselves until proper initialisation. + entity->sprite = 1523; + } + else + { + entity->sprite = entity->skill[7]; + } + } + + node_t* weaponNode = list_Node(&my->children, 7); + if ( weaponNode ) + { + Entity* weapon = (Entity*)weaponNode->element; + if ( MONSTER_ARMBENDED || (weapon->flags[INVISIBLE] && my->monsterState == MONSTER_STATE_WAIT) ) + { + // if weapon invisible and I'm not attacking, relax arm. + entity->focalx = limbs[MYCONID][4][0]; // 0 + entity->focaly = limbs[MYCONID][4][1]; + entity->focalz = limbs[MYCONID][4][2]; // 2 + + if ( entity->sprite == 1523 ) + { + entity->focaly += 0.25; + } + } + else + { + // else flex arm. + entity->focalx = limbs[MYCONID][4][0] + 0.75; + entity->focaly = limbs[MYCONID][4][1] + 0.25; + entity->focalz = limbs[MYCONID][4][2] - 0.75; + if ( entity->sprite == 1523 ) + { + entity->sprite += 1; + } + else + { + entity->sprite += 2; + } + } + } + my->setHumanoidLimbOffset(entity, MYCONID, LIMB_HUMANOID_RIGHTARM); + entity->yaw += MONSTER_WEAPONYAW; + break; + // left arm + } + case LIMB_HUMANOID_LEFTARM: + { + if ( multiplayer != CLIENT ) + { + if ( myStats->gloves == nullptr ) + { + entity->sprite = 1521; + } + else + { + if ( setGloveSprite(myStats, entity, SPRITE_GLOVE_LEFT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( entity->skill[7] == 0 ) + { + if ( entity->sprite == 1521 ) + { + // these are the default arms. + // chances are they may be wrong if sent by the server, + } + else + { + // otherwise we're being sent gloves armor etc so it's probably right. + entity->skill[7] = entity->sprite; + } + } + if ( entity->skill[7] == 0 ) + { + // we set this ourselves until proper initialisation. + entity->sprite = 1521; + } + else + { + entity->sprite = entity->skill[7]; + } + } + + shieldarm = entity; + node_t* shieldNode = list_Node(&my->children, 8); + if ( shieldNode ) + { + Entity* shield = (Entity*)shieldNode->element; + if ( shield->flags[INVISIBLE] && my->monsterState == MONSTER_STATE_WAIT ) + { + entity->focalx = limbs[MYCONID][5][0]; // 0 + entity->focaly = limbs[MYCONID][5][1]; + entity->focalz = limbs[MYCONID][5][2]; // 2 + + if ( entity->sprite == 1521 ) + { + entity->focaly -= 0.25; + } + } + else + { + entity->focalx = limbs[MYCONID][5][0] + 0.75; + entity->focaly = limbs[MYCONID][5][1] - 0.25; + entity->focalz = limbs[MYCONID][5][2] - 0.75; + if ( entity->sprite == 1521 ) + { + entity->sprite += 1; + } + else + { + entity->sprite += 2; + } + } + } + my->setHumanoidLimbOffset(entity, MYCONID, LIMB_HUMANOID_LEFTARM); + if ( my->monsterDefend && my->monsterAttack == 0 ) + { + MONSTER_SHIELDYAW = PI / 5; + } + else + { + MONSTER_SHIELDYAW = 0; + } + entity->yaw += MONSTER_SHIELDYAW; + break; + } + // weapon + case LIMB_HUMANOID_WEAPON: + if ( multiplayer != CLIENT ) + { + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->sprite = itemModel(myStats->weapon); + if ( itemCategory(myStats->weapon) == SPELLBOOK ) + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + if ( weaponarm != nullptr ) + { + my->handleHumanoidWeaponLimb(entity, weaponarm); + } + break; + // shield + case LIMB_HUMANOID_SHIELD: + if ( multiplayer != CLIENT ) + { + if ( myStats->shield == nullptr ) + { + entity->flags[INVISIBLE] = true; + entity->sprite = 0; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(myStats->shield); + if ( itemTypeIsQuiver(myStats->shield->type) ) + { + entity->handleQuiverThirdPersonModel(*myStats, my->sprite); + } + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + my->handleHumanoidShieldLimb(entity, shieldarm); + break; + // cloak + case LIMB_HUMANOID_CLOAK: + entity->focalx = limbs[MYCONID][8][0]; + entity->focaly = limbs[MYCONID][8][1]; + entity->focalz = limbs[MYCONID][8][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(myStats->cloak); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + entity->x -= cos(my->yaw); + entity->y -= sin(my->yaw); + entity->yaw += PI / 2; + break; + // helm + case LIMB_HUMANOID_HELMET: + helmet = entity; + entity->focalx = limbs[MYCONID][9][0]; // 0 + entity->focaly = limbs[MYCONID][9][1]; // 0 + entity->focalz = limbs[MYCONID][9][2]; // -2 + entity->pitch = my->pitch; + entity->roll = 0; + if ( multiplayer != CLIENT ) + { + entity->sprite = itemModel(myStats->helmet); + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + my->setHelmetLimbOffset(entity); + break; + // mask + case LIMB_HUMANOID_MASK: + entity->focalx = limbs[MYCONID][10][0]; // 0 + entity->focaly = limbs[MYCONID][10][1]; // 0 + entity->focalz = limbs[MYCONID][10][2]; // .25 + entity->pitch = my->pitch; + entity->roll = PI / 2; + if ( multiplayer != CLIENT ) + { + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( myStats->mask != nullptr ) + { + if ( myStats->mask->type == TOOL_GLASSES ) + { + entity->sprite = 165; // GlassesWorn.vox + } + else if ( myStats->mask->type == MONOCLE ) + { + entity->sprite = 1196; // monocleWorn.vox + } + else + { + entity->sprite = itemModel(myStats->mask); + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + if ( EquipmentModelOffsets.modelOffsetExists(MYCONID, entity->sprite, my->sprite) ) + { + my->setHelmetLimbOffset(entity); + my->setHelmetLimbOffsetWithMask(helmet, entity); + } + else + { + entity->focalx = limbs[MYCONID][10][0] + .35; // .35 + entity->focaly = limbs[MYCONID][10][1] - 2; // -2 + entity->focalz = limbs[MYCONID][10][2]; // .25 + } + break; + // head left + case 12: + { + entity->focalx = limbs[MYCONID][11][0]; + entity->focaly = limbs[MYCONID][11][1]; + entity->focalz = limbs[MYCONID][11][2]; + entity->x += limbs[MYCONID][12][0] * cos(my->yaw + PI / 2) + limbs[MYCONID][12][1] * cos(my->yaw); + entity->y += limbs[MYCONID][12][0] * sin(my->yaw + PI / 2) + limbs[MYCONID][12][1] * sin(my->yaw); + entity->z += limbs[MYCONID][12][2]; + entity->pitch = my->pitch; + if ( multiplayer != CLIENT ) + { + entity->sprite = 1528; + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + + additionalLimb = entity; + bool moving = false; + if ( dist > 0.1 ) + { + moving = true; + } + if ( entity->skill[0] == 0 ) + { + if ( moving ) + { + entity->fskill[0] += std::min(dist * MONSTER_MWALKSPEED, 2.f * MONSTER_MWALKSPEED); // move proportional to move speed + } + else if ( my->monsterAttack != 0 ) + { + entity->fskill[0] += MONSTER_MWALKSPEED; // move fixed speed when attacking if stationary + } + else + { + entity->fskill[0] += 0.01; // otherwise move slow idle + } + + if ( entity->fskill[0] > PI / 3 || ((!moving || my->monsterAttack != 0) && entity->fskill[0] > PI / 5) ) + { + // switch direction if angle too great, angle is shorter if attacking or stationary + entity->skill[0] = 1; + } + } + else // reverse of the above + { + if ( moving ) + { + entity->fskill[0] -= std::min(dist * MONSTER_MWALKSPEED, 2.f * MONSTER_MWALKSPEED); + } + else if ( my->monsterAttack != 0 ) + { + entity->fskill[0] -= MONSTER_MWALKSPEED; + } + else + { + entity->fskill[0] -= 0.007; + } + + if ( entity->fskill[0] < -PI / 32 ) + { + entity->skill[0] = 0; + } + } + entity->yaw += -entity->fskill[0] / 4; + //entity->pitch += entity->fskill[0] / 4; + entity->roll = -entity->fskill[0]; + } + break; + // head right + case 13: + { + entity->focalx = limbs[MYCONID][13][0]; + entity->focaly = limbs[MYCONID][13][1]; + entity->focalz = limbs[MYCONID][13][2]; + entity->x += limbs[MYCONID][14][0] * cos(my->yaw + PI / 2) + limbs[MYCONID][14][1] * cos(my->yaw); + entity->y += limbs[MYCONID][14][0] * sin(my->yaw + PI / 2) + limbs[MYCONID][14][1] * sin(my->yaw); + entity->z += limbs[MYCONID][14][2]; + entity->pitch = my->pitch; + if ( multiplayer != CLIENT ) + { + entity->sprite = 1529; + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + + if ( additionalLimb ) // follow the yaw of the previous limb. + { + entity->yaw -= -additionalLimb->fskill[0] / 4; + //entity->pitch += additionalLimb->fskill[0] / 16; + entity->roll = additionalLimb->fskill[0]; + } + } + break; + // head center + case 14: + entity->focalx = limbs[MYCONID][15][0]; + entity->focaly = limbs[MYCONID][15][1]; + entity->focalz = limbs[MYCONID][15][2]; + entity->x += limbs[MYCONID][16][0] * cos(my->yaw + PI / 2) + limbs[MYCONID][16][1] * cos(my->yaw); + entity->y += limbs[MYCONID][16][0] * sin(my->yaw + PI / 2) + limbs[MYCONID][16][1] * sin(my->yaw); + entity->z += limbs[MYCONID][16][2] - 0.01; + entity->pitch = my->pitch; + /*if ( keystatus[SDLK_h] ) + { + entity->fskill[0] += 0.05; + }*/ + if ( entity->sprite == 1526 ) + { + entity->focalz -= .25; + } + + entity->pitch += (entity->fskill[2]) * (PI / 64) * sin(entity->fskill[1]); + entity->roll = (entity->fskill[2]) * (PI / 64) * sin(entity->fskill[1] + PI / 2); + + real_t bobScale = 0.08 * entity->skill[0]; + entity->scalex = 1.01 + bobScale * sin(entity->fskill[3]); + entity->scaley = 1.01 + bobScale * sin(entity->fskill[3]); + entity->scalez = 1.01 - bobScale * sin(entity->fskill[3]); + + if ( my->monsterAttack == MONSTER_POSE_SPECIAL_WINDUP1 + && my->monsterAttackTime == 1 ) + { + entity->skill[0] = 4; + + entity->fskill[3] = 0.0; + entity->fskill[1] = 2 * PI * 0.0; + entity->fskill[2] = 0.0; + } + + if ( entity->skill[0] > 0 ) + { + entity->fskill[2] = std::min(1.0, std::max(0.05, entity->fskill[2] * 1.15)); + if ( entity->skill[0] == 4 ) // circle first + { + if ( entity->fskill[1] >= 2 * PI ) + { + entity->skill[0]--; + } + } + else + { + entity->fskill[3] += 0.25; + if ( entity->fskill[3] >= 2 * PI ) + { + entity->fskill[3] -= 2 * PI; + entity->skill[0]--; + } + } + } + else + { + entity->fskill[2] *= 0.95; + } + entity->fskill[1] += 0.125; + + //entity->scalex = 1.008 + std::max(0.0, 0.05 * sin(entity->fskill[0])); + //entity->scaley = 1.008 + std::max(0.0, 0.05 * sin(entity->fskill[0])); + //entity->scalez = 1.008 + 0.05 * sin(entity->fskill[0]); + if ( multiplayer != CLIENT ) + { + if ( debugModel && (svFlags & SV_FLAG_CHEATS) ) + { + static int stage = 0; + if ( keystatus[SDLK_g] ) + { + keystatus[SDLK_g] = 0; + stage += 1; + if ( stage > 2 ) + { + stage = 0; + } + } + entity->sprite = 1525; + if ( stage == 0 ) + { + entity->sprite = 1525; + } + else if ( stage == 1 ) + { + entity->sprite = 1526; + } + else if ( stage == 2 ) + { + entity->sprite = 1527; + } + } + else + { + entity->sprite = 1525; + if ( myStats->getAttribute("monster_m_type") == "duster" ) + { + entity->sprite = 1526; + } + else if ( myStats->getAttribute("monster_m_type") == "discanter" ) + { + entity->sprite = 1527; + } + } + + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring + || (helmet && helmet->sprite > 0 && !helmet->flags[INVISIBLE]) ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + break; + } + } + // rotate shield a bit + node_t* shieldNode = list_Node(&my->children, 8); + if ( shieldNode ) + { + Entity* shieldEntity = (Entity*)shieldNode->element; + if ( shieldEntity->sprite != items[TOOL_TORCH].index && shieldEntity->sprite != items[TOOL_LANTERN].index && shieldEntity->sprite != items[TOOL_CRYSTALSHARD].index ) + { + shieldEntity->yaw -= PI / 6; + } + } + if ( MONSTER_ATTACK > 0 && MONSTER_ATTACK <= MONSTER_POSE_MAGIC_CAST3 ) + { + MONSTER_ATTACKTIME++; + } + else if ( MONSTER_ATTACK == 0 ) + { + MONSTER_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } +} + +void Entity::monsterMChooseWeapon(const Entity* target, double dist) +{ + if ( monsterSpecialState != 0 ) + { + //Holding a weapon assigned from the special attack. Don't switch weapons. + //messagePlayer() + return; + } + + Stat *myStats = getStats(); + if ( !myStats ) + { + return; + } + + if ( myStats->weapon && (itemCategory(myStats->weapon) == SPELLBOOK) ) + { + return; + } + + bool inMeleeRange = monsterInMeleeRange(target, dist); + + //if ( inMeleeRange ) + //{ + // //if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 && local_rng.rand() % 10 == 0 ) + // //{ + // // bool tryThrow = true; + // // if ( tryThrow ) + // // { + // // node_t* thrownNode = itemNodeInInventory(myStats, -1, THROWN); + // // if ( thrownNode ) + // // { + // // bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, thrownNode, false, true); + // // if ( !swapped ) + // // { + // // //Don't return, make sure holding a melee weapon at least. + // // } + // // else + // // { + // // monsterSpecialState = MONSTER_M_SPECIAL_THROW; + // // return; + // // } + // // } + // // } + // //} + + // //Switch to a melee weapon if not already wielding one. Unless monster special state is overriding the AI. + // if ( !myStats->weapon || !isMeleeWeapon(*myStats->weapon) ) + // { + // node_t* weaponNode = getMeleeWeaponItemNodeInInventory(myStats); + // if ( !weaponNode ) + // { + // /*if ( myStats->weapon && myStats->weapon->type == MAGICSTAFF_SLOW ) + // { + // monsterUnequipSlotFromCategory(myStats, &myStats->weapon, MAGICSTAFF); + // }*/ + // return; //Resort to fists. + // } + + // bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, true); + // if ( !swapped ) + // { + // //Don't return so that monsters will at least equip ranged weapons in melee range if they don't have anything else. + // } + // else + // { + // return; + // } + // } + // else + // { + // return; + // } + //} + + if ( myStats->getAttribute("monster_m_type") == "discanter" ) + { + if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 + && local_rng.rand() % 3 == 0 ) + { + if ( (dist >= TOUCHRANGE && local_rng.rand() % 3 == 0) || dist > 80.0 ) + { + monsterSpecialState = MONSTER_M_SPECIAL_CAST3; + } + else + { + if ( dist <= TOUCHRANGE && myStats->getEffectActive(EFF_SPORES) ) + { + // inside melee and currently sporing + } + else + { + monsterSpecialState = MONSTER_M_SPECIAL_CAST1; + } + } + } + return; + } + + //Switch to a thrown weapon or a ranged weapon + /*if ( !myStats->weapon || isMeleeWeapon(*myStats->weapon) )*/ + { + //First search the inventory for a THROWN weapon. + node_t *weaponNode = nullptr; + if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 + && (dist > STRIKERANGE) + && local_rng.rand() % 3 == 0 ) + { + weaponNode = itemNodeInInventory(myStats, -1, THROWN); + if ( weaponNode ) + { + if ( swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, true) ) + { + monsterSpecialState = MONSTER_M_SPECIAL_THROW; + return; + } + } + } + //if ( !weaponNode ) + //{ + // //If couldn't find any, search the inventory for a ranged weapon. + // weaponNode = getRangedWeaponItemNodeInInventory(myStats, true); + //} + + bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, true); + return; + } + + return; +} + + + diff --git a/src/monster_mimic.cpp b/src/monster_mimic.cpp index 37b913ed9..ec6bc4998 100644 --- a/src/monster_mimic.cpp +++ b/src/monster_mimic.cpp @@ -132,11 +132,123 @@ void initMimic(Entity* my, Stat* myStats) } } +void initMiniMimic(Entity* my, Stat* myStats) +{ + node_t* node; + + my->z = 0; + my->initMonster(1794); + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = 619; + MONSTER_SPOTVAR = 3; + MONSTER_IDLESND = -1; + MONSTER_IDLEVAR = 1; + } + + my->monsterSpecialState = MIMIC_INERT; + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + if ( isMonsterStatsDefault(*myStats) ) + { + my->mimicSetStats(myStats); + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + if ( rng.rand() % 10 == 0 ) + { + my->setEffect(EFF_MIMIC_LOCKED, true, -1, false); + } + } + } + + // trunk + Entity* entity = newEntity(1794, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MINIMIMIC][1][0]; + entity->focaly = limbs[MINIMIMIC][1][1]; + entity->focalz = limbs[MINIMIMIC][1][2]; + entity->behavior = &actMiniMimicLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // lid + entity = newEntity(1795, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MINIMIMIC][2][0]; + entity->focaly = limbs[MINIMIMIC][2][1]; + entity->focalz = limbs[MINIMIMIC][2][2]; + entity->behavior = &actMiniMimicLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + if ( multiplayer == CLIENT || MONSTER_INIT ) + { + return; + } +} + void actMimicLimb(Entity* my) { my->actMonsterLimb(false); } +void actMiniMimicLimb(Entity* my) +{ + my->actMonsterLimb(false); +} + void mimicDie(Entity* my) { int c; @@ -179,6 +291,48 @@ void mimicDie(Entity* my) return; } +void miniMimicDie(Entity* my) +{ + int c; + for ( c = 0; c < 5; c++ ) + { + Entity* entity = spawnGib(my); + if ( entity ) + { + serverSpawnGibForClient(entity); + } + } + + my->spawnBlood(); + + // wood chunk particles + for ( c = 0; c < 10; c++ ) + { + Entity* entity = spawnGib(my); + entity->skill[5] = 1; // poof + entity->flags[INVISIBLE] = false; + entity->sprite = 187; // Splinter.vox + entity->x = my->x; + entity->y = my->y; + entity->z = -7 + local_rng.rand() % 14; + entity->yaw = (local_rng.rand() % 360) * PI / 180.0; + entity->pitch = (local_rng.rand() % 360) * PI / 180.0; + entity->roll = (local_rng.rand() % 360) * PI / 180.0; + entity->vel_x = cos(entity->yaw) * (0.5 + (local_rng.rand() % 100) / 100.f); + entity->vel_y = sin(entity->yaw) * (0.5 + (local_rng.rand() % 100) / 100.f); + entity->vel_z = -.25; + entity->fskill[3] = 0.04; + serverSpawnGibForClient(entity); + } + playSoundEntity(my, 625 + local_rng.rand() % 3, 64); + playSoundEntity(my, 177, 64); + + my->removeMonsterDeathNodes(); + + list_RemoveNode(my->mynode); + return; +} + #define MIMIC_TRUNK 2 #define MIMIC_LID 3 @@ -186,6 +340,11 @@ void mimicSpecialEat(Entity* my, Stat* myStats); // unused void mimicAnimate(Entity* my, Stat* myStats, double dist) { + if ( !my ) + { + return; + } + node_t* node; Entity* entity = nullptr; Entity* head = nullptr; @@ -193,12 +352,19 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) my->flags[INVISIBLE] = true; // hide the "AI" bodypart - my->focalx = limbs[MIMIC][0][0]; - my->focaly = limbs[MIMIC][0][1]; - my->focalz = limbs[MIMIC][0][2]; + Monster monsterType = MIMIC; + if ( my->getMonsterTypeFromSprite() == MINIMIMIC ) + { + monsterType = MINIMIMIC; + } + + my->focalx = limbs[monsterType][0][0]; + my->focaly = limbs[monsterType][0][1]; + my->focalz = limbs[monsterType][0][2]; if ( multiplayer != CLIENT ) { - my->z = limbs[MIMIC][5][2]; + my->z = limbs[monsterType][5][2]; + my->creatureHandleLiftZ(); } if ( multiplayer != CLIENT ) @@ -211,6 +377,23 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) { my->monsterRotate(); } + + if ( myStats->type == MIMIC ) + { + if ( myStats && myStats->getEffectActive(EFF_MIMIC_VOID) ) + { + my->sprite = 1792; + } + else + { + if ( my->sprite == 1792 ) + { + createParticleErupt(my, 625); + serverSpawnMiscParticles(my, PARTICLE_EFFECT_ERUPT, 625); + } + my->sprite = 1247; + } + } } bool bWalkCycle = false; @@ -236,6 +419,7 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) if ( bodypart == MIMIC_TRUNK ) { + entity->sprite = my->sprite; head = entity; auto& attackStageSwing = entity->skill[0]; @@ -335,8 +519,8 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) if ( my->monsterAttackTime >= 0 ) { - real_t rate = limbs[MIMIC][16][0]; - real_t setpoint = limbs[MIMIC][16][1]; + real_t rate = limbs[monsterType][16][0]; + real_t setpoint = limbs[monsterType][16][1]; if ( limbAngleWithinRange(entity->monsterWeaponYaw, rate, setpoint) ) { entity->monsterWeaponYaw = setpoint; @@ -361,8 +545,8 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) || my->monsterAttack == MONSTER_POSE_MIMIC_LOCKED2 || my->monsterAttack == MONSTER_POSE_MIMIC_MAGIC2 ) { - real_t rollRate = entity->fskill[2] * limbs[MIMIC][11][0] * 2; - const real_t rollSetpoint = entity->fskill[2] * limbs[MIMIC][11][1] * 2; + real_t rollRate = entity->fskill[2] * limbs[monsterType][11][0] * 2; + const real_t rollSetpoint = entity->fskill[2] * limbs[monsterType][11][1] * 2; if ( my->monsterAttack == MONSTER_POSE_MIMIC_DISTURBED2 ) { rollRate *= 1.5; @@ -417,28 +601,28 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) { if ( my->monsterAttackTime >= TICKS_PER_SECOND / 20 ) { - if ( limbAngleWithinRange(entity->monsterWeaponYaw, limbs[MIMIC][14][0], limbs[MIMIC][14][1]) ) + if ( limbAngleWithinRange(entity->monsterWeaponYaw, limbs[monsterType][14][0], limbs[monsterType][14][1]) ) { attackStageSwing = 0; - entity->monsterWeaponYaw = limbs[MIMIC][14][1]; + entity->monsterWeaponYaw = limbs[monsterType][14][1]; my->monsterAttack = 0; } else { - entity->monsterWeaponYaw += limbs[MIMIC][14][0]; + entity->monsterWeaponYaw += limbs[monsterType][14][0]; } } } else if ( attackStageSwing == 0 ) { - if ( limbAngleWithinRange(entity->monsterWeaponYaw, limbs[MIMIC][10][0], limbs[MIMIC][10][1]) ) + if ( limbAngleWithinRange(entity->monsterWeaponYaw, limbs[monsterType][10][0], limbs[monsterType][10][1]) ) { attackStageSwing = 1; - entity->monsterWeaponYaw = limbs[MIMIC][10][1]; + entity->monsterWeaponYaw = limbs[monsterType][10][1]; } else { - entity->monsterWeaponYaw += limbs[MIMIC][10][0]; + entity->monsterWeaponYaw += limbs[monsterType][10][0]; } } else if ( attackStageSwing == 1 ) @@ -479,14 +663,14 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) } else if ( walkCycle % 2 == 0 ) { - entity->monsterWeaponYaw -= limbs[MIMIC][12][1]; + entity->monsterWeaponYaw -= limbs[monsterType][12][1]; if ( walkShuffleRollDelay > 0 ) { - entity->monsterWeaponYaw -= limbs[MIMIC][12][1] * 4; + entity->monsterWeaponYaw -= limbs[monsterType][12][1] * 4; } - if ( entity->monsterWeaponYaw <= limbs[MIMIC][12][2] ) + if ( entity->monsterWeaponYaw <= limbs[monsterType][12][2] ) { - entity->monsterWeaponYaw = limbs[MIMIC][12][2]; + entity->monsterWeaponYaw = limbs[monsterType][12][2]; walkCycle++; if ( !aggressiveMove ) @@ -505,7 +689,7 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) } else if ( walkCycle % 2 == 1 ) { - entity->monsterWeaponYaw += limbs[MIMIC][12][0]; + entity->monsterWeaponYaw += limbs[monsterType][12][0]; if ( entity->monsterWeaponYaw >= 0.0 ) { entity->monsterWeaponYaw = 0.0; @@ -529,7 +713,7 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) } } - real_t rate = limbs[MIMIC][13][0]; + real_t rate = limbs[monsterType][13][0]; if ( walkCycle % 4 < 2 ) { walkShuffleYaw += rate; @@ -580,7 +764,7 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) } entity->roll = (PI / 16) * (walkCycle % 4 < 2 ? -roll : roll); - real_t bounceAmount = (aggressiveMove ? 2.0 : 1.0) + limbs[MIMIC][13][1]; + real_t bounceAmount = (aggressiveMove ? 2.0 : 1.0) + limbs[monsterType][13][1]; bounceFromRoll = roll * bounceAmount; entity->z -= bounceFromRoll; } @@ -589,6 +773,7 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) } else if ( bodypart == MIMIC_LID ) { + entity->sprite = my->sprite + 1; auto& attackStageSwing = entity->skill[0]; auto& lidBounceStage = entity->skill[1]; auto& lidBounceAngle = entity->fskill[1]; @@ -675,8 +860,8 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) if ( attackStageSwing == 0 ) { - real_t rate = limbs[MIMIC][15][1]; - real_t setpoint = limbs[MIMIC][15][2]; + real_t rate = limbs[monsterType][15][1]; + real_t setpoint = limbs[monsterType][15][2]; if ( limbAngleWithinRange(entity->monsterWeaponYaw, rate, setpoint) ) { entity->monsterWeaponYaw = setpoint; @@ -689,7 +874,7 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) } else { - real_t rate = limbs[MIMIC][15][0]; + real_t rate = limbs[monsterType][15][0]; if ( limbAngleWithinRange(entity->monsterWeaponYaw, rate, 0.0) ) { entity->monsterWeaponYaw = 0.0; @@ -798,42 +983,42 @@ void mimicAnimate(Entity* my, Stat* myStats, double dist) switch ( bodypart ) { case MIMIC_TRUNK: - entity->x += limbs[MIMIC][3][0] * cos(my->yaw); - entity->y += limbs[MIMIC][3][1] * sin(my->yaw); - entity->z += limbs[MIMIC][3][2]; - entity->focalx = limbs[MIMIC][1][0]; - entity->focaly = limbs[MIMIC][1][1]; - entity->focalz = limbs[MIMIC][1][2]; + entity->x += limbs[monsterType][3][0] * cos(my->yaw); + entity->y += limbs[monsterType][3][1] * sin(my->yaw); + entity->z += limbs[monsterType][3][2]; + entity->focalx = limbs[monsterType][1][0]; + entity->focaly = limbs[monsterType][1][1]; + entity->focalz = limbs[monsterType][1][2]; if ( entity->pitch < 0.0 ) { - entity->z += -0.5 * sin(entity->pitch) * limbs[MIMIC][7][2]; + entity->z += -0.5 * sin(entity->pitch) * limbs[monsterType][7][2]; } else { - entity->z += -0.5 * sin(entity->pitch) * limbs[MIMIC][7][1]; + entity->z += -0.5 * sin(entity->pitch) * limbs[monsterType][7][1]; } - //entity->z += sin(PI * entity->fskill[0] / 3) * limbs[MIMIC][7][2]; + //entity->z += sin(PI * entity->fskill[0] / 3) * limbs[monsterType][7][2]; break; case MIMIC_LID: - entity->x += limbs[MIMIC][4][0] * cos(my->yaw); - entity->y += limbs[MIMIC][4][1] * sin(my->yaw); - entity->z += limbs[MIMIC][4][2]; - entity->focalx = limbs[MIMIC][2][0]; - entity->focaly = limbs[MIMIC][2][1]; - entity->focalz = limbs[MIMIC][2][2]; - - //entity->x += limbs[MIMIC][6][0] * cos(my->yaw) * sin(entity->fskill[0] * PI / 8); - //entity->y += limbs[MIMIC][6][1] * sin(my->yaw) * sin(entity->fskill[0] * PI / 8); - //entity->z += limbs[MIMIC][6][2] * sin(entity->fskill[0] * PI / 8); + entity->x += limbs[monsterType][4][0] * cos(my->yaw); + entity->y += limbs[monsterType][4][1] * sin(my->yaw); + entity->z += limbs[monsterType][4][2]; + entity->focalx = limbs[monsterType][2][0]; + entity->focaly = limbs[monsterType][2][1]; + entity->focalz = limbs[monsterType][2][2]; + + //entity->x += limbs[monsterType][6][0] * cos(my->yaw) * sin(entity->fskill[0] * PI / 8); + //entity->y += limbs[monsterType][6][1] * sin(my->yaw) * sin(entity->fskill[0] * PI / 8); + //entity->z += limbs[monsterType][6][2] * sin(entity->fskill[0] * PI / 8); if ( head->pitch < 0.0 ) { - entity->z += -0.5 * sin(head->pitch) * limbs[MIMIC][6][2]; + entity->z += -0.5 * sin(head->pitch) * limbs[monsterType][6][2]; } else { - entity->z += -0.5 * sin(head->pitch) * limbs[MIMIC][6][1]; + entity->z += -0.5 * sin(head->pitch) * limbs[monsterType][6][1]; } break; default: @@ -885,7 +1070,7 @@ bool Entity::disturbMimic(Entity* touched, bool takenDamage, bool doMessage) } } - if ( myStats && myStats->EFFECTS[EFF_MIMIC_LOCKED] ) + if ( myStats && myStats->getEffectActive(EFF_MIMIC_LOCKED) ) { if ( myStats->EFFECTS_TIMERS[EFF_MIMIC_LOCKED] == -1 ) { @@ -898,7 +1083,7 @@ bool Entity::disturbMimic(Entity* touched, bool takenDamage, bool doMessage) } } - if ( myStats && !myStats->EFFECTS[EFF_MIMIC_LOCKED] ) + if ( myStats && !myStats->getEffectActive(EFF_MIMIC_LOCKED) ) { if ( monsterSpecialState == MIMIC_INERT ) { @@ -936,13 +1121,24 @@ bool Entity::disturbMimic(Entity* touched, bool takenDamage, bool doMessage) monsterAcquireAttackTarget(*touched, MONSTER_STATE_PATH, true); if ( touched->behavior == &actPlayer ) { - messagePlayerColor(touched->skill[2], MESSAGE_INTERACTION, - makeColorRGB(255, 0 ,0), Language::get(6082)); + if ( doMessage ) + { + if ( myStats && myStats->getEffectActive(EFF_MIMIC_VOID) ) + { + messagePlayerColor(touched->skill[2], MESSAGE_INTERACTION, + makeColorRGB(255, 0, 0), Language::get(6559)); + } + else + { + messagePlayerColor(touched->skill[2], MESSAGE_INTERACTION, + makeColorRGB(255, 0 ,0), Language::get(6082)); + } + } } } } - if ( !myStats->EFFECTS[EFF_MIMIC_LOCKED] ) + if ( myStats && !myStats->getEffectActive(EFF_MIMIC_LOCKED) ) { attack(MONSTER_POSE_MIMIC_DISTURBED, 0, nullptr); playSoundEntity(this, 21, 64); diff --git a/src/monster_minotaur.cpp b/src/monster_minotaur.cpp index 0178bc000..f9a26a3d8 100644 --- a/src/monster_minotaur.cpp +++ b/src/monster_minotaur.cpp @@ -70,14 +70,14 @@ void initMinotaur(Entity* my, Stat* myStats) myStats->STR = 60; myStats->DEX = 20; myStats->CON = 20; - myStats->EFFECTS[EFF_VAMPIRICAURA] = true; + myStats->setEffectActive(EFF_VAMPIRICAURA, 1); myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] = -1; } // random effects // minotaurs can traverse waters and pits (pits with magic :)) - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; // generates equipment and weapons if available from editor @@ -303,7 +303,7 @@ void minotaurMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -354,6 +354,9 @@ void minotaurMoveBodyparts(Entity* my, Stat* myStats, double dist) bodypart++; } } + + my->z = -6; + my->creatureHandleLiftZ(); } //Move bodyparts @@ -716,7 +719,7 @@ void actMinotaurTimer(Entity* my) if ( spawnedsomebody ) { - playSoundNotification(175, 128); + playSoundNotification(175, 64); for ( c = 0; c < MAXPLAYERS; c++ ) { Uint32 color = makeColorRGB(0, 255, 255); @@ -883,7 +886,7 @@ void actMinotaurCeilingBuster(Entity* my) if ( myStats ) { // easy hack to stop the minotaur while he breaks stuff - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 10; } } @@ -968,7 +971,7 @@ void actMinotaurCeilingBuster(Entity* my) } list_RemoveNode(entity->mynode); } - else if ( entity->behavior == &actDoor ) + else if ( entity->behavior == &actDoor || entity->behavior == &actIronDoor ) { if ( multiplayer != CLIENT ) { diff --git a/src/monster_moth.cpp b/src/monster_moth.cpp new file mode 100644 index 000000000..a0af23d72 --- /dev/null +++ b/src/monster_moth.cpp @@ -0,0 +1,1249 @@ +/*------------------------------------------------------------------------------- + + BARONY + File: monster_skeleton.cpp + Desc: implements all of the skeleton monster's code + + Copyright 2013-2016 (c) Turning Wheel LLC, all rights reserved. + See LICENSE for details. + +-------------------------------------------------------------------------------*/ + +#include "main.hpp" +#include "game.hpp" +#include "stat.hpp" +#include "entity.hpp" +#include "items.hpp" +#include "monster.hpp" +#include "engine/audio/sound.hpp" +#include "net.hpp" +#include "collision.hpp" +#include "player.hpp" +#include "magic/magic.hpp" +#include "prng.hpp" +#include "scores.hpp" +#include "mod_tools.hpp" + +void initMoth(Entity* my, Stat* myStats) +{ + node_t* node; + + my->z = 0; + my->initMonster(1819); + if ( my->sprite == 1822 ) + { + my->flags[BURNABLE] = false; + } + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = -1; + MONSTER_SPOTVAR = 1; + MONSTER_IDLESND = -1; + MONSTER_IDLEVAR = 3; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + myStats->setAttribute("moth_state", "0"); + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + if ( myStats->getAttribute("special_npc") == "fire sprite" ) + { + // min 1, max 10 + myStats->HP = myStats->LVL * 10; //10-100 + myStats->MAXHP = myStats->HP; + myStats->OLDHP = myStats->HP; + myStats->STR = myStats->LVL - 6; //-5-5 + myStats->DEX = myStats->LVL; // 1-10 + myStats->INT = myStats->LVL; // 1-10 + myStats->CON = myStats->LVL; // 1-10 + myStats->PER = 3 + myStats->LVL; // 3-13 + } + } + } + + for ( int i = 0; i < 6; ++i ) + { + if ( i >= 1 && my->sprite == 1822 ) + { + continue; + } + + // body + Entity* entity = newEntity(my->sprite, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[MOTH_SMALL][1][0]; + entity->focaly = limbs[MOTH_SMALL][1][1]; + entity->focalz = limbs[MOTH_SMALL][1][2]; + entity->behavior = &actMothLimb; + + // animation offsets + entity->fskill[1] = (2 * PI) * i / 6.0; // flapping + + if ( i == 1 || i == 3 ) + { + entity->fskill[6] = i == 1 ? 1.0 : -1.0; // left/right + } + else if ( i == 0 || i == 2 ) + { + entity->fskill[7] = i == 2 ? 1.0 : -1.0; // up/down + } + else if ( i == 4 ) + { + entity->fskill[8] = -3.0; // circling + entity->fskill[7] = 2.0; // up/down + } + else if ( i == 5 ) + { + entity->fskill[8] = 3.0; // circling + entity->fskill[7] = -2.0; // up/down + entity->fskill[9] = PI; // current circling + } + + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // wingleft + entity = newEntity(my->sprite + 1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[MOTH_SMALL][3][0]; + entity->focaly = limbs[MOTH_SMALL][3][1]; + entity->focalz = limbs[MOTH_SMALL][3][2]; + entity->behavior = &actMothLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // wingright + entity = newEntity(my->sprite + 2, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[MOTH_SMALL][4][0]; + entity->focaly = limbs[MOTH_SMALL][4][1]; + entity->focalz = limbs[MOTH_SMALL][4][2]; + entity->behavior = &actMothLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + } +} + +void actMothLimb(Entity* my) +{ + my->actMonsterLimb(false); + if ( my->sprite == 1822 || my->sprite == 1822 + 1 || my->sprite == 1822 + 2 ) + { + my->lightBonus = vec4_t{ 0.25, 0.25, 0.25, 0.0 }; + } +} + +void mothDie(Entity* my) +{ + Stat* myStats = my->getStats(); + if ( myStats && myStats->getAttribute("fire_sprite") != "" ) + { + spawnPoof(my->x, my->y, my->z, 0.25, true); + } + else + { + int c; + for ( c = 0; c < 3; c++ ) + { + Entity* entity = spawnGib(my); + if ( entity ) + { + entity->skill[5] = 1; // poof + + switch ( c ) + { + case 0: + entity->sprite = my->sprite; + break; + case 1: + entity->sprite = my->sprite + 1; + break; + case 2: + entity->sprite = my->sprite + 2; + break; + default: + break; + } + + serverSpawnGibForClient(entity); + } + } + + my->spawnBlood(); + + playSoundEntity(my, 670 + local_rng.rand() % 2, 128); + } + + my->removeMonsterDeathNodes(); + + list_RemoveNode(my->mynode); + return; +} + +#define MOTH_BODY 2 +#define MOTH_LEFTWING 3 +#define MOTH_RIGHTWING 4 + +#define BODY_ATTACK body->skill[3] +#define BODY_ATTACKTIME body->skill[4] +#define BODY_INIT body->skill[5] +#define BODY_FLOAT_X body->fskill[2] +#define BODY_FLOAT_Y body->fskill[3] +#define BODY_FLOAT_Z body->fskill[4] +#define BODY_FLOAT_ATK body->fskill[5] +#define BODY_LEFTRIGHT_OFFSET body->fskill[6] +#define BODY_HEIGHT_OFFSET body->fskill[7] +#define BODY_CIRCLING_AMOUNT body->fskill[8] +#define BODY_CURRENT_CIRCLING body->fskill[9] +#define BODY_OFFSET_REDUCE body->fskill[10] +#define BODY_CIRCLING_ATTACK body->fskill[11] +#define BODY_CIRCLING_ATTACK_SETPOINT body->fskill[12] + +int mothGetAttackPose(Entity* my, int basePose) +{ + if ( !my ) + { + return 0; + } + if ( basePose == MONSTER_POSE_MELEE_WINDUP1 ) + { + // find a body available to attack + std::vector available; + for ( int i = 0; i < my->bodyparts.size(); i += 3 ) + { + if ( (i / 3) < 4 ) + { + Entity* body = my->bodyparts.at(i); + if ( BODY_ATTACK == 0 && !body->flags[INVISIBLE] ) + { + available.push_back(i); + } + } + } + + if ( available.size() > 0 ) + { + int pick = available[local_rng.rand() % available.size()]; + switch ( pick / 3 ) + { + case 0: + return MONSTER_POSE_MELEE_WINDUP1; + case 1: + return MONSTER_POSE_MELEE_WINDUP2; + case 2: + return MONSTER_POSE_MELEE_WINDUP3; + case 3: + return MONSTER_POSE_RANGED_WINDUP1; + default: + return 0; + } + } + else + { + return 0; + } + } + else if ( basePose == MONSTER_POSE_MAGIC_WINDUP1 ) + { + Stat* myStats = my->getStats(); + if ( myStats && myStats->getAttribute("fire_sprite") != "" ) + { + if ( my->bodyparts.size() > 0 ) + { + Entity* body = my->bodyparts.at(0); + if ( BODY_ATTACK == 0 && !body->flags[INVISIBLE] ) + { + return MONSTER_POSE_MAGIC_WINDUP1; + } + } + return 0; + } + + // find a body available to attack + std::vector available; + for ( int i = 0; i < my->bodyparts.size(); i += 3 ) + { + if ( (i / 3) >= 4 ) + { + Entity* body = my->bodyparts.at(i); + if ( BODY_ATTACK == 0 && !body->flags[INVISIBLE] ) + { + available.push_back(i); + } + } + } + + if ( available.size() > 0 ) + { + int pick = available[local_rng.rand() % available.size()]; + switch ( pick / 3 ) + { + case 4: + return MONSTER_POSE_MAGIC_WINDUP1; + case 5: + return MONSTER_POSE_MAGIC_WINDUP2; + default: + return 0; + } + } + else + { + return 0; + } + } + else + { + return basePose; + } +} + +void mothAnimate(Entity* my, Stat* myStats, double dist) +{ + node_t* node; + Entity* entity = nullptr; + Entity* head = nullptr; + int bodypart; + + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + //my->flags[PASSABLE] = true; + + my->sizex = 4; + my->sizey = 4; + + my->focalx = limbs[MOTH_SMALL][0][0]; + my->focaly = limbs[MOTH_SMALL][0][1]; + my->focalz = limbs[MOTH_SMALL][0][2]; + if ( multiplayer != CLIENT ) + { + my->z = limbs[MOTH_SMALL][5][2]; + if ( !myStats->getEffectActive(EFF_LEVITATING) ) + { + myStats->setEffectActive(EFF_LEVITATING, 1); + myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; + } + my->creatureHandleLiftZ(); + } + + //my->setEffect(EFF_STUNNED, true, -1, false); + //my->monsterLookDir = 0.0; + //my->yaw = 0.0; + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_5] ) + { + my->yaw += 0.05; + my->monsterLookDir = my->yaw; + } + if ( keystatus[SDLK_g] ) + { + keystatus[SDLK_g] = 0; + //MONSTER_ATTACK = mothGetAttackPose(my, MONSTER_POSE_MELEE_WINDUP1); + MONSTER_ATTACK = mothGetAttackPose(my, MONSTER_POSE_MAGIC_WINDUP1); + MONSTER_ATTACKTIME = 0; + } + if ( keystatus[SDLK_h] ) + { + keystatus[SDLK_h] = 0; + myStats->setEffectValueUnsafe(EFF_STUNNED, myStats->getEffectActive(EFF_STUNNED) ? 0 : 1); + myStats->EFFECTS_TIMERS[EFF_STUNNED] = myStats->getEffectActive(EFF_STUNNED) ? -1 : 0; + } + } + + my->mistformGLRender = 0.0; + if ( myStats && myStats->getEffectActive(EFF_MIST_FORM) ) + { + my->mistformGLRender = 1.0; + } + + bool fireSprite = my->sprite == 1822; + + if ( fireSprite ) + { + my->removeLightField(); + my->light = addLight(my->x / 16, my->y / 16, "fire_sprite_glow"); + } + + if ( multiplayer != CLIENT && myStats ) + { + if ( fireSprite ) + { + if ( myStats->getAttribute("fire_sprite") != "" ) + { + if ( my->ticks > std::stoi(myStats->getAttribute("fire_sprite")) ) + { + my->setHP(0); + my->setObituary(Language::get(6620)); + } + } + } + + real_t percentHP = myStats->HP / (real_t)std::max(1, myStats->MAXHP); + if ( percentHP < 0.1 ) + { + myStats->setAttribute("moth_state", "5"); + } + else if ( percentHP < 0.2 ) + { + myStats->setAttribute("moth_state", "4"); + } + else if ( percentHP < 0.4 ) + { + myStats->setAttribute("moth_state", "3"); + } + else if ( percentHP < 0.6 ) + { + myStats->setAttribute("moth_state", "2"); + } + else if ( percentHP < 0.8 ) + { + myStats->setAttribute("moth_state", "1"); + } + else + { + myStats->setAttribute("moth_state", "0"); + } + + int state = myStats->getAttribute("moth_state") != "" ? std::stoi(myStats->getAttribute("moth_state")) : 0; + std::set toDisappear; + if ( state >= 1 ) + { + toDisappear.insert(5); + } + if ( state >= 2 ) + { + toDisappear.insert(3); + } + if ( state >= 3 ) + { + toDisappear.insert(2); + } + if ( state >= 4 ) + { + toDisappear.insert(1); + } + if ( state >= 5 ) + { + toDisappear.insert(4); + } + + for ( int i = 0; i < my->bodyparts.size(); i += 3 ) + { + int index = i / 3; + Entity* body = my->bodyparts.at(i); + if ( toDisappear.find(index) != toDisappear.end() ) + { + if ( !my->bodyparts.at(i)->flags[INVISIBLE] ) + { + my->bodyparts.at(i)->flags[INVISIBLE] = true; + serverUpdateEntityBodypart(my, i + MOTH_BODY); + for ( int c = 0; c < 3; c++ ) + { + Entity* entity = spawnGib(my); + if ( entity ) + { + entity->x = my->bodyparts.at(i)->x; + entity->y = my->bodyparts.at(i)->y; + entity->z = my->bodyparts.at(i)->z; + entity->skill[5] = 1; // poof + + switch ( c ) + { + case 0: + entity->sprite = my->bodyparts.at(i)->sprite; + break; + case 1: + entity->sprite = my->bodyparts.at(i)->sprite + 1; + break; + case 2: + entity->sprite = my->bodyparts.at(i)->sprite + 2; + break; + default: + break; + } + + serverSpawnGibForClient(entity); + } + } + } + } + else + { + if ( !BODY_INIT ) + { + BODY_INIT = 1; + body->flags[INVISIBLE] = false; + } + else + { + body->flags[INVISIBLE] = false; + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( fireSprite ) + { + // catch all for extra limbs hide + for ( int i = 3; i < my->bodyparts.size(); ++i ) + { + Entity* bodypart = my->bodyparts.at(i); + bodypart->flags[INVISIBLE] = true; + bodypart->flags[INVISIBLE_DITHER] = false; + } + } + } + + int numBodies = 0; + for ( int i = 0; i < my->bodyparts.size(); i += 3 ) + { + Entity* body = my->bodyparts.at(i); + if ( !body->flags[INVISIBLE] ) + { + ++numBodies; + } + } + + if ( MONSTER_ATTACK >= MONSTER_POSE_MELEE_WINDUP1 && MONSTER_ATTACK <= MONSTER_POSE_RANGED_WINDUP1 + && MONSTER_ATTACKTIME == 0 ) + { + int bodypart = (MONSTER_ATTACK - MONSTER_POSE_MELEE_WINDUP1) * 3; + if ( bodypart < my->bodyparts.size() ) + { + Entity* body = my->bodyparts.at(bodypart); + BODY_ATTACK = MONSTER_POSE_MELEE_WINDUP1; + BODY_ATTACKTIME = 0; + } + } + else if ( MONSTER_ATTACK >= MONSTER_POSE_MAGIC_WINDUP1 && MONSTER_ATTACK <= MONSTER_POSE_MAGIC_WINDUP2 + && MONSTER_ATTACKTIME == 0 ) + { + int bodypart = 3 * 4 + ((MONSTER_ATTACK - MONSTER_POSE_MAGIC_WINDUP1) * 3); + if ( fireSprite ) + { + bodypart = 0; + } + if ( bodypart < my->bodyparts.size() ) + { + Entity* body = my->bodyparts.at(bodypart); + BODY_ATTACK = MONSTER_POSE_MAGIC_WINDUP1; + BODY_ATTACKTIME = 0; + } + /*int bodypart = (local_rng.rand() % 6) * 3; + if ( bodypart < my->bodyparts.size() ) + { + Entity* body = my->bodyparts.at(bodypart); + BODY_ATTACK = MONSTER_POSE_MAGIC_WINDUP1; + BODY_ATTACKTIME = 0; + }*/ + } + + //Move bodyparts + Entity* leftWing = nullptr; + Entity* body = nullptr; + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, ++bodypart ) + { + if ( bodypart < MOTH_BODY ) + { + continue; + } + + entity = (Entity*)node->element; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + entity->mistformGLRender = my->mistformGLRender; + if ( (bodypart - MOTH_BODY) % 3 == 0 ) // bodies + { + body = entity; + } + + if ( (bodypart - MOTH_BODY) / 3 >= 4 ) // bodies circling + { + entity->yaw = 0.0; + } + else + { + entity->yaw = my->yaw; + } + + if ( (bodypart - MOTH_BODY) % 3 == 0 ) // bodies + { + entity->sprite = my->sprite; + body = entity; + + if ( multiplayer == SERVER ) + { + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + + entity->fskill[0] = fmod(entity->fskill[0], 2 * PI); + while ( entity->fskill[0] >= PI ) + { + entity->fskill[0] -= 2 * PI; + } + while ( entity->fskill[0] < -PI ) + { + entity->fskill[0] += 2 * PI; + } + if ( MONSTER_ATTACK == 0 ) + { + BODY_FLOAT_ATK = 0.0; + } + + if ( BODY_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + if ( BODY_ATTACKTIME == 0 ) + { + entity->fskill[0] = 0.0; + entity->skill[1] = 0; + BODY_FLOAT_ATK = 0.0; + BODY_CIRCLING_ATTACK = 0.0; + BODY_CIRCLING_ATTACK_SETPOINT = 2.0; + + playSoundEntityLocal(my, 170, 32); + } + else + { + if ( BODY_ATTACKTIME >= 2 * TICKS_PER_SECOND ) + { + if ( multiplayer != CLIENT ) + { + real_t prevYaw = my->yaw; + if ( Entity* target = uidToEntity(my->monsterTarget) ) + { + my->yaw = atan2(target->y - my->y, target->x - my->x); + } + if ( fireSprite ) + { + castSpell(my->getUID(), getSpellFromID(SPELL_FLAMES), true, false); + } + else + { + int spell = local_rng.rand() % 3; + if ( spell == 0 ) + { + castSpell(my->getUID(), getSpellFromID(SPELL_TELEPULL), true, false); + } + else if ( spell == 1 ) + { + castSpell(my->getUID(), getSpellFromID(SPELL_MIST_FORM), true, false); + } + else + { + castSpell(my->getUID(), getSpellFromID(SPELL_CONFUSE), true, false); + } + } + my->yaw = prevYaw; + } + BODY_ATTACK = 0; + BODY_CIRCLING_ATTACK_SETPOINT = 0.0; + } + } + } + + if ( BODY_ATTACK == MONSTER_POSE_MELEE_WINDUP1 || BODY_ATTACK == 1 ) // main body + { + { + if ( BODY_ATTACKTIME == 0 ) + { + entity->fskill[0] = 0.0; + entity->skill[1] = 0; + BODY_FLOAT_ATK = 0.0; + } + else + { + if ( BODY_ATTACKTIME >= (int)limbs[MOTH_SMALL][15][0] ) + { + if ( BODY_ATTACKTIME == (int)limbs[MOTH_SMALL][15][0] ) + { + if ( multiplayer != CLIENT ) + { + const Sint32 temp = MONSTER_ATTACKTIME; + if ( !body->flags[INVISIBLE] ) + { + my->attack(1, 0, nullptr); // slop + } + BODY_ATTACK = 1; + MONSTER_ATTACKTIME = temp; + } + } + + if ( entity->skill[1] == 0 ) + { + real_t speed = limbs[MOTH_SMALL][13][2]; + real_t setpoint = PI / 16; + if ( limbAngleWithinRange(entity->fskill[0], -speed, setpoint) ) + { + entity->fskill[0] = setpoint; + entity->skill[1] = 1; + } + else + { + entity->fskill[0] -= speed; + entity->fskill[0] = std::max(entity->fskill[0], setpoint); + } + } + else + { + real_t speed = limbs[MOTH_SMALL][13][1]; + entity->fskill[0] += speed; + entity->fskill[0] = std::min(entity->fskill[0], 0.0); + } + } + else + { + real_t speed = limbs[MOTH_SMALL][13][0]; + entity->fskill[0] -= speed; + entity->fskill[0] = std::max(entity->fskill[0], -((PI / 2) + PI / 32)); + } + + if ( BODY_ATTACKTIME >= (int)limbs[MOTH_SMALL][18][0] ) + { + BODY_FLOAT_ATK -= limbs[MOTH_SMALL][18][1]; + BODY_FLOAT_ATK = std::max(BODY_FLOAT_ATK, (real_t)limbs[MOTH_SMALL][18][2]); + } + else if ( BODY_ATTACKTIME >= (int)limbs[MOTH_SMALL][17][0] ) + { + BODY_FLOAT_ATK += limbs[MOTH_SMALL][17][1]; + BODY_FLOAT_ATK = std::min(BODY_FLOAT_ATK, (real_t)limbs[MOTH_SMALL][17][2]); + } + else if ( BODY_ATTACKTIME >= (int)limbs[MOTH_SMALL][16][0] ) + { + BODY_FLOAT_ATK -= limbs[MOTH_SMALL][16][1]; + BODY_FLOAT_ATK = std::max(BODY_FLOAT_ATK, (real_t)limbs[MOTH_SMALL][16][2]); + } + } + + if ( BODY_ATTACKTIME >= (int)limbs[MOTH_SMALL][15][1] ) + { + BODY_ATTACK = 0; + } + } + } + } + if ( (bodypart - MOTH_BODY) % 3 == 1 ) // leftwings + { + entity->sprite = my->sprite + 1; + entity->fskill[1] = fmod(entity->fskill[1], 2 * PI); + while ( entity->fskill[1] >= PI ) + { + entity->fskill[1] -= 2 * PI; + } + while ( entity->fskill[1] < -PI ) + { + entity->fskill[1] += 2 * PI; + } + entity->fskill[0] = fmod(entity->fskill[0], 2 * PI); + while ( entity->fskill[0] >= PI ) + { + entity->fskill[0] -= 2 * PI; + } + while ( entity->fskill[0] < -PI ) + { + entity->fskill[0] += 2 * PI; + } + + if ( BODY_ATTACK == MONSTER_POSE_MELEE_WINDUP1 || BODY_ATTACK == 1 ) + { + if ( BODY_ATTACKTIME == 0 ) + { + entity->fskill[1] = -PI / 2; + entity->skill[1] = 0; + } + + + if ( entity->skill[1] == 0 ) + { + real_t speed = limbs[MOTH_SMALL][14][0]; + if ( limbAngleWithinRange(entity->fskill[1], speed, 0.0) ) + { + entity->fskill[1] = 0.0; + entity->skill[1] = 1; + } + else + { + if ( entity->fskill[1] > 0.01 ) + { + entity->fskill[1] -= speed; + } + else if ( entity->fskill[1] < -0.01 ) + { + entity->fskill[1] += speed; + } + } + entity->fskill[0] = body->fskill[0]; + } + else if ( entity->skill[1] == 1 ) + { + real_t speed = limbs[MOTH_SMALL][14][0]; + if ( limbAngleWithinRange(entity->fskill[1], speed, PI / 4) ) + { + entity->fskill[1] = PI / 4; + entity->skill[1] = 2; + } + else + { + entity->fskill[1] += speed; + } + entity->fskill[0] = body->fskill[0]; + } + else if ( entity->skill[1] == 2 ) + { + real_t speed = limbs[MOTH_SMALL][14][1]; + real_t setpoint = -PI / 2 - PI / 8; + if ( limbAngleWithinRange(entity->fskill[1], -speed, setpoint) ) + { + entity->fskill[1] = setpoint; + entity->skill[1] = 3; + } + else + { + entity->fskill[1] -= speed; + } + entity->fskill[0] = body->fskill[0]; + } + else if ( entity->skill[1] == 3 ) + { + real_t speed = limbs[MOTH_SMALL][14][2]; + if ( limbAngleWithinRange(entity->fskill[1], speed, 0.0) ) + { + entity->fskill[1] = 0.0; + entity->skill[1] = 4; + } + else + { + entity->fskill[1] += speed; + } + if ( limbAngleWithinRange(entity->fskill[0], speed * 2, 0.0) ) + { + entity->fskill[0] = 0.0; + } + else + { + entity->fskill[0] += speed * 2; + } + } + } + } + else if ( (bodypart - MOTH_BODY) % 3 == 1 ) // rightwings + { + entity->sprite = my->sprite + 2; + } + + switch ( bodypart ) + { + case MOTH_BODY + 3 * 0: + case MOTH_BODY + 3 * 1: + case MOTH_BODY + 3 * 2: + case MOTH_BODY + 3 * 3: + case MOTH_BODY + 3 * 4: + case MOTH_BODY + 3 * 5: + { + entity->x += limbs[MOTH_SMALL][6][0] * cos(entity->yaw); + entity->y += limbs[MOTH_SMALL][6][1] * sin(entity->yaw); + entity->z += limbs[MOTH_SMALL][6][2]; + entity->focalx = limbs[MOTH_SMALL][1][0]; + entity->focaly = limbs[MOTH_SMALL][1][1]; + entity->focalz = limbs[MOTH_SMALL][1][2]; + + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_6] ) + { + entity->fskill[0] -= 0.05; + } + else if ( keystatus[SDLK_KP_4] ) + { + entity->fskill[0] += 0.05; + } + } + entity->pitch = entity->fskill[0]; + + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_PLUS] ) + { + keystatus[SDLK_KP_PLUS] = 0; + entity->skill[0] = entity->skill[0] == 0 ? 1 : 0; + } + } + if ( entity->skill[0] == 0 ) + { + entity->fskill[1] += 0.1; + } + + { + BODY_FLOAT_X = limbs[MOTH_SMALL][10][0] * sin(body->fskill[1] * limbs[MOTH_SMALL][11][0]) * cos(entity->yaw + PI / 2); + BODY_FLOAT_Y = limbs[MOTH_SMALL][10][1] * sin(body->fskill[1] * limbs[MOTH_SMALL][11][1]) * sin(entity->yaw + PI / 2); + BODY_FLOAT_Z = limbs[MOTH_SMALL][10][2] * sin(body->fskill[1] * limbs[MOTH_SMALL][11][2]); + real_t floatAtkZ = BODY_FLOAT_ATK < 0 ? 2 * sin(BODY_FLOAT_ATK * PI / 8) : 0.5 * sin(BODY_FLOAT_ATK * PI / 8); + BODY_FLOAT_Z += floatAtkZ; + } + + BODY_FLOAT_X += BODY_FLOAT_ATK * cos(entity->yaw); + BODY_FLOAT_Y += BODY_FLOAT_ATK * sin(entity->yaw); + + real_t reduce = 1.0 - BODY_OFFSET_REDUCE; + real_t setpoint = std::max(0.0, (numBodies - 1) / (real_t)5); + if ( BODY_OFFSET_REDUCE > setpoint + 0.01 ) + { + BODY_OFFSET_REDUCE -= 0.05; + BODY_OFFSET_REDUCE = std::max(setpoint, BODY_OFFSET_REDUCE); + } + if ( BODY_OFFSET_REDUCE < setpoint - 0.01 ) + { + BODY_OFFSET_REDUCE += 0.05; + BODY_OFFSET_REDUCE = std::min(setpoint, BODY_OFFSET_REDUCE); + } + + BODY_FLOAT_X += BODY_OFFSET_REDUCE * BODY_LEFTRIGHT_OFFSET * cos(entity->yaw + PI / 2) * limbs[MOTH_SMALL][2][0]; + BODY_FLOAT_Y += BODY_OFFSET_REDUCE * BODY_LEFTRIGHT_OFFSET * sin(entity->yaw + PI / 2) * limbs[MOTH_SMALL][2][0]; + BODY_FLOAT_X += BODY_OFFSET_REDUCE * BODY_LEFTRIGHT_OFFSET * cos(entity->yaw) * limbs[MOTH_SMALL][2][1]; + BODY_FLOAT_Y += BODY_OFFSET_REDUCE * BODY_LEFTRIGHT_OFFSET * sin(entity->yaw) * limbs[MOTH_SMALL][2][1]; + BODY_FLOAT_Z += BODY_OFFSET_REDUCE * BODY_HEIGHT_OFFSET * limbs[MOTH_SMALL][2][2]; + + { + real_t setpoint = BODY_CIRCLING_ATTACK_SETPOINT; + if ( BODY_CIRCLING_ATTACK > setpoint + 0.01 ) + { + real_t diff = std::max(0.025, (BODY_CIRCLING_ATTACK - setpoint) / 10.0); + BODY_CIRCLING_ATTACK -= diff; + BODY_CIRCLING_ATTACK = std::max(setpoint, BODY_CIRCLING_ATTACK); + } + if ( BODY_CIRCLING_ATTACK < setpoint - 0.01 ) + { + real_t diff = std::max(0.025, (setpoint - BODY_CIRCLING_ATTACK) / 10.0); + BODY_CIRCLING_ATTACK += diff; + BODY_CIRCLING_ATTACK = std::min(setpoint, BODY_CIRCLING_ATTACK); + } + } + + if ( abs(BODY_CIRCLING_AMOUNT) > 0.01 || abs(BODY_CIRCLING_ATTACK) > 0.01 ) + { + static ConsoleVariable cvar_moth_circle_yaw("/moth_circle_yaw", PI / 2); + if ( (bodypart - MOTH_BODY) / 3 < 4 ) // static bodies + { + // no yaw? + } + else + { + entity->yaw += BODY_CURRENT_CIRCLING; + entity->yaw += *cvar_moth_circle_yaw; + } + + real_t speed = 1.0; + if ( BODY_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + speed = 2.0; + } + + real_t& amount = abs(BODY_CIRCLING_AMOUNT) > abs(BODY_CIRCLING_ATTACK) + ? BODY_CIRCLING_AMOUNT : BODY_CIRCLING_ATTACK; + + if ( BODY_CIRCLING_AMOUNT < 0.0 ) + { + BODY_CURRENT_CIRCLING -= speed * 0.075; + } + else + { + BODY_CURRENT_CIRCLING += speed * 0.075; + } + BODY_CURRENT_CIRCLING = fmod(BODY_CURRENT_CIRCLING, 2 * PI); + while ( BODY_CURRENT_CIRCLING >= PI ) + { + BODY_CURRENT_CIRCLING -= 2 * PI; + } + while ( BODY_CURRENT_CIRCLING < -PI ) + { + BODY_CURRENT_CIRCLING += 2 * PI; + } + BODY_FLOAT_X += amount * cos(BODY_CURRENT_CIRCLING); + BODY_FLOAT_Y += amount * sin(BODY_CURRENT_CIRCLING); + } + + entity->x += BODY_FLOAT_X; + entity->y += BODY_FLOAT_Y; + entity->z += BODY_FLOAT_Z; + if ( BODY_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 && !body->flags[INVISIBLE] ) + { + Entity* fx = spawnMagicParticleCustom(entity, 576, 1.0, 10.0); + fx->z += 0.5; + fx->vel_z = 0.04; + } + + if ( !body->flags[INVISIBLE] ) + { + if ( fireSprite ) + { + Entity* fx = spawnMagicParticleCustom(entity, 13, 0.5, 1.0); + fx->vel_x = 0.25 * cos(entity->yaw + PI); + fx->vel_y = 0.25 * sin(entity->yaw + PI); + //fx->vel_z = -0.3; + fx->flags[SPRITE] = true; + fx->ditheringDisabled = true; + } + } + break; + } + case MOTH_LEFTWING + 3 * 0: + case MOTH_LEFTWING + 3 * 1: + case MOTH_LEFTWING + 3 * 2: + case MOTH_LEFTWING + 3 * 3: + case MOTH_LEFTWING + 3 * 4: + case MOTH_LEFTWING + 3 * 5: + if ( body ) + { + entity->yaw = body->yaw; + entity->flags[INVISIBLE] = body->flags[INVISIBLE]; + } + entity->x += limbs[MOTH_SMALL][8][0] * cos(entity->yaw + PI / 2); + entity->y += limbs[MOTH_SMALL][8][1] * sin(entity->yaw + PI / 2); + entity->z += limbs[MOTH_SMALL][8][2]; + entity->focalx = limbs[MOTH_SMALL][3][0]; + entity->focaly = limbs[MOTH_SMALL][3][1]; + entity->focalz = limbs[MOTH_SMALL][3][2]; + + // wings flap sync with body + { + real_t wingMin = -1.3; + real_t wingMax = 0.8; + real_t wingMid = wingMin + (wingMax - wingMin) / 2; + real_t speed = 1.0; + if ( BODY_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + speed = 2.0; + } + entity->fskill[1] = wingMid + ((wingMax - wingMin) / 2) * sin(speed * body->fskill[1] * limbs[MOTH_SMALL][11][2]); + } + + entity->pitch = entity->fskill[0]; + entity->roll = entity->fskill[1]; + leftWing = entity; + + if ( body ) + { + entity->x += BODY_FLOAT_X; + entity->y += BODY_FLOAT_Y; + entity->z += BODY_FLOAT_Z; + + entity->x += limbs[MOTH_SMALL][12][0] * sin(body->pitch) * cos(entity->yaw); + entity->y += limbs[MOTH_SMALL][12][1] * sin(body->pitch) * sin(entity->yaw); + entity->z += limbs[MOTH_SMALL][12][2] * abs(sin(body->pitch / 2)); + } + break; + case MOTH_RIGHTWING + 3 * 0: + case MOTH_RIGHTWING + 3 * 1: + case MOTH_RIGHTWING + 3 * 2: + case MOTH_RIGHTWING + 3 * 3: + case MOTH_RIGHTWING + 3 * 4: + case MOTH_RIGHTWING + 3 * 5: + if ( body ) + { + entity->yaw = body->yaw; + entity->flags[INVISIBLE] = body->flags[INVISIBLE]; + } + entity->x += limbs[MOTH_SMALL][9][0] * cos(entity->yaw + PI / 2); + entity->y += limbs[MOTH_SMALL][9][1] * sin(entity->yaw + PI / 2); + entity->z += limbs[MOTH_SMALL][9][2]; + entity->focalx = limbs[MOTH_SMALL][4][0]; + entity->focaly = limbs[MOTH_SMALL][4][1]; + entity->focalz = limbs[MOTH_SMALL][4][2]; + if ( leftWing ) + { + entity->fskill[0] = leftWing->fskill[0]; + entity->fskill[1] = -leftWing->fskill[1]; + } + entity->pitch = entity->fskill[0]; + entity->roll = entity->fskill[1]; + + if ( body ) + { + entity->x += BODY_FLOAT_X; + entity->y += BODY_FLOAT_Y; + entity->z += BODY_FLOAT_Z; + + entity->x += limbs[MOTH_SMALL][12][0] * sin(body->pitch) * cos(entity->yaw); + entity->y += limbs[MOTH_SMALL][12][1] * sin(body->pitch) * sin(entity->yaw); + entity->z += limbs[MOTH_SMALL][12][2] * abs(sin(body->pitch / 2)); + + } + break; + default: + break; + } + } + + if ( MONSTER_ATTACK > 0 ) + { + MONSTER_ATTACKTIME++; + } + else if ( MONSTER_ATTACK == 0 ) + { + MONSTER_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } + + for (int i = 0; i < my->bodyparts.size(); i += 3) + { + Entity* body = my->bodyparts.at(i); + if ( BODY_ATTACK > 0 ) + { + BODY_ATTACKTIME++; + } + else if ( BODY_ATTACK == 0 ) + { + BODY_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } + } +} + +void Entity::mothChooseWeapon(const Entity* target, double dist) +{ + Stat* myStats = getStats(); + if ( !myStats ) + { + return; + } + + if ( monsterSpecialState != 0 && monsterSpecialTimer != 0 ) + { + return; + } + + if ( monsterSpecialTimer == 0 + && (ticks % 10 == 0) + && (monsterAttack == 0 || ((monsterAttack == 1) && monsterAttackTime >= 25)) + && dist < 128 ) + { + Stat* targetStats = target->getStats(); + if ( !targetStats ) + { + return; + } + + // try to charm enemy. + int specialRoll = -1; + int bonusFromHP = 0; + specialRoll = local_rng.rand() % 40; + if ( myStats && myStats->getAttribute("fire_sprite") != "" ) + { + specialRoll = 0; + } + if ( myStats->HP <= myStats->MAXHP * 0.8 ) + { + bonusFromHP += 2; // +% chance if on low health + } + if ( myStats->HP <= myStats->MAXHP * 0.4 ) + { + bonusFromHP += 3; // +extra % chance if on lower health + } + + int requiredRoll = (2 + bonusFromHP); + + if ( dist < STRIKERANGE ) + { + requiredRoll += 5; + } + + + if ( specialRoll < requiredRoll ) + { + monsterSpecialState = MOTH_CAST; + } + } +} \ No newline at end of file diff --git a/src/monster_rat.cpp b/src/monster_rat.cpp index 275c6e89e..b50bdd07a 100644 --- a/src/monster_rat.cpp +++ b/src/monster_rat.cpp @@ -170,54 +170,32 @@ void ratAnimate(Entity* my, double dist) } } - static ConsoleVariable cvar_useFocalZ("/rat_anim_use_focal_z", false); - // attack cycle if (MONSTER_ATTACK) { const int frame = TICKS_PER_SECOND / 10; const bool algernon = my->sprite >= 1068; if (MONSTER_ATTACKTIME == frame * 0) { // frame 1 my->sprite = algernon ? 1070 : 1063; - if (*cvar_useFocalZ) { - my->focalz = -1.5; - } else { - my->z = 4.5; - } + my->z = 4.5; } if (MONSTER_ATTACKTIME == frame * 1) { // frame 2 my->sprite = algernon ? 1071 : 1064; - if (*cvar_useFocalZ) { - my->focalz = -2.5; - } else { - my->z = 3.5; - } + my->z = 3.5; } if (MONSTER_ATTACKTIME == frame * 2) { // frame 3 my->sprite = algernon ? 1072 : 1065; - if (*cvar_useFocalZ) { - my->focalz = -3.5; - } else { - my->z = 2.5; - } + my->z = 2.5; } if (MONSTER_ATTACKTIME == frame * 4) { // frame 4 my->sprite = algernon ? 1073 : 1066; - if (*cvar_useFocalZ) { - my->focalz = -4; - } else { - my->z = 2; - } + my->z = 2; const Sint32 temp = MONSTER_ATTACKTIME; my->attack(1, 0, nullptr); // munch MONSTER_ATTACKTIME = temp; } if (MONSTER_ATTACKTIME == frame * 6) { // frame 5 my->sprite = algernon ? 1074 : 1067; - if (*cvar_useFocalZ) { - my->focalz = -3; - } else { - my->z = 3; - } + my->z = 3; } if (MONSTER_ATTACKTIME == frame * 7) { // end if (algernon) { @@ -227,15 +205,15 @@ void ratAnimate(Entity* my, double dist) my->sprite = 131; my->z = 6; } - my->focalz = 0; MONSTER_ATTACK = 0; MONSTER_ATTACKTIME = 0; } else { ++MONSTER_ATTACKTIME; - my->new_z = my->z; } } + my->new_z = my->z; + my->creatureHandleLiftZ(); } void ratDie(Entity* my) diff --git a/src/monster_s.cpp b/src/monster_s.cpp new file mode 100644 index 000000000..3b9c8a82e --- /dev/null +++ b/src/monster_s.cpp @@ -0,0 +1,2061 @@ +/*------------------------------------------------------------------------------- + + BARONY + File: monster_goatman.cpp + Desc: implements all of the goatman monster's code + + Copyright 2013-2016 (c) Turning Wheel LLC, all rights reserved. + See LICENSE for details. + +-------------------------------------------------------------------------------*/ + +#include "main.hpp" +#include "game.hpp" +#include "stat.hpp" +#include "entity.hpp" +#include "items.hpp" +#include "monster.hpp" +#include "engine/audio/sound.hpp" +#include "net.hpp" +#include "collision.hpp" +#include "player.hpp" +#include "prng.hpp" +#include "scores.hpp" +#include "mod_tools.hpp" + +real_t getNormalHeightMonsterS(Entity& my) +{ + return -1.25; +} + +void initMonsterS(Entity* my, Stat* myStats) +{ + if ( !my ) + { + return; + } + node_t* node; + bool spawnedBoss = false; + + my->flags[BURNABLE] = true; + my->initMonster(1536); + my->z = getNormalHeightMonsterS(*my); + + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = 853; + MONSTER_SPOTVAR = 3; + MONSTER_IDLESND = 850; + MONSTER_IDLEVAR = 3; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( myStats->sex == FEMALE ) + { + my->sprite = 1537; + } + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + + // boss variants + //const bool boss = + // rng.rand() % 50 == 0 && + // !my->flags[USERFLAG2] && + // !myStats->MISC_FLAGS[STAT_FLAG_DISABLE_MINIBOSS]; + //if ( (boss || (*cvar_summonBosses && conductGameChallenges[CONDUCT_CHEATS_ENABLED])) && myStats->leader_uid == 0 ) + //{ + // myStats->setAttribute("special_npc", "gharbad"); + // strcpy(myStats->name, MonsterMata_t::getSpecialNPCName(*myStats).c_str()); + // my->sprite = MonsterMata_t::getSpecialNPCBaseModel(*myStats); + // myStats->sex = MALE; + // myStats->STR += 10; + // myStats->DEX += 2; + // myStats->MAXHP += 75; + // myStats->HP = myStats->MAXHP; + // myStats->OLDHP = myStats->MAXHP; + // myStats->CHR = -1; + // spawnedBoss = true; + // //TODO: Boss stats + + // //Spawn in potions. + // int end = rng.rand()%NUM_GOATMAN_BOSS_GHARBAD_POTIONS + 5; + // for ( int i = 0; i < end; ++i ) + // { + // switch ( rng.rand()%10 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // case 8: + // newItem(POTION_BOOZE, EXCELLENT, 0, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + // break; + // case 9: + // newItem(POTION_HEALING, EXCELLENT, 0, 1, rng.rand(), false, &myStats->inventory); + // break; + // default: + // printlog("Tried to spawn goatman boss \"Gharbad\" invalid potion."); + // break; + // } + // } + + // newItem(CRYSTAL_SHURIKEN, EXCELLENT, 1 + rng.rand()%1, rng.rand()%NUM_GOATMAN_BOSS_GHARBAD_THROWN_WEAPONS + 2, rng.rand(), true, &myStats->inventory); + //} + + // random effects + /*if ( rng.rand() % 8 == 0 ) + { + my->setEffect(EFF_ASLEEP, true, 1800 + rng.rand() % 1800, false); + }*/ + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + //bool isShaman = false; + //if ( rng.rand() % 2 && !spawnedBoss && !minion ) + //{ + // isShaman = true; + // if ( myStats->leader_uid == 0 && !my->flags[USERFLAG2] && rng.rand() % 2 == 0 ) + // { + // Entity* entity = summonMonster(GOATMAN, my->x, my->y); + // if ( entity ) + // { + // entity->parent = my->getUID(); + // if ( Stat* followerStats = entity->getStats() ) + // { + // followerStats->leader_uid = entity->parent; + // } + // entity->seedEntityRNG(rng.getU32()); + // } + // if ( rng.rand() % 5 == 0 ) + // { + // // summon second ally randomly. + // entity = summonMonster(GOATMAN, my->x, my->y); + // if ( entity ) + // { + // entity->parent = my->getUID(); + // if ( Stat* followerStats = entity->getStats() ) + // { + // followerStats->leader_uid = entity->parent; + // } + // entity->seedEntityRNG(rng.getU32()); + // } + // } + // } + //} + //else + //{ + // myStats->DEX += 1; // more speed for brawlers. + //} + + // generate the default inventory items for the monster, provided the editor sprite allowed enough default slots + //switch ( defaultItems ) + //{ + // case 6: + // case 5: + // case 4: + // case 3: + // case 2: + // case 1: + // if ( isShaman && rng.rand() % 10 == 0 ) + // { + // switch ( rng.rand() % 4 ) + // { + // case 0: + // newItem(SPELLBOOK_SLOW, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 1: + // newItem(SPELLBOOK_FIREBALL, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 2: + // newItem(SPELLBOOK_COLD, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // case 3: + // newItem(SPELLBOOK_FORCEBOLT, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); + // break; + // } + // } + // break; + // default: + // break; + //} + + + ////Give weapons. + //if ( !spawnedBoss ) + //{ + // if ( !isShaman && rng.rand() % 3 > 0 ) + // { + // newItem(STEEL_CHAKRAM, SERVICABLE, 0, rng.rand()%NUM_GOATMAN_THROWN_WEAPONS + 1, rng.rand(), false, &myStats->inventory); + // } + // int numpotions = rng.rand() % NUM_GOATMAN_POTIONS + 2; + // if ( rng.rand() % 3 == 0 ) + // { + // int numhealpotion = rng.rand() % 2 + 1; + // newItem(POTION_HEALING, static_cast(rng.rand() % 3 + DECREPIT), 0, numhealpotion, rng.rand(), false, &myStats->inventory); + // numpotions -= numhealpotion; + // } + // if ( rng.rand() % 4 > 0 ) + // { + // // undroppable + // newItem(POTION_BOOZE, static_cast(rng.rand() % 3 + DECREPIT), 0, numpotions, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, &myStats->inventory); + // } + //} + + //if ( isShaman ) + //{ + // //give shield + // if ( myStats->shield == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] == 1 ) + // { + // // give shield + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // myStats->shield = newItem(TOOL_CRYSTALSHARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 8: + // myStats->shield = newItem(MIRROR_SHIELD, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shield = newItem(TOOL_LANTERN, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give cloak + // if ( myStats->cloak == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_CLOAK] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // break; + // default: + // myStats->cloak = newItem(CLOAK, WORN, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give helmet + // if ( myStats->helmet == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_HELM] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // myStats->helmet = newItem(HAT_HOOD, WORN, -1 + rng.rand() % 3, 1, 0, false, nullptr); + // break; + // default: + // myStats->helmet = newItem(HAT_WIZARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give armor + // if ( myStats->breastplate == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // myStats->breastplate = newItem(WIZARD_DOUBLET, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 2: + // myStats->breastplate = newItem(LEATHER_BREASTPIECE, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // break; + // } + // } + // // give booties + // if ( myStats->shoes == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_BOOTS] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // myStats->shoes = newItem(IRON_BOOTS, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->shoes = newItem(CRYSTAL_BOOTS, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shoes = newItem(STEEL_BOOTS, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give weapon + // if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + // { + // switch ( rng.rand() % 12 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // myStats->weapon = newItem(MAGICSTAFF_COLD, static_cast(rng.rand() % 2 + SERVICABLE), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 5: + // case 6: + // case 7: + // case 8: + // myStats->weapon = newItem(MAGICSTAFF_FIRE, static_cast(rng.rand() % 2 + SERVICABLE), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 9: + // switch ( rng.rand() % 4 ) + // { + // case 0: + // myStats->weapon = newItem(SPELLBOOK_SLOW, static_cast(rng.rand() % 2 + DECREPIT), -1 + rng.rand() % 3, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, nullptr); + // break; + // case 1: + // myStats->weapon = newItem(SPELLBOOK_FIREBALL, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, nullptr); + // break; + // case 2: + // myStats->weapon = newItem(SPELLBOOK_COLD, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, nullptr); + // break; + // case 3: + // myStats->weapon = newItem(SPELLBOOK_FORCEBOLT, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, MONSTER_ITEM_UNDROPPABLE_APPEARANCE, false, nullptr); + // break; + // } + // break; + // default: + // myStats->weapon = newItem(MAGICSTAFF_SLOW, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + //} + //else + //{ + // ////give shield + // //if ( myStats->shield == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] == 1 ) + // //{ + // // switch ( rng.rand() % 20 ) + // // { + // // case 0: + // // case 1: + // // case 2: + // // case 3: + // // case 4: + // // case 5: + // // case 6: + // // case 7: + // // myStats->shield = newItem(TOOL_CRYSTALSHARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // case 8: + // // myStats->shield = newItem(MIRROR_SHIELD, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // default: + // // myStats->shield = newItem(TOOL_LANTERN, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // // break; + // // } + // //} + // // give cloak + // /*if ( myStats->cloak == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_CLOAK] == 1 ) + // { + // switch ( rng.rand() % 10 ) + // { + // case 0: + // case 1: + // break; + // default: + // myStats->cloak = newItem(CLOAK, WORN, -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // }*/ + // //// give helmet + // //if ( myStats->helmet == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_HELM] == 1 ) + // //{ + // // switch ( rng.rand() % 10 ) + // // { + // // case 0: + // // case 1: + // // myStats->helmet = newItem(HAT_HOOD, WORN, -1 + rng.rand() % 3, 1, 0, false, nullptr); + // // break; + // // default: + // // myStats->helmet = newItem(HAT_WIZARD, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, 0, false, nullptr); + // // break; + // // } + // //} + // // give armor + // if ( myStats->breastplate == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // myStats->breastplate = newItem(STEEL_BREASTPIECE, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 3: + // case 4: + // myStats->breastplate = newItem(LEATHER_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 5: + // case 6: + // myStats->breastplate = newItem(IRON_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->breastplate = newItem(CRYSTAL_BREASTPIECE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // break; + // } + // } + // // give booties + // if ( myStats->shoes == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_BOOTS] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // myStats->shoes = newItem(IRON_BOOTS, static_cast(rng.rand() % 3 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->shoes = newItem(CRYSTAL_BOOTS, static_cast(rng.rand() % 4 + DECREPIT), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->shoes = newItem(STEEL_BOOTS, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + // // give weapon + // if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + // { + // switch ( rng.rand() % 20 ) + // { + // case 0: + // case 1: + // case 2: + // case 3: + // case 4: + // case 5: + // case 6: + // case 7: + // myStats->weapon = newItem(STEEL_AXE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 18: + // myStats->weapon = newItem(CRYSTAL_BATTLEAXE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // case 19: + // myStats->weapon = newItem(CRYSTAL_MACE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // default: + // myStats->weapon = newItem(STEEL_MACE, static_cast(rng.rand() % 3 + WORN), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + // break; + // } + // } + //} + } + } + + // torso + const int torso_sprite = (my->sprite == 1536 || my->sprite == 1537) ? 1560 : + (my->sprite == 1538 || my->sprite == 1539) ? 1561 : 1562; + Entity* entity = newEntity(torso_sprite, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->scalex = 1.01; + entity->scaley = 1.01; + entity->scalez = 1.01; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[SALAMANDER][1][0]; // 0 + entity->focaly = limbs[SALAMANDER][1][1]; // 0 + entity->focalz = limbs[SALAMANDER][1][2]; // 0 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // right leg + entity = newEntity((my->sprite == 1536 || my->sprite == 1537) ? 1555 : + (my->sprite == 1538 || my->sprite == 1539) ? 1557 : 1559, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[SALAMANDER][2][0]; // 0 + entity->focaly = limbs[SALAMANDER][2][1]; // 0 + entity->focalz = limbs[SALAMANDER][2][2]; // 2 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // left leg + entity = newEntity((my->sprite == 1536 || my->sprite == 1537) ? 1554 : + (my->sprite == 1538 || my->sprite == 1539) ? 1556 : 1558, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[SALAMANDER][3][0]; // 0 + entity->focaly = limbs[SALAMANDER][3][1]; // 0 + entity->focalz = limbs[SALAMANDER][3][2]; // 2 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // right arm + entity = newEntity((my->sprite == 1536 || my->sprite == 1537) ? 1544 : + (my->sprite == 1538 || my->sprite == 1539) ? 1548 : 1552, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[SALAMANDER][4][0]; // 0 + entity->focaly = limbs[SALAMANDER][4][1]; // 0 + entity->focalz = limbs[SALAMANDER][4][2]; // 1.5 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // left arm + entity = newEntity((my->sprite == 1536 || my->sprite == 1537) ? 1542 : + (my->sprite == 1538 || my->sprite == 1539) ? 1546 : 1550, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[SALAMANDER][5][0]; // 0 + entity->focaly = limbs[SALAMANDER][5][1]; // 0 + entity->focalz = limbs[SALAMANDER][5][2]; // 1.5 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // world weapon + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[SALAMANDER][6][0]; // 1.5 + entity->focaly = limbs[SALAMANDER][6][1]; // 0 + entity->focalz = limbs[SALAMANDER][6][2]; // -.5 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + entity->pitch = .25; + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // shield + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[SALAMANDER][7][0]; // 2 + entity->focaly = limbs[SALAMANDER][7][1]; // 0 + entity->focalz = limbs[SALAMANDER][7][2]; // 0 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // cloak + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[SALAMANDER][8][0]; // 0 + entity->focaly = limbs[SALAMANDER][8][1]; // 0 + entity->focalz = limbs[SALAMANDER][8][2]; // 4 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // helmet + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->scalex = 1.01; + entity->scaley = 1.01; + entity->scalez = 1.01; + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[SALAMANDER][9][0]; // 0 + entity->focaly = limbs[SALAMANDER][9][1]; // 0 + entity->focalz = limbs[SALAMANDER][9][2]; // -2 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // mask + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->noColorChangeAllyLimb = 1.0; + entity->focalx = limbs[SALAMANDER][10][0]; // 0 + entity->focaly = limbs[SALAMANDER][10][1]; // 0 + entity->focalz = limbs[SALAMANDER][10][2]; // .25 + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // tail + entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 4; + entity->sizey = 4; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[SALAMANDER][11][0]; + entity->focaly = limbs[SALAMANDER][11][1]; + entity->focalz = limbs[SALAMANDER][11][2]; + entity->behavior = &actMonsterSLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + if ( multiplayer == CLIENT || MONSTER_INIT ) + { + return; + } +} + +void actMonsterSLimb(Entity* my) +{ + my->actMonsterLimb(true); +} + +void monsterSDie(Entity* my) +{ + Entity* gib = spawnGib(my); + gib->skill[5] = 1; // poof + gib->sprite = my->sprite; + serverSpawnGibForClient(gib); + for ( int c = 0; c < 8; c++ ) + { + Entity* gib = spawnGib(my); + serverSpawnGibForClient(gib); + } + + my->spawnBlood(); + + playSoundEntity(my, 856 + local_rng.rand() % 4, 128); + + my->removeMonsterDeathNodes(); + + list_RemoveNode(my->mynode); + return; +} + +#define MONSTER_SWALKSPEED .13 + +void monsterSMoveBodyparts(Entity* my, Stat* myStats, double dist) +{ + node_t* node; + Entity* entity = nullptr, * entity2 = nullptr; + Entity* additionalLimb = nullptr; + Entity* rightbody = nullptr; + Entity* weaponarm = nullptr; + int bodypart; + bool wearingring = false; + + my->focalx = limbs[SALAMANDER][0][0]; + my->focaly = limbs[SALAMANDER][0][1]; + my->focalz = limbs[SALAMANDER][0][2]; + + /*if ( keystatus[SDLK_g] ) + { + keystatus[SDLK_g] = 0; + my->sprite += 1; + if ( my->sprite > 1541 ) + { + my->sprite = 1536; + } + }*/ + + bool debugModel = monsterDebugModels(my, &dist); + + // set invisibility //TODO: isInvisible()? + if ( multiplayer != CLIENT ) + { + if ( myStats->ring != nullptr ) + if ( myStats->ring->type == RING_INVISIBILITY ) + { + wearingring = true; + } + if ( myStats->cloak != nullptr ) + if ( myStats->cloak->type == CLOAK_INVISIBILITY ) + { + wearingring = true; + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) + { + my->flags[INVISIBLE] = true; + my->flags[BLOCKSIGHT] = false; + bodypart = 0; + for ( node = my->children.first; node != nullptr; node = node->next ) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + bodypart++; + continue; + } + if ( bodypart >= 7 ) + { + break; + } + entity = (Entity*)node->element; + if ( !entity->flags[INVISIBLE] ) + { + entity->flags[INVISIBLE] = true; + serverUpdateEntityBodypart(my, bodypart); + } + bodypart++; + } + } + else + { + my->flags[INVISIBLE] = false; + my->flags[BLOCKSIGHT] = true; + bodypart = 0; + for ( node = my->children.first; node != nullptr; node = node->next ) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + bodypart++; + continue; + } + if ( bodypart >= 7 ) + { + break; + } + entity = (Entity*)node->element; + if ( entity->flags[INVISIBLE] ) + { + entity->flags[INVISIBLE] = false; + serverUpdateEntityBodypart(my, bodypart); + serverUpdateEntityFlag(my, INVISIBLE); + } + bodypart++; + } + } + + // sleeping + if ( myStats->getEffectActive(EFF_ASLEEP) ) + { + my->z = 3.0; + my->pitch = PI / 4; + } + else + { + my->z = getNormalHeightMonsterS(*my); + + if ( my->monsterAttack == 0 ) + { + if ( debugModel ) + { + my->pitch = my->fskill[0]; + if ( my->fskill[1] > 0.0 ) + { + my->fskill[1] = std::max(0.0, my->fskill[1] - 0.05); + my->z += -3.0 * sqrt(sin(PI * my->fskill[1])); + } + } + else + { + my->pitch = 0; + } + } + } + my->creatureHandleLiftZ(); + } + + Entity* shieldarm = nullptr; + Entity* helmet = nullptr; + + //Move bodyparts + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, bodypart++ ) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + // post-swing head animation. client doesn't need to adjust the entity pitch, server will handle. + if ( my->monsterAttack != MONSTER_POSE_RANGED_WINDUP3 && bodypart == 1 && multiplayer != CLIENT ) + { + limbAnimateToLimit(my, ANIMATE_PITCH, 0.1, 0, false, 0.0); + } + continue; + } + entity = (Entity*)node->element; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + if ( MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 && bodypart == LIMB_HUMANOID_RIGHTARM ) + { + // don't let the creatures's yaw move the casting arm + } + else + { + entity->yaw = my->yaw; + } + + if ( bodypart == LIMB_HUMANOID_RIGHTLEG || bodypart == LIMB_HUMANOID_LEFTARM ) + { + my->humanoidAnimateWalk(entity, node, bodypart, MONSTER_SWALKSPEED, dist, 0.4); + } + else if ( bodypart == LIMB_HUMANOID_LEFTLEG || bodypart == LIMB_HUMANOID_RIGHTARM || bodypart == LIMB_HUMANOID_CLOAK ) + { + // left leg, right arm, cloak. + if ( bodypart == LIMB_HUMANOID_RIGHTARM ) + { + weaponarm = entity; + if ( MONSTER_ATTACK > 0 ) + { + if ( my->monsterAttack == MONSTER_POSE_RANGED_WINDUP3 ) + { + Entity* rightbody = nullptr; + // set rightbody to left leg. + node_t* rightbodyNode = list_Node(&my->children, LIMB_HUMANOID_LEFTLEG); + if ( rightbodyNode ) + { + rightbody = (Entity*)rightbodyNode->element; + } + else + { + return; + } + + if ( my->monsterAttackTime == 0 ) + { + // init rotations + weaponarm->pitch = 0; + my->monsterArmbended = 0; + my->monsterWeaponYaw = 0; + weaponarm->roll = 0; + weaponarm->skill[1] = 0; + if ( multiplayer != CLIENT ) + { + myStats->setEffectActive(EFF_PARALYZED, 1); + myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 40; + } + } + if ( multiplayer != CLIENT ) + { + // move the head and weapon yaw + limbAnimateToLimit(my, ANIMATE_PITCH, -0.1, 11 * PI / 6, true, 0.05); + limbAnimateToLimit(my, ANIMATE_WEAPON_YAW, -0.25, 14 * PI / 8, false, 0.0); + } + limbAnimateToLimit(weaponarm, ANIMATE_PITCH, -0.25, 7 * PI / 4, true, 0.0); + //limbAnimateToLimit(weaponarm, ANIMATE_ROLL, -0.25, 7 * PI / 4, false, 0.0); + + if ( my->monsterAttackTime >= 4 * ANIMATE_DURATION_WINDUP / (monsterGlobalAnimationMultiplier / 10.0) ) + { + if ( multiplayer != CLIENT ) + { + my->attack(MONSTER_POSE_MELEE_WINDUP1, 0, nullptr); + } + } + } + else + { + my->handleWeaponArmAttack(entity); + } + } + } + else if ( bodypart == LIMB_HUMANOID_CLOAK ) + { + entity->pitch = entity->fskill[0]; + } + + my->humanoidAnimateWalk(entity, node, bodypart, MONSTER_SWALKSPEED, dist, 0.4); + + if ( bodypart == LIMB_HUMANOID_CLOAK ) + { + entity->fskill[0] = entity->pitch; + entity->roll = my->roll - fabs(entity->pitch) / 2; + entity->pitch = 0; + } + } + switch ( bodypart ) + { + // torso + case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[SALAMANDER][1][0]; + entity->focaly = limbs[SALAMANDER][1][1]; + entity->focalz = limbs[SALAMANDER][1][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) + { + entity->sprite = + (my->sprite == 1536 || my->sprite == 1537) ? 1560 : + (my->sprite == 1538 || my->sprite == 1539) ? 1561 : 1562; + } + else + { + entity->sprite = itemModel(myStats->breastplate, false, my); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, SALAMANDER, LIMB_HUMANOID_TORSO); + break; + // right leg + case LIMB_HUMANOID_RIGHTLEG: + entity->focalx = limbs[SALAMANDER][2][0]; + entity->focaly = limbs[SALAMANDER][2][1]; + entity->focalz = limbs[SALAMANDER][2][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->shoes == nullptr ) + { + entity->sprite = + (my->sprite == 1536 || my->sprite == 1537) ? 1555 : + (my->sprite == 1538 || my->sprite == 1539) ? 1557 : 1559; + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_RIGHT_OFFSET); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, SALAMANDER, LIMB_HUMANOID_RIGHTLEG); + break; + // left leg + case LIMB_HUMANOID_LEFTLEG: + entity->focalx = limbs[SALAMANDER][3][0]; + entity->focaly = limbs[SALAMANDER][3][1]; + entity->focalz = limbs[SALAMANDER][3][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->shoes == nullptr ) + { + entity->sprite = (my->sprite == 1536 || my->sprite == 1537) ? 1554 : + (my->sprite == 1538 || my->sprite == 1539) ? 1556 : 1558; + } + else + { + my->setBootSprite(entity, SPRITE_BOOT_LEFT_OFFSET); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + my->setHumanoidLimbOffset(entity, SALAMANDER, LIMB_HUMANOID_LEFTLEG); + break; + // right arm + case LIMB_HUMANOID_RIGHTARM: + { + if ( multiplayer != CLIENT ) + { + if ( myStats->gloves == nullptr ) + { + entity->sprite = (my->sprite == 1536 || my->sprite == 1537) ? 1544 : + (my->sprite == 1538 || my->sprite == 1539) ? 1548 : 1552; + } + else + { + if ( setGloveSprite(myStats, entity, SPRITE_GLOVE_RIGHT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( entity->skill[7] == 0 ) + { + if ( entity->sprite == 1544 || entity->sprite == 1548 || entity->sprite == 1552 ) + { + // these are the default arms. + // chances are they may be wrong if sent by the server, + } + else + { + // otherwise we're being sent gloves armor etc so it's probably right. + entity->skill[7] = entity->sprite; + } + } + if ( entity->skill[7] == 0 ) + { + // we set this ourselves until proper initialisation. + entity->sprite = (my->sprite == 1536 || my->sprite == 1537) ? 1544 : + (my->sprite == 1538 || my->sprite == 1539) ? 1548 : 1552; + } + else + { + entity->sprite = entity->skill[7]; + } + } + + node_t* weaponNode = list_Node(&my->children, 7); + if ( weaponNode ) + { + Entity* weapon = (Entity*)weaponNode->element; + if ( MONSTER_ARMBENDED || (weapon->flags[INVISIBLE] && my->monsterState == MONSTER_STATE_WAIT) ) + { + // if weapon invisible and I'm not attacking, relax arm. + entity->focalx = limbs[SALAMANDER][4][0]; // 0 + entity->focaly = limbs[SALAMANDER][4][1]; + entity->focalz = limbs[SALAMANDER][4][2]; // 2 + + /*if ( entity->sprite == 1523 ) + { + entity->focaly += 0.25; + }*/ + } + else + { + // else flex arm. + entity->focalx = limbs[SALAMANDER][4][0] + 0.75; + entity->focaly = limbs[SALAMANDER][4][1] + 0.25; + entity->focalz = limbs[SALAMANDER][4][2] - 0.75; + if ( entity->sprite == 1544 || entity->sprite == 1548 || entity->sprite == 1552 ) + { + entity->sprite += 1; + } + else + { + entity->sprite += 2; + } + } + } + my->setHumanoidLimbOffset(entity, SALAMANDER, LIMB_HUMANOID_RIGHTARM); + entity->yaw += MONSTER_WEAPONYAW; + break; + // left arm + } + case LIMB_HUMANOID_LEFTARM: + { + if ( multiplayer != CLIENT ) + { + if ( myStats->gloves == nullptr ) + { + entity->sprite = (my->sprite == 1536 || my->sprite == 1537) ? 1542 : + (my->sprite == 1538 || my->sprite == 1539) ? 1546 : 1550; + } + else + { + if ( setGloveSprite(myStats, entity, SPRITE_GLOVE_LEFT_OFFSET) != 0 ) + { + // successfully set sprite for the human model + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( multiplayer == CLIENT ) + { + if ( entity->skill[7] == 0 ) + { + if ( entity->sprite == 1542 || entity->sprite == 1546 || entity->sprite == 1550 ) + { + // these are the default arms. + // chances are they may be wrong if sent by the server, + } + else + { + // otherwise we're being sent gloves armor etc so it's probably right. + entity->skill[7] = entity->sprite; + } + } + if ( entity->skill[7] == 0 ) + { + // we set this ourselves until proper initialisation. + entity->sprite = (my->sprite == 1536 || my->sprite == 1537) ? 1542 : + (my->sprite == 1538 || my->sprite == 1539) ? 1546 : 1550; + } + else + { + entity->sprite = entity->skill[7]; + } + } + + shieldarm = entity; + node_t* shieldNode = list_Node(&my->children, 8); + if ( shieldNode ) + { + Entity* shield = (Entity*)shieldNode->element; + if ( shield->flags[INVISIBLE] && my->monsterState == MONSTER_STATE_WAIT ) + { + entity->focalx = limbs[SALAMANDER][5][0]; // 0 + entity->focaly = limbs[SALAMANDER][5][1]; + entity->focalz = limbs[SALAMANDER][5][2]; // 2 + + /*if ( entity->sprite == 1521 ) + { + entity->focaly -= 0.25; + }*/ + } + else + { + entity->focalx = limbs[SALAMANDER][5][0] + 0.75; + entity->focaly = limbs[SALAMANDER][5][1] - 0.25; + entity->focalz = limbs[SALAMANDER][5][2] - 0.75; + if ( entity->sprite == 1542 || entity->sprite == 1546 || entity->sprite == 1550 ) + { + entity->sprite += 1; + } + else + { + entity->sprite += 2; + } + } + } + my->setHumanoidLimbOffset(entity, SALAMANDER, LIMB_HUMANOID_LEFTARM); + if ( my->monsterDefend && my->monsterAttack == 0 ) + { + MONSTER_SHIELDYAW = PI / 5; + } + else + { + MONSTER_SHIELDYAW = 0; + } + entity->yaw += MONSTER_SHIELDYAW; + break; + } + // weapon + case LIMB_HUMANOID_WEAPON: + if ( multiplayer != CLIENT ) + { + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->sprite = itemModel(myStats->weapon); + if ( itemCategory(myStats->weapon) == SPELLBOOK ) + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + if ( weaponarm != nullptr ) + { + my->handleHumanoidWeaponLimb(entity, weaponarm); + } + break; + // shield + case LIMB_HUMANOID_SHIELD: + if ( multiplayer != CLIENT ) + { + if ( myStats->shield == nullptr ) + { + entity->flags[INVISIBLE] = true; + entity->sprite = 0; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(myStats->shield); + if ( itemTypeIsQuiver(myStats->shield->type) ) + { + entity->handleQuiverThirdPersonModel(*myStats); + } + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + my->handleHumanoidShieldLimb(entity, shieldarm); + break; + // cloak + case LIMB_HUMANOID_CLOAK: + entity->focalx = limbs[SALAMANDER][8][0]; + entity->focaly = limbs[SALAMANDER][8][1]; + entity->focalz = limbs[SALAMANDER][8][2]; + if ( multiplayer != CLIENT ) + { + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + entity->sprite = itemModel(myStats->cloak); + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + entity->x -= cos(my->yaw); + entity->y -= sin(my->yaw); + entity->yaw += PI / 2; + break; + // helm + case LIMB_HUMANOID_HELMET: + helmet = entity; + entity->focalx = limbs[SALAMANDER][9][0]; // 0 + entity->focaly = limbs[SALAMANDER][9][1]; // 0 + entity->focalz = limbs[SALAMANDER][9][2]; // -2 + entity->pitch = my->pitch; + entity->roll = 0; + if ( multiplayer != CLIENT ) + { + entity->sprite = itemModel(myStats->helmet); + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + my->setHelmetLimbOffset(entity); + break; + // mask + case LIMB_HUMANOID_MASK: + entity->focalx = limbs[SALAMANDER][10][0]; // 0 + entity->focaly = limbs[SALAMANDER][10][1]; // 0 + entity->focalz = limbs[SALAMANDER][10][2]; // .25 + entity->pitch = my->pitch; + entity->roll = PI / 2; + if ( multiplayer != CLIENT ) + { + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( myStats->mask != nullptr ) + { + if ( myStats->mask->type == TOOL_GLASSES ) + { + entity->sprite = 165; // GlassesWorn.vox + } + else if ( myStats->mask->type == MONOCLE ) + { + entity->sprite = 1196; // monocleWorn.vox + } + else + { + entity->sprite = itemModel(myStats->mask); + } + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + if ( EquipmentModelOffsets.modelOffsetExists(SALAMANDER, entity->sprite, my->sprite) ) + { + my->setHelmetLimbOffset(entity); + my->setHelmetLimbOffsetWithMask(helmet, entity); + } + else + { + entity->focalx = limbs[SALAMANDER][10][0] + .35; // .35 + entity->focaly = limbs[SALAMANDER][10][1] - 2; // -2 + entity->focalz = limbs[SALAMANDER][10][2]; // .25 + } + break; + // tail + case 12: + { + entity->focalx = limbs[SALAMANDER][11][0]; + entity->focaly = limbs[SALAMANDER][11][1]; + entity->focalz = limbs[SALAMANDER][11][2]; + entity->x += limbs[SALAMANDER][12][0] * cos(my->yaw + PI / 2) + limbs[SALAMANDER][12][1] * cos(my->yaw); + entity->y += limbs[SALAMANDER][12][0] * sin(my->yaw + PI / 2) + limbs[SALAMANDER][12][1] * sin(my->yaw); + entity->z += limbs[SALAMANDER][12][2]; + entity->pitch = my->pitch + 0.15; + if ( multiplayer != CLIENT ) + { + entity->sprite = 1563; + switch ( my->sprite ) + { + case 1536: + entity->sprite = 1563; + break; + case 1537: + entity->sprite = 1564; + break; + case 1538: + entity->sprite = 1565; + break; + case 1539: + entity->sprite = 1566; + break; + case 1540: + entity->sprite = 1567; + break; + case 1541: + entity->sprite = 1568; + break; + default: + break; + } + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? + { + entity->flags[INVISIBLE] = true; + } + else + { + entity->flags[INVISIBLE] = false; + } + if ( multiplayer == SERVER ) + { + // update sprites for clients + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->skill[11] != entity->flags[INVISIBLE] ) + { + entity->skill[11] = entity->flags[INVISIBLE]; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + else + { + if ( entity->sprite <= 0 ) + { + entity->flags[INVISIBLE] = true; + } + } + + if ( entity->sprite == 1564 + || entity->sprite == 1566 + || entity->sprite == 1568 ) + { + entity->focalx += 0.5; + entity->focalz -= 0.25; + } + + bool moving = false; + if ( dist > 0.1 ) + { + moving = true; + } + if ( entity->skill[0] == 0 ) + { + if ( moving ) + { + entity->fskill[0] += std::min(dist * MONSTER_SWALKSPEED, 2.f * MONSTER_SWALKSPEED); // move proportional to move speed + } + else if ( my->monsterAttack != 0 ) + { + entity->fskill[0] += MONSTER_SWALKSPEED; // move fixed speed when attacking if stationary + } + else + { + entity->fskill[0] += 0.01; // otherwise move slow idle + } + + if ( entity->fskill[0] > PI / 3 || ((!moving || my->monsterAttack != 0) && entity->fskill[0] > PI / 5) ) + { + // switch direction if angle too great, angle is shorter if attacking or stationary + entity->skill[0] = 1; + } + } + else // reverse of the above + { + if ( moving ) + { + entity->fskill[0] -= std::min(dist * MONSTER_SWALKSPEED, 2.f * MONSTER_SWALKSPEED); + } + else if ( my->monsterAttack != 0 ) + { + entity->fskill[0] -= MONSTER_SWALKSPEED; + } + else + { + entity->fskill[0] -= 0.007; + } + + if ( entity->fskill[0] < -0.0 ) + { + entity->skill[0] = 0; + entity->skill[1] = entity->skill[1] != 0 ? 0 : 1; + } + } + //entity->yaw += -entity->fskill[0]; + real_t dir = entity->skill[1] == 0 ? 1 : -1; + entity->pitch += 0.5 * sin(entity->fskill[0]); + entity->roll = dir * -0.25 * sin(entity->fskill[0]); + } + break; + } + } + // rotate shield a bit + node_t* shieldNode = list_Node(&my->children, 8); + if ( shieldNode ) + { + Entity* shieldEntity = (Entity*)shieldNode->element; + if ( shieldEntity->sprite != items[TOOL_TORCH].index && shieldEntity->sprite != items[TOOL_LANTERN].index && shieldEntity->sprite != items[TOOL_CRYSTALSHARD].index ) + { + shieldEntity->yaw -= PI / 6; + } + } + if ( MONSTER_ATTACK > 0 && MONSTER_ATTACK <= MONSTER_POSE_MAGIC_CAST3 ) + { + MONSTER_ATTACKTIME++; + } + else if ( MONSTER_ATTACK == 0 ) + { + MONSTER_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } +} + +//void Entity::goatmanChooseWeapon(const Entity* target, double dist) +//{ +// if ( monsterSpecialState != 0 ) +// { +// //Holding a weapon assigned from the special attack. Don't switch weapons. +// //messagePlayer() +// return; +// } +// +// //TODO: I don't like this function getting called every frame. Find a better place to put it. +// //Although if I do that, can't do this dirty little hack for the goatman's special... +// +// //TODO: If applying attack animations that will involve holding a potion for several frames while this code has a chance to run, do a check here to cancel the function if holding a potion. +// +// Stat *myStats = getStats(); +// if ( !myStats ) +// { +// return; +// } +// +// if ( myStats->weapon && (itemCategory(myStats->weapon) == SPELLBOOK) ) +// { +// return; +// } +// +// int specialRoll = -1; +// bool usePotionSpecial = false; +// +// /* +// * For the goatman's special: +// * * If specialRoll == 0, want to use a booze or healing potion (prioritize healing potion if damaged enough). +// * * If no have potion, try to use THROWN in melee. +// * * If in melee, if potion is not a healing potion, check if have any THROWN and then 50% chance to use those instead. +// */ +// +// node_t* hasPotion = nullptr; +// bool isHealingPotion = false; +// +// if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 ) +// { +// //messagePlayer(clientnum, "Cooldown done!"); +// specialRoll = local_rng.rand()%10; +// +// if ( specialRoll == 0 ) +// { +// if ( myStats->HP <= myStats->MAXHP / 3 * 2 ) +// { +// //Try to get a health potion. +// hasPotion = itemNodeInInventory(myStats, POTION_EXTRAHEALING, static_cast(-1)); +// if ( !hasPotion ) +// { +// hasPotion = itemNodeInInventory(myStats, POTION_HEALING, static_cast(-1)); +// if ( hasPotion ) +// { +// //Equip and chuck it now. +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, hasPotion, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap healing potion into hand!"); +// //Don't return, want to try equipping either a potion of booze, or one of the other weapon routes (e.h. a THROWN special if in melee or just an axe if worst comes to worst). +// } +// else +// { +// monsterSpecialState = GOATMAN_POTION; +// //monsterHitTime = 2 * HITRATE; +// return; +// } +// } +// } +// else +// { +// //Equip and chuck it now. +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, hasPotion, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap healing potion into hand!"); +// //Don't return, want to try equipping either a potion of booze, or one of the other weapon routes (e.h. a THROWN special if in melee or just an axe if worst comes to worst). +// } +// else +// { +// monsterSpecialState = GOATMAN_POTION; +// //monsterHitTime = 2 * HITRATE; +// return; +// } +// } +// } +// +// if ( !hasPotion ) +// { +// //Couldn't find a healing potion? Try for a potion of booze. +// hasPotion = itemNodeInInventory(myStats, POTION_BOOZE, static_cast(-1)); +// if ( hasPotion ) +// { +// //Equip and chuck it now. +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, hasPotion, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap healing potion into hand!"); +// //Don't return, want to try equipping either a potion of booze, or one of the other weapon routes (e.h. a THROWN special if in melee or just an axe if worst comes to worst). +// } +// else +// { +// monsterSpecialState = GOATMAN_POTION; +// //monsterHitTime = 2 * HITRATE; +// return; +// } +// } +// } +// } +// } +// +// bool inMeleeRange = monsterInMeleeRange(target, dist); +// +// if ( inMeleeRange ) +// { +// if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 && specialRoll == 0 ) +// { +// bool tryChakram = true; +// if ( hasPotion && local_rng.rand()%10 ) +// { +// tryChakram = false; +// } +// +// if ( tryChakram ) +// { +// //Grab a chakram instead. +// node_t* thrownNode = itemNodeInInventory(myStats, -1, THROWN); +// if ( thrownNode ) +// { +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, thrownNode, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap THROWN into hand! Cursed? (%d)", myStats->weapon->beatitude); +// //Don't return, make sure holding a melee weapon at least. +// } +// else +// { +// monsterSpecialState = GOATMAN_THROW; +// return; +// } +// } +// } +// } +// +// //Switch to a melee weapon if not already wielding one. Unless monster special state is overriding the AI. +// if ( !myStats->weapon || !isMeleeWeapon(*myStats->weapon) ) +// { +// node_t* weaponNode = getMeleeWeaponItemNodeInInventory(myStats); +// if ( !weaponNode ) +// { +// if ( myStats->weapon && myStats->weapon->type == MAGICSTAFF_SLOW ) +// { +// monsterUnequipSlotFromCategory(myStats, &myStats->weapon, MAGICSTAFF); +// } +// return; //Resort to fists. +// } +// +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, false); +// if ( !swapped ) +// { +// //printlog("Error in Entity::goatmanChooseWeapon(): failed to swap melee weapon into hand! Cursed? (%d)", myStats->weapon->beatitude); +// //Don't return so that monsters will at least equip ranged weapons in melee range if they don't have anything else. +// } +// else +// { +// return; +// } +// } +// else +// { +// return; +// } +// } +// +// //if ( hasPotion ) +// //{ +// // //Try to equip the potion first. If fails, then equip normal ranged. +// // bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, hasPotion, false, false); +// // if ( !swapped ) +// // { +// // printlog("Error in Entity::goatmanChooseWeapon(): failed to swap non-healing potion into hand! (non-melee block) Cursed? (%d)", myStats->weapon->beatitude); +// // } +// // else +// // { +// // monsterSpecialState = GOATMAN_POTION; +// // return; +// // } +// //} +// +// //Switch to a thrown weapon or a ranged weapon. Potions are reserved as a special attack. +// if ( !myStats->weapon || isMeleeWeapon(*myStats->weapon) ) +// { +// //First search the inventory for a THROWN weapon. +// node_t *weaponNode = nullptr; +// if ( monsterSpecialTimer == 0 && (ticks % 10 == 0) && monsterAttack == 0 && local_rng.rand() % 10 == 0 ) +// { +// weaponNode = itemNodeInInventory(myStats, -1, THROWN); +// if ( weaponNode ) +// { +// if ( swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, false) ) +// { +// monsterSpecialState = GOATMAN_THROW; +// return; +// } +// } +// } +// if ( !weaponNode ) +// { +// //If couldn't find any, search the inventory for a ranged weapon. +// weaponNode = getRangedWeaponItemNodeInInventory(myStats, true); +// } +// +// bool swapped = swapMonsterWeaponWithInventoryItem(this, myStats, weaponNode, false, false); +// return; +// } +// +// return; +//} +// +//bool Entity::goatmanCanWieldItem(const Item& item) const +//{ +// Stat* myStats = getStats(); +// if ( !myStats ) +// { +// return false; +// } +// +// if ( monsterAllyIndex >= 0 && (monsterAllyClass != ALLY_CLASS_MIXED || item.interactNPCUid == getUID()) ) +// { +// return monsterAllyEquipmentInClass(item); +// } +// +// switch ( itemCategory(&item) ) +// { +// case WEAPON: +// return true; +// case POTION: +// switch ( item.type ) +// { +// case POTION_BOOZE: +// return true; +// case POTION_HEALING: +// return true; +// default: +// return false; +// } +// break; +// case TOOL: +// if ( itemTypeIsQuiver(item.type) ) +// { +// return true; +// } +// break; +// case THROWN: +// return true; +// case ARMOR: +// { //Little baby compiler stop whining, wah wah. +// int equipType = checkEquipType(&item); +// if ( equipType == TYPE_HAT || equipType == TYPE_HELM ) +// { +// return false; //No can wear hats, because horns. +// } +// return true; //Can wear all other armor. +// } +// default: +// return false; +// } +// +// return false; +//} +// + + + + diff --git a/src/monster_scarab.cpp b/src/monster_scarab.cpp index 059cb5a36..c32e3cfc3 100644 --- a/src/monster_scarab.cpp +++ b/src/monster_scarab.cpp @@ -54,6 +54,10 @@ void initScarab(Entity* my, Stat* myStats) myStats->DEX -= 4; myStats->LVL = 10; } + else if ( currentlevel >= 26 ) + { + myStats->setAttribute("SCARAB_GREATER_CURSE", "1"); + } if ( !myStats->leader_uid ) { myStats->leader_uid = 0; @@ -244,7 +248,7 @@ void scarabAnimate(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -575,6 +579,7 @@ void scarabAnimate(Entity* my, Stat* myStats, double dist) } } my->new_z = my->z = 6.0 - sin(scarabFly) * 6.0; + my->creatureHandleLiftZ(); } } diff --git a/src/monster_scorpion.cpp b/src/monster_scorpion.cpp index 012c95f24..976877f14 100644 --- a/src/monster_scorpion.cpp +++ b/src/monster_scorpion.cpp @@ -204,7 +204,7 @@ void scorpionAnimate(Entity* my, double dist) if ( multiplayer != CLIENT ) { Stat* myStats = my->getStats(); - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -247,6 +247,9 @@ void scorpionAnimate(Entity* my, double dist) bodypart++; } } + + my->z = 6.0; + my->creatureHandleLiftZ(); } bool skrabblag = false; diff --git a/src/monster_sentrybot.cpp b/src/monster_sentrybot.cpp index 68f1a80c3..8fb0bdd9c 100644 --- a/src/monster_sentrybot.cpp +++ b/src/monster_sentrybot.cpp @@ -298,7 +298,7 @@ void initGyroBot(Entity* my, Stat* myStats) // apply random stat increases if set in stat_shared.cpp or editor setRandomMonsterStats(myStats, rng); - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; // generate 6 items max, less if there are any forced items from boss variants @@ -542,6 +542,7 @@ void sentryBotAnimate(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { my->z = limbs[race][11][2]; + my->creatureHandleLiftZ(); } if ( ticks % (3 * TICKS_PER_SECOND) == 0 && local_rng.rand() % 5 > 0 ) @@ -1081,9 +1082,11 @@ void gyroBotAnimate(Entity* my, Stat* myStats, double dist) { ++foundBadSound; } - if ( ent->entityShowOnMap < detectDuration ) + if ( ent->getEntityShowOnMapDuration() == 0 + || (ent->getEntityShowOnMapSource() == Entity::SHOW_MAP_GYRO + && ent->getEntityShowOnMapDuration() < detectDuration) ) { - ent->entityShowOnMap = detectDuration; + ent->setEntityShowOnMap(Entity::SHOW_MAP_GYRO, detectDuration); } } } @@ -1102,9 +1105,11 @@ void gyroBotAnimate(Entity* my, Stat* myStats, double dist) { foundBadSound = 3; } - if ( ent->entityShowOnMap < detectDuration ) + if ( ent->getEntityShowOnMapDuration() == 0 + || (ent->getEntityShowOnMapSource() == Entity::SHOW_MAP_GYRO + && ent->getEntityShowOnMapDuration() < detectDuration) ) { - ent->entityShowOnMap = detectDuration; + ent->setEntityShowOnMap(Entity::SHOW_MAP_GYRO, detectDuration); } } } @@ -1119,9 +1124,11 @@ void gyroBotAnimate(Entity* my, Stat* myStats, double dist) { foundGoodSound = 5; } - if ( ent->entityShowOnMap < detectDuration ) + if ( ent->getEntityShowOnMapDuration() == 0 + || (ent->getEntityShowOnMapSource() == Entity::SHOW_MAP_GYRO + && ent->getEntityShowOnMapDuration() < detectDuration) ) { - ent->entityShowOnMap = detectDuration; + ent->setEntityShowOnMap(Entity::SHOW_MAP_GYRO, detectDuration); } } } @@ -1147,9 +1154,11 @@ void gyroBotAnimate(Entity* my, Stat* myStats, double dist) { ++foundGoodSound; } - if ( ent->entityShowOnMap < detectDuration ) + if ( ent->getEntityShowOnMapDuration() == 0 + || (ent->getEntityShowOnMapSource() == Entity::SHOW_MAP_GYRO + && ent->getEntityShowOnMapDuration() < detectDuration) ) { - ent->entityShowOnMap = detectDuration; + ent->setEntityShowOnMap(Entity::SHOW_MAP_GYRO, detectDuration); } } else if ( my->monsterAllyPickupItems == ALLY_GYRO_DETECT_ITEMS_MAGIC @@ -1159,21 +1168,26 @@ void gyroBotAnimate(Entity* my, Stat* myStats, double dist) { ++foundGoodSound; } - if ( ent->entityShowOnMap < detectDuration ) + if ( ent->getEntityShowOnMapDuration() == 0 + || (ent->getEntityShowOnMapSource() == Entity::SHOW_MAP_GYRO + && ent->getEntityShowOnMapDuration() < detectDuration) ) { - ent->entityShowOnMap = detectDuration; + ent->setEntityShowOnMap(Entity::SHOW_MAP_GYRO, detectDuration); } } else if ( my->monsterAllyPickupItems == ALLY_GYRO_DETECT_ITEMS_VALUABLE - && items[itemOnGround->type].value >= 400 ) + && (itemOnGround->getGoldValue() >= 400 + || (itemOnGround->type >= KEY_STONE && itemOnGround->type <= KEY_MACHINE)) ) { if ( gyroBotFoundNewEntity(*ent) ) { foundGoodSound = 5; } - if ( ent->entityShowOnMap < detectDuration ) + if ( ent->getEntityShowOnMapDuration() == 0 + || (ent->getEntityShowOnMapSource() == Entity::SHOW_MAP_GYRO + && ent->getEntityShowOnMapDuration() < detectDuration) ) { - ent->entityShowOnMap = detectDuration; + ent->setEntityShowOnMap(Entity::SHOW_MAP_GYRO, detectDuration); } } free(itemOnGround); @@ -1182,7 +1196,8 @@ void gyroBotAnimate(Entity* my, Stat* myStats, double dist) } } } - if ( ent->entityShowOnMap > 0 ) + if ( ent->getEntityShowOnMapSource() == Entity::SHOW_MAP_GYRO + && ent->getEntityShowOnMapDuration() > 0 ) { doPing = true; } @@ -1326,9 +1341,9 @@ void gyroBotAnimate(Entity* my, Stat* myStats, double dist) } } } - if ( !myStats->EFFECTS[EFF_LEVITATING] ) + if ( !myStats->getEffectActive(EFF_LEVITATING) ) { - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; } } @@ -1875,6 +1890,7 @@ void dummyBotAnimate(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { my->z = 0; + my->creatureHandleLiftZ(); } //Move bodyparts @@ -2150,7 +2166,7 @@ void dummyBotAnimate(Entity* my, Stat* myStats, double dist) case DUMMY_BOX: entity->x += limbs[DUMMYBOT][9][0] * cos(entity->yaw); entity->y += limbs[DUMMYBOT][9][1] * sin(entity->yaw); - entity->z = limbs[DUMMYBOT][9][2]; + entity->z = my->z + limbs[DUMMYBOT][9][2]; entity->focalx = limbs[DUMMYBOT][4][0]; entity->focaly = limbs[DUMMYBOT][4][1]; entity->focalz = limbs[DUMMYBOT][4][2]; @@ -2158,7 +2174,7 @@ void dummyBotAnimate(Entity* my, Stat* myStats, double dist) case DUMMY_LID: entity->x += limbs[DUMMYBOT][10][0] * cos(entity->yaw); entity->y += limbs[DUMMYBOT][10][1] * sin(entity->yaw); - entity->z = limbs[DUMMYBOT][10][2]; + entity->z = my->z + limbs[DUMMYBOT][10][2]; entity->focalx = limbs[DUMMYBOT][5][0]; entity->focaly = limbs[DUMMYBOT][5][1]; entity->focalz = limbs[DUMMYBOT][5][2]; @@ -2166,7 +2182,7 @@ void dummyBotAnimate(Entity* my, Stat* myStats, double dist) case DUMMY_CRANK: entity->x += limbs[DUMMYBOT][12][0] * cos(entity->yaw) + limbs[DUMMYBOT][12][1] * cos(entity->yaw + PI / 2); entity->y += limbs[DUMMYBOT][12][0] * sin(entity->yaw) + limbs[DUMMYBOT][12][1] * sin(entity->yaw + PI / 2); - entity->z = limbs[DUMMYBOT][12][2]; + entity->z = my->z + limbs[DUMMYBOT][12][2]; entity->focalx = limbs[DUMMYBOT][11][0]; entity->focaly = limbs[DUMMYBOT][11][1]; entity->focalz = limbs[DUMMYBOT][11][2]; diff --git a/src/monster_shadow.cpp b/src/monster_shadow.cpp index 1c3a40c67..bb6e0cf9e 100644 --- a/src/monster_shadow.cpp +++ b/src/monster_shadow.cpp @@ -111,7 +111,7 @@ void initShadow(Entity* my, Stat* myStats) } // random effects - myStats->EFFECTS[EFF_LEVITATING] = true; + myStats->setEffectActive(EFF_LEVITATING, 1); myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; // generates equipment and weapons if available from editor @@ -414,7 +414,7 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -468,15 +468,25 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { - if ( my->monsterAnimationLimbOvershoot == ANIMATE_OVERSHOOT_NONE ) + if ( myStats->getEffectActive(EFF_LIFT) ) { my->z = -1.2; - my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; + my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_NONE; + my->creatureHandleLiftZ(); } - if ( dist < 0.1 ) + else { - // not moving, float. - limbAnimateWithOvershoot(my, ANIMATE_Z, 0.005, -2, 0.005, -1.2, ANIMATE_DIR_NEGATIVE); + my->creatureHoverZ = 0.0; + if ( my->monsterAnimationLimbOvershoot == ANIMATE_OVERSHOOT_NONE ) + { + my->z = -1.2; + my->monsterAnimationLimbOvershoot = ANIMATE_OVERSHOOT_TO_SETPOINT; + } + if ( dist < 0.1 ) + { + // not moving, float. + limbAnimateWithOvershoot(my, ANIMATE_Z, 0.005, -2, 0.005, -1.2, ANIMATE_DIR_NEGATIVE); + } } } } @@ -504,7 +514,7 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT && bodypart == 1 ) { // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { //my->z = 2.5; my->pitch = PI / 4; @@ -684,7 +694,7 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { // freeze in place. - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 100; } } @@ -872,14 +882,14 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_TORSO: if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == NULL ) + if ( myStats->breastplate == NULL || !itemModel(myStats->breastplate, false, my) ) { entity->sprite = my->sprite == 1087 ? 1090 : (my->sprite == 1095 ? 1098 : 482); } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); } if ( multiplayer == SERVER ) { @@ -1026,7 +1036,7 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == NULL || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == NULL || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1095,7 +1105,7 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->flags[INVISIBLE] = false; entity->sprite = itemModel(myStats->shield); } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1191,7 +1201,7 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == NULL || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == NULL || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1248,7 +1258,7 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == NULL || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == NULL || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1301,17 +1311,7 @@ void shadowMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1461,7 +1461,7 @@ void Entity::shadowSpecialAbility(bool initialMimic) } //1. Turn invisible. - //myStats->EFFECTS[EFF_INVISIBLE] = true; + //myStats->setEffectActive(EFF_INVISIBLE, 1); //myStats->EFFECTS_TIMERS[EFF_INVISIBLE] = 0; //Does not deactivate until it attacks. //messagePlayer(clientnum, "Turned invisible!"); diff --git a/src/monster_shared.cpp b/src/monster_shared.cpp index 324777c49..dea6e4b3e 100644 --- a/src/monster_shared.cpp +++ b/src/monster_shared.cpp @@ -16,6 +16,8 @@ #include "entity.hpp" #include "prng.hpp" #include "monster.hpp" +#include "shops.hpp" +#include "net.hpp" #include @@ -85,6 +87,10 @@ void Entity::initMonster(int mySprite) { z = limbs[MIMIC][5][2]; } + if ( monsterType == MINIMIMIC ) + { + z = limbs[MINIMIMIC][5][2]; + } } else { if (arachnophobia_filter) { @@ -165,6 +171,7 @@ void Entity::initMonster(int mySprite) monsterSpellAnimation = MONSTER_SPELLCAST_HUMANOID; break; case MIMIC: + case MINIMIMIC: monsterFootstepType = MONSTER_FOOTSTEP_NONE; monsterSpellAnimation = MONSTER_SPELLCAST_NONE; break; @@ -239,6 +246,34 @@ void Entity::initMonster(int mySprite) monsterFootstepType = MONSTER_FOOTSTEP_STOMP; monsterSpellAnimation = MONSTER_SPELLCAST_NONE; break; + case DRYAD: + monsterFootstepType = MONSTER_FOOTSTEP_USE_BOOTS; + monsterSpellAnimation = MONSTER_SPELLCAST_HUMANOID; + break; + case MYCONID: + monsterFootstepType = MONSTER_FOOTSTEP_USE_BOOTS; + monsterSpellAnimation = MONSTER_SPELLCAST_HUMANOID; + break; + case SALAMANDER: + monsterFootstepType = MONSTER_FOOTSTEP_USE_BOOTS; + monsterSpellAnimation = MONSTER_SPELLCAST_HUMANOID; + break; + case GREMLIN: + monsterFootstepType = MONSTER_FOOTSTEP_USE_BOOTS; + monsterSpellAnimation = MONSTER_SPELLCAST_HUMANOID; + break; + case REVENANT_SKULL: + case MONSTER_ADORCISED_WEAPON: + case FLAME_ELEMENTAL: + case HOLOGRAM: + case MOTH_SMALL: + case EARTH_ELEMENTAL: + case DUCK_SMALL: + case MONSTER_UNUSED_6: + case MONSTER_UNUSED_7: + case MONSTER_UNUSED_8: + // unused + break; default: monsterFootstepType = MONSTER_FOOTSTEP_NONE; monsterSpellAnimation = MONSTER_SPELLCAST_NONE; @@ -250,7 +285,18 @@ void Entity::initMonster(int mySprite) Monster Entity::getMonsterTypeFromSprite() const { Sint32 mySprite = this->sprite; - return Entity::getMonsterTypeFromSprite(mySprite); + Monster result = Entity::getMonsterTypeFromSprite(mySprite); + if ( result != NOTHING ) + { + if ( flags[SPRITE] ) + { +#ifndef NDEBUG + assert(true && "getMonsterTypeFromSprite found a conflicting SPRITE flag!"); +#endif + return NOTHING; + } + } + return result; } Monster Entity::getMonsterTypeFromSprite(const int sprite) @@ -339,9 +385,9 @@ void Entity::actMonsterLimb(bool processLight) } } - if ( parentEnt && parentEnt->behavior == &actMonster && parentEnt->monsterEntityRenderAsTelepath == 1 ) + if ( parentEnt && parentEnt->behavior == &actMonster && parentEnt->monsterEntityRenderAsTelepath != 0 ) { - monsterEntityRenderAsTelepath = 1; + monsterEntityRenderAsTelepath = parentEnt->monsterEntityRenderAsTelepath; } else { @@ -351,6 +397,27 @@ void Entity::actMonsterLimb(bool processLight) void Entity::removeMonsterDeathNodes() { + if ( monsterCanTradeWith(-1) ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i]->isLocalPlayer() && shopkeeper[i] == getUID() ) + { + players[i]->closeAllGUIs(CLOSEGUI_ENABLE_SHOOTMODE, CLOSEGUI_CLOSE_ALL); + } + else if ( i > 0 && !client_disconnected[i] && multiplayer == SERVER && !players[i]->isLocalPlayer() ) + { + // inform client of abandonment + strcpy((char*)net_packet->data, "SHPC"); + SDLNet_Write32(getUID(), &net_packet->data[4]); + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + } + } + removeLightField(); int i = 0; node_t *nextnode = nullptr; diff --git a/src/monster_shopkeeper.cpp b/src/monster_shopkeeper.cpp index 28cfb9bd1..5b27363e6 100644 --- a/src/monster_shopkeeper.cpp +++ b/src/monster_shopkeeper.cpp @@ -26,9 +26,14 @@ std::vector generateShopkeeperConsumables(Entity& my, Stat& myStats, int storetype) { + auto& rng = my.entity_rng ? *my.entity_rng : local_rng; std::vector itemsGenerated; + /*if ( !strcmp(map.name, "Mages Guild") ) + { + return itemsGenerated; + }*/ if ( ShopkeeperConsumables_t::entries.find(storetype) == ShopkeeperConsumables_t::entries.end() ) { return itemsGenerated; @@ -59,10 +64,29 @@ std::vector generateShopkeeperConsumables(Entity& my, Stat& myStats, int Status status = slot.status[rng.uniform(0, slot.status.size() - 1)]; Sint16 beatitude = slot.beatitude[rng.uniform(0, slot.beatitude.size() - 1)]; Sint16 count = slot.count[rng.uniform(0, slot.count.size() - 1)]; + if ( !strcmp(map.name, "Mages Guild") ) + { + int numplayers = 0; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] ) + { + ++numplayers; + } + } + if ( type == POTION_HEALING || type == POTION_RESTOREMAGIC ) + { + int limit = std::max(2, 1 + numplayers); + if ( count > limit ) + { + count = limit; + } + } + } Uint32 appearance = 0; if ( slot.appearance.empty() ) { - appearance = rand(); + appearance = rng.rand(); } else { @@ -175,7 +199,7 @@ void initShopkeeper(Entity* my, Stat* myStats) // random effects if ( rng.rand() % 20 == 0 ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 3600; } @@ -317,6 +341,12 @@ void initShopkeeper(Entity* my, Stat* myStats) bool doneLockpick = false; bool doneBackpack = false; bool doneTinkeringKit = false; + bool doneFoci = false; + if ( !strcmp(map.name, "Mages Guild") ) + { + doneAlembic = true; + doneTinkeringKit = true; + } bool doneFeather = false; int doneHardwareHat = 0; switch ( my->monsterStoreType ) @@ -389,11 +419,7 @@ void initShopkeeper(Entity* my, Stat* myStats) tmpItem->count = 1; tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); } - if ( tmpItem->type >= BRONZE_TOMAHAWK && tmpItem->type <= CRYSTAL_SHURIKEN ) - { - // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) - tmpItem->status = std::min(static_cast(DECREPIT + (tmpItem->type - BRONZE_TOMAHAWK)), EXCELLENT); - } + itemLevelCurvePostProcess(my, tmpItem, rng); } } break; @@ -414,6 +440,10 @@ void initShopkeeper(Entity* my, Stat* myStats) tmpItem->count = 1; tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); } + if ( tmpItem ) + { + itemLevelCurvePostProcess(my, tmpItem, rng); + } } break; case 2: @@ -438,6 +468,10 @@ void initShopkeeper(Entity* my, Stat* myStats) tmpItem->count = 1; tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); } + if ( tmpItem ) + { + itemLevelCurvePostProcess(my, tmpItem, rng); + } } break; case 3: @@ -447,19 +481,20 @@ void initShopkeeper(Entity* my, Stat* myStats) switch ( rng.rand() % 3 ) { case 0: - if ( shoplevel >= 18 ) + case 1: + /*if ( shoplevel >= 18 ) { - tmpItem = newItem(itemLevelCurveEntity(*my, SPELLBOOK, 0, shoplevel, rng), static_cast(WORN + rng.rand() % 3), rng.rand() % blessedShopkeeper, 1 + rng.rand() % 2, rng.rand(), true, &myStats->inventory); + tmpItem = newItem(itemLevelCurveEntity(*my, SPELLBOOK, 0, shoplevel, rng), static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 2, rng.rand(), true, &myStats->inventory); } else { - tmpItem = newItem(static_cast(SPELLBOOK_FORCEBOLT + rng.rand() % 21), static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 2, rng.rand(), true, &myStats->inventory); - } + }*/ + tmpItem = newItem(static_cast(SPELLBOOK_FORCEBOLT + rng.rand() % 21), static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 2, rng.rand(), true, &myStats->inventory); break; - case 1: + case 2: tmpItem = newItem(itemLevelCurveEntity(*my, SCROLL, 0, 35, rng), static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 2, rng.rand(), true, &myStats->inventory); break; - case 2: + /*case 2: if ( rng.rand() % 3 == 0 ) { tmpItem = newItem(itemLevelCurveEntity(*my, SCROLL, 0, 35, rng), static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 2, rng.rand(), true, &myStats->inventory); @@ -468,12 +503,38 @@ void initShopkeeper(Entity* my, Stat* myStats) { tmpItem = newItem(READABLE_BOOK, static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 3, rng.rand(), false, &myStats->inventory); } - break; + break;*/ } // post-processing if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } + } + if ( tmpItem ) + { + if ( items[tmpItem->type].category == TOME_SPELL || items[tmpItem->type].category == SPELLBOOK ) + { + int spell_level = currentlevel + 6; + //if ( spell_level >= 10 ) + //{ + // if ( rng.rand() % 8 == 0 ) // some lower level spells + // { + // spell_level = 0 + 5 * rng.rand() % 3; + // } + //} + itemLevelCurvePostProcess(my, tmpItem, rng, spell_level); + } + else + { + itemLevelCurvePostProcess(my, tmpItem, rng); + } + if ( items[tmpItem->type].category == SPELLBOOK && shoplevel >= 18 ) + { + tmpItem->beatitude = rng.rand() % blessedShopkeeper; + } } } if ( !doneFeather && rng.rand() % 20 == 0 ) @@ -501,7 +562,10 @@ void initShopkeeper(Entity* my, Stat* myStats) tmpItem = newItem(TOOL_ALEMBIC, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } } } if ( rng.rand() % 2 == 0 ) @@ -509,7 +573,10 @@ void initShopkeeper(Entity* my, Stat* myStats) tmpItem = newItem(TOOL_ALEMBIC, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } } } tmpItem = newItem(TOOL_ALEMBIC, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); @@ -522,7 +589,10 @@ void initShopkeeper(Entity* my, Stat* myStats) // post-processing if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } } } newItem(POTION_EMPTY, SERVICABLE, 0, 2 + rng.rand() % 5, 0, true, &myStats->inventory); @@ -535,7 +605,44 @@ void initShopkeeper(Entity* my, Stat* myStats) // staff shop for ( c = 0; c < numitems; c++ ) { - if ( shoplevel >= 18 ) + if ( !doneFoci && rng.rand() % 15 == 0 ) + { + int limit = 5; + if ( rng.rand() % 10 == 0 ) + { + // no limit + } + else if ( currentlevel <= 8 ) + { + limit = 2; + } + else if ( currentlevel <= 12 ) + { + limit = 2; + } + switch ( rng.rand() % limit ) + { + case 0: + tmpItem =newItem(TOOL_FOCI_FIRE, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); + break; + case 1: + tmpItem = newItem(TOOL_FOCI_SNOW, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); + break; + case 2: + tmpItem = newItem(TOOL_FOCI_SAND, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); + break; + case 3: + tmpItem = newItem(TOOL_FOCI_ARCS, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); + break; + case 4: + tmpItem = newItem(TOOL_FOCI_NEEDLES, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); + break; + default: + break; + } + doneFoci = true; + } + else if ( shoplevel >= 18 ) { tmpItem = newItem(itemLevelCurveEntity(*my, MAGICSTAFF, 0, shoplevel, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); } @@ -544,9 +651,16 @@ void initShopkeeper(Entity* my, Stat* myStats) tmpItem = newItem(itemLevelCurveEntity(*my, MAGICSTAFF, 0, 15, rng), static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); } // post-processing - if ( rng.rand() % blessedShopkeeper > 0 ) + if ( rng.rand() % blessedShopkeeper > 0 ) + { + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } + } + if ( tmpItem ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + itemLevelCurvePostProcess(my, tmpItem, rng); } } break; @@ -558,14 +672,23 @@ void initShopkeeper(Entity* my, Stat* myStats) // post-processing if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } } } break; case 7: + { + int spawnedItems = 0; // make sure to not spawn over 20 items otherwise overwrites the special shop selection // hardware store for ( c = 0; c < numitems; c++ ) { + if ( spawnedItems >= 20 ) + { + break; + } if ( rng.rand() % 20 == 0 ) { tmpItem = newItem(itemLevelCurveEntity(*my, THROWN, 0, shoplevel + 20, rng), static_cast(SERVICABLE + rng.rand() % 2), 0, 3 + rng.rand() % 3, rng.rand(), false, &myStats->inventory); @@ -574,52 +697,66 @@ void initShopkeeper(Entity* my, Stat* myStats) { tmpItem = newItem(static_cast(TOOL_PICKAXE + rng.rand() % 11), static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 3, rng.rand(), false, &myStats->inventory); } + ++spawnedItems; // post-processing if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } } - if ( tmpItem->type >= BRONZE_TOMAHAWK && tmpItem->type <= CRYSTAL_SHURIKEN ) + if ( tmpItem ) { - // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) - tmpItem->status = std::min(static_cast(DECREPIT + (tmpItem->type - BRONZE_TOMAHAWK)), EXCELLENT); + itemLevelCurvePostProcess(my, tmpItem, rng); } - if ( !doneLockpick && rng.rand() % 2 == 0 ) + if ( !doneLockpick && rng.rand() % 2 == 0 && spawnedItems < 20 ) { tmpItem = newItem(TOOL_LOCKPICK, static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 3, rng.rand(), true, &myStats->inventory); + ++spawnedItems; if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } } doneLockpick = true; } - if ( !doneTinkeringKit && rng.rand() % 5 == 0 ) + if ( !doneTinkeringKit && rng.rand() % 5 == 0 && spawnedItems < 20 ) { newItem(TOOL_TINKERING_KIT, DECREPIT, 0, 1, rng.rand(), true, &myStats->inventory); + ++spawnedItems; doneTinkeringKit = true; } - if ( !doneAlembic && rng.rand() % 2 == 0 ) + if ( !doneAlembic && rng.rand() % 2 == 0 && spawnedItems < 20 ) { tmpItem = newItem(TOOL_ALEMBIC, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); + ++spawnedItems; if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + } } - if ( rng.rand() % 2 == 0 ) + if ( rng.rand() % 2 == 0 && spawnedItems < 20 ) { tmpItem = newItem(TOOL_ALEMBIC, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); - if ( rng.rand() % blessedShopkeeper > 0 ) + ++spawnedItems; + if ( tmpItem ) { tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); } } - if ( rng.rand() % 2 == 0 ) + else if ( rng.rand() % 2 == 0 && spawnedItems < 20 ) { tmpItem = newItem(TOOL_ALEMBIC, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); - if ( rng.rand() % blessedShopkeeper > 0 ) + ++spawnedItems; + if ( tmpItem ) { tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); } @@ -628,9 +765,10 @@ void initShopkeeper(Entity* my, Stat* myStats) } } - if ( !doneBackpack && rng.rand() % 10 == 0 ) + if ( !doneBackpack && rng.rand() % 10 == 0 && spawnedItems < 20 ) { newItem(CLOAK_BACKPACK, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); + ++spawnedItems; doneBackpack = true; } if ( (doneHardwareHat == 0 && rng.rand() % 5 == 0) || (doneHardwareHat == 1 && rng.rand() % 20 == 0) ) @@ -639,46 +777,52 @@ void initShopkeeper(Entity* my, Stat* myStats) int numHats = 1 + ((rng.rand() % 4 == 0) ? 1 : 0); while ( numHats > 0 ) { + if ( spawnedItems >= 20 ) { break; } --numHats; int roll = rng.rand() % 15; ItemType hat = WOODEN_SHIELD; switch ( roll ) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - hat = HELM_MINING; - break; - case 10: - case 11: - case 12: - hat = MASK_HAZARD_GOGGLES; - break; - case 13: - hat = MASK_PIPE; - break; - case 14: - hat = MASK_MOUTHKNIFE; - break; - default: - break; + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + hat = HELM_MINING; + break; + case 10: + case 11: + case 12: + hat = MASK_HAZARD_GOGGLES; + break; + case 13: + hat = MASK_PIPE; + break; + case 14: + hat = MASK_MOUTHKNIFE; + break; + default: + break; } Item* tmpItem = newItem(hat, static_cast(WORN + rng.rand() % 3), 0, 1, rng.rand(), true, &myStats->inventory); + ++spawnedItems; if ( rng.rand() % blessedShopkeeper > 0 ) { - tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); - tmpItem->beatitude += rng.rand() % blessedShopkeeper; + if ( tmpItem ) + { + tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); + tmpItem->beatitude += rng.rand() % blessedShopkeeper; + } } } } break; + } case 8: // weapon/hunting store if ( shoplevel < 10 && customShopkeeperInUse == 0 ) @@ -807,11 +951,7 @@ void initShopkeeper(Entity* my, Stat* myStats) tmpItem->count = 1; tmpItem->status = static_cast(SERVICABLE + rng.rand() % 2); } - if ( tmpItem->type >= BRONZE_TOMAHAWK && tmpItem->type <= CRYSTAL_SHURIKEN ) - { - // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) - tmpItem->status = std::min(static_cast(DECREPIT + (tmpItem->type - BRONZE_TOMAHAWK)), EXCELLENT); - } + itemLevelCurvePostProcess(my, tmpItem, rng); } } break; @@ -819,7 +959,7 @@ void initShopkeeper(Entity* my, Stat* myStats) // general store for ( c = 0; c < numitems; c++ ) { - Category cat = static_cast(rng.rand() % (NUMCATEGORIES - 1)); + Category cat = static_cast(rng.rand() % (Category::CATEGORY_MAX - 2)); tmpItem = newItem(itemLevelCurveEntity(*my, cat, 0, shoplevel + 5, rng), static_cast(WORN + rng.rand() % 3), 0, 1 + rng.rand() % 3, rng.rand(), false, &myStats->inventory); if ( tmpItem && (itemCategory(tmpItem) == WEAPON || itemCategory(tmpItem) == ARMOR || itemCategory(tmpItem) == RING || itemCategory(tmpItem) == AMULET) ) { @@ -830,11 +970,8 @@ void initShopkeeper(Entity* my, Stat* myStats) tmpItem->count = 1; } } - if ( tmpItem && tmpItem->type >= BRONZE_TOMAHAWK && tmpItem->type <= CRYSTAL_SHURIKEN ) - { - // thrown weapons always fixed status. (tomahawk = decrepit, shuriken = excellent) - tmpItem->status = std::min(static_cast(DECREPIT + (tmpItem->type - BRONZE_TOMAHAWK)), EXCELLENT); - } + + itemLevelCurvePostProcess(my, tmpItem, rng); } if ( !doneTinkeringKit && rng.rand() % 20 == 0 ) { @@ -1295,7 +1432,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -1348,7 +1485,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 1.5; my->pitch = PI / 4; @@ -1358,6 +1495,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) my->z = -1; my->pitch = 0; } + my->creatureHandleLiftZ(); } Entity* helmet = nullptr; @@ -1411,13 +1549,13 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) case 2: if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) { entity->sprite = 218; } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); } if ( multiplayer == SERVER ) { @@ -1602,7 +1740,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1675,7 +1813,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1753,7 +1891,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1811,7 +1949,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1864,17 +2002,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1940,7 +2068,7 @@ void shopkeeperMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(SHOPKEEPER, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(SHOPKEEPER, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); diff --git a/src/monster_skeleton.cpp b/src/monster_skeleton.cpp index c24c8cae6..377642b6a 100644 --- a/src/monster_skeleton.cpp +++ b/src/monster_skeleton.cpp @@ -661,7 +661,7 @@ void skeletonDie(Entity* my) { // refund mana to caster. int spellCost = getCostOfSpell(&spell_summon, leader); - if ( (leader->getINT() + leaderStats->getModifiedProficiency(PRO_MAGIC)) >= SKILL_LEVEL_EXPERT ) + if ( (leader->getINT() + leaderStats->getModifiedProficiency(spell_summon.skillID)) >= SKILL_LEVEL_EXPERT ) { // we summoned 2 units, halve the return rate. spellCost /= 2; @@ -793,7 +793,7 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) { wearingring = true; } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -846,7 +846,7 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 2; my->pitch = PI / 4; @@ -856,6 +856,33 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) my->z = -.5; my->pitch = 0; } + + my->creatureHandleLiftZ(); + + if ( myStats->getAttribute("revenant_skeleton") != "" ) + { + if ( my->parent != 0 ) + { + Entity* parent = uidToEntity(my->parent); + if ( !parent ) + { + my->setHP(0); + my->setObituary(Language::get(6806)); + } + } + + int lifetime = std::stoi(myStats->getAttribute("revenant_skeleton")); + --lifetime; + if ( lifetime <= 0 ) + { + my->setHP(0); + my->setObituary(Language::get(6806)); + } + else + { + myStats->setAttribute("revenant_skeleton", std::to_string(lifetime)); + } + } } Entity* shieldarm = nullptr; @@ -912,15 +939,21 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[SKELETON][1][0]; + entity->focaly = limbs[SKELETON][1][1]; + entity->focalz = limbs[SKELETON][1][2]; if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) { entity->sprite = my->sprite == 1103 ? 1106 : 230; } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); } if ( multiplayer == SERVER ) { @@ -1225,7 +1258,7 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1298,7 +1331,7 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1342,7 +1375,7 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1400,7 +1433,7 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1453,17 +1486,7 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1528,7 +1551,7 @@ void skeletonMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(SKELETON, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(SKELETON, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); @@ -2098,4 +2121,4 @@ void Entity::skeletonSummonSetEquipment(Stat* myStats, int rank) myStats->mask->isDroppable = false; } } -} +} \ No newline at end of file diff --git a/src/monster_slime.cpp b/src/monster_slime.cpp index 6a8319b74..7f06d4c56 100644 --- a/src/monster_slime.cpp +++ b/src/monster_slime.cpp @@ -186,7 +186,7 @@ void slimeSetStats(Entity& my, Stat& myStats) myStats.MAXHP = myStats.HP; myStats.OLDHP = myStats.HP; - myStats.setProficiency(PRO_MAGIC, level * 10); + myStats.setProficiency(PRO_SORCERY, level * 10); } void initSlime(Entity* my, Stat* myStats) @@ -456,7 +456,7 @@ void slimeAnimate(Entity* my, Stat* myStats, double dist) { if ( Stat* myStats = my->getStats() ) { - myStats->EFFECTS[EFF_STUNNED] = true; + myStats->setEffectActive(EFF_STUNNED, 1); myStats->EFFECTS_TIMERS[EFF_STUNNED] = slimeSprayDelay / 2; } } @@ -662,6 +662,7 @@ void slimeAnimate(Entity* my, Stat* myStats, double dist) slimeWaterBob = 0.0; } my->focalz += (swimming && MONSTER_ATTACK == 0) ? (1.0 + slimeWaterBob) : 0.0; + my->creatureHandleLiftZ(); } void slimeDie(Entity* my) diff --git a/src/monster_spider.cpp b/src/monster_spider.cpp index 69f052d0e..110d42fd4 100644 --- a/src/monster_spider.cpp +++ b/src/monster_spider.cpp @@ -330,7 +330,7 @@ void spiderMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -373,6 +373,9 @@ void spiderMoveBodyparts(Entity* my, Stat* myStats, double dist) bodypart++; } } + + my->z = 4.5; + my->creatureHandleLiftZ(); } // animate limbs diff --git a/src/monster_succubus.cpp b/src/monster_succubus.cpp index a8a70f10f..f35f3f776 100644 --- a/src/monster_succubus.cpp +++ b/src/monster_succubus.cpp @@ -408,7 +408,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) wearingring = true; } } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -461,7 +461,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 1.5; } @@ -469,6 +469,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->z = -1; } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; @@ -531,6 +532,12 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[SUCCUBUS][1][0]; + entity->focaly = limbs[SUCCUBUS][1][1]; + entity->focalz = limbs[SUCCUBUS][1][2]; my->setHumanoidLimbOffset(entity, SUCCUBUS, LIMB_HUMANOID_TORSO); break; // right leg @@ -680,7 +687,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) { if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -755,7 +762,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -799,7 +806,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -857,7 +864,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -910,17 +917,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -985,7 +982,7 @@ void succubusMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(SUCCUBUS, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(SUCCUBUS, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); @@ -1071,8 +1068,8 @@ void Entity::succubusChooseWeapon(const Entity* target, double dist) bonusFromHP += 1; // +extra 2.5% chance if on lower health } - int requiredRoll = (1 + bonusFromHP + (targetStats->EFFECTS[EFF_CONFUSED] ? 4 : 0) - + (targetStats->EFFECTS[EFF_DRUNK] ? 2 : 0)); // +2.5% base, + extra if target is inebriated + int requiredRoll = (1 + bonusFromHP + (targetStats->getEffectActive(EFF_CONFUSED) ? 4 : 0) + + (targetStats->getEffectActive(EFF_DRUNK) ? 2 : 0)); // +2.5% base, + extra if target is inebriated if ( dist < 40 ) { diff --git a/src/monster_summons.cpp b/src/monster_summons.cpp new file mode 100644 index 000000000..e3712322c --- /dev/null +++ b/src/monster_summons.cpp @@ -0,0 +1,2782 @@ +/*------------------------------------------------------------------------------- + + BARONY + File: monster_skeleton.cpp + Desc: implements all of the skeleton monster's code + + Copyright 2013-2016 (c) Turning Wheel LLC, all rights reserved. + See LICENSE for details. + +-------------------------------------------------------------------------------*/ + +#include "main.hpp" +#include "game.hpp" +#include "stat.hpp" +#include "entity.hpp" +#include "items.hpp" +#include "monster.hpp" +#include "engine/audio/sound.hpp" +#include "net.hpp" +#include "collision.hpp" +#include "player.hpp" +#include "magic/magic.hpp" +#include "prng.hpp" +#include "scores.hpp" +#include "mod_tools.hpp" + +void initRevenantSkull(Entity* my, Stat* myStats) +{ + node_t* node; + + my->z = 0; + my->flags[BURNABLE] = false; + my->initMonster(1796); + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = -1; + MONSTER_SPOTVAR = 1; + MONSTER_IDLESND = -1; + MONSTER_IDLEVAR = 1; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + if ( myStats->getAttribute("revenant_skull") != "" ) + { + myStats->HP = 80 + std::max(0, (myStats->LVL - 5)) * 4; + myStats->MAXHP = myStats->HP; + myStats->OLDHP = myStats->HP; + myStats->STR = 10 + std::max(0, (myStats->LVL - 5)) / 3; + myStats->DEX = 3 + std::max(0, (myStats->LVL - 5)) / 3; + myStats->CON = 5 + std::max(0, (myStats->LVL - 5)) / 4; + myStats->PER = 8 + std::max(0, (myStats->LVL - 5)) / 3; + } + } + } + + // body + Entity* entity = newEntity(1796, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[REVENANT_SKULL][1][0]; + entity->focaly = limbs[REVENANT_SKULL][1][1]; + entity->focalz = limbs[REVENANT_SKULL][1][2]; + entity->behavior = &actRevenantSkullLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); +} + +void initAdorcisedWeapon(Entity* my, Stat* myStats) +{ + node_t* node; + + my->z = 0; + my->initMonster(1797); + my->flags[BURNABLE] = false; + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = -1; + MONSTER_SPOTVAR = 1; + MONSTER_IDLESND = -1; + MONSTER_IDLEVAR = 1; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + if ( myStats->weapon == nullptr && myStats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] == 1 ) + { + int pick = rng.rand() % 8; + switch ( pick ) + { + case 0: + case 1: + myStats->weapon = newItem(IRON_SWORD, static_cast(DECREPIT + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + case 2: + case 3: + myStats->weapon = newItem(IRON_SPEAR, static_cast(DECREPIT + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + case 4: + case 5: + myStats->weapon = newItem(IRON_MACE, static_cast(DECREPIT + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + case 6: + case 7: + myStats->weapon = newItem(IRON_AXE, static_cast(DECREPIT + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + case 8: + case 9: + myStats->weapon = newItem(MAGICSTAFF_FIRE, static_cast(DECREPIT + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, nullptr); + break; + } + } + } + } + + // body + Entity* entity = newEntity(15, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[MONSTER_ADORCISED_WEAPON][1][0]; + entity->focaly = limbs[MONSTER_ADORCISED_WEAPON][1][1]; + entity->focalz = limbs[MONSTER_ADORCISED_WEAPON][1][2]; + entity->behavior = &actAdorcisedWeaponLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); +} + +void initFlameElemental(Entity* my, Stat* myStats) +{ + node_t* node; + + my->z = 0; + my->initMonster(1804); + my->flags[BURNABLE] = false; + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = -1; + MONSTER_SPOTVAR = 1; + MONSTER_IDLESND = -1; + MONSTER_IDLEVAR = 1; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + } + } + + // body + Entity* entity = newEntity(1804, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + //entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = limbs[FLAME_ELEMENTAL][1][0]; + entity->focaly = limbs[FLAME_ELEMENTAL][1][1]; + entity->focalz = limbs[FLAME_ELEMENTAL][1][2]; + entity->behavior = &actFlameElementalLimb; + entity->parent = my->getUID(); + entity->lightBonus = vec4_t{ 0.25, 0.25, 0.25, 0.0 }; + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); +} + +void actRevenantSkullLimb(Entity* my) +{ + if ( my->light ) + { + list_RemoveNode(my->light->node); + my->light = nullptr; + } + + my->light = addLight(my->x / 16, my->y / 16, "revenant_skull_glow"); + my->actMonsterLimb(false); +} + +void actAdorcisedWeaponLimb(Entity* my) +{ + if ( my->light ) + { + list_RemoveNode(my->light->node); + my->light = nullptr; + } + + my->light = addLight(my->x / 16, my->y / 16, "adorcised_weapon_glow"); + my->actMonsterLimb(false); +} + +void actHologramLimb(Entity* my) +{ + if ( my->light ) + { + list_RemoveNode(my->light->node); + my->light = nullptr; + } + + my->light = addLight(my->x / 16, my->y / 16, "summoned_skeleton_glow"); + my->actMonsterLimb(false); +} + +void actFlameElementalLimb(Entity* my) +{ + if ( my->light ) + { + list_RemoveNode(my->light->node); + my->light = nullptr; + } + + my->light = addLight(my->x / 16, my->y / 16, "flame_elemental_glow"); + my->actMonsterLimb(false); +} + +void revenantSkullDie(Entity* my) +{ + real_t gibx = my->x; + real_t giby = my->y; + real_t gibz = my->z; + real_t gibYaw = my->yaw; + + node_t* node = nullptr; + int bodypart = 0; + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, ++bodypart ) + { + if ( bodypart < 2 ) + { + continue; + } + + if ( Entity* entity = (Entity*)node->element ) + { + real_t gibx = entity->x; + real_t giby = entity->y; + real_t gibz = entity->z; + real_t gibYaw = entity->yaw; + } + break; + } + + my->removeMonsterDeathNodes(); + + int c; + for ( c = 0; c < 6; c++ ) + { + Entity* entity = spawnGib(my); + if ( entity ) + { + switch ( c ) + { + case 0: + entity->sprite = my->sprite; + entity->skill[5] = 1; // poof + break; + default: + entity->sprite = 2354; + break; + } + entity->x = gibx; + entity->y = giby; + entity->z = gibz; + entity->yaw = gibYaw; + serverSpawnGibForClient(entity); + } + } + playSoundEntity(my, 94, 128); + list_RemoveNode(my->mynode); + return; +} + +void adorcisedWeaponDie(Entity* my) +{ + my->removeMonsterDeathNodes(); + spawnPoof(my->x, my->y, my->z, 1.0, true); + list_RemoveNode(my->mynode); + return; +} + +void flameElementalDie(Entity* my) +{ + Stat* myStats = my->getStats(); + + if ( myStats && myStats->MP > 0 ) + { + int damage = myStats->LVL; + Entity* caster = uidToEntity(myStats->leader_uid); + damage += getSpellDamageFromID(SPELL_FIREBALL, caster, nullptr, caster); + real_t radius = getSpellEffectDurationFromID(SPELL_FLAME_ELEMENTAL, caster, nullptr, caster); + createSpellExplosionArea(SPELL_FIREBALL, caster, my->x, my->y, 0.0, radius, damage, my); + } + + my->removeMonsterDeathNodes(); + spawnPoof(my->x, my->y, my->z, 1.0, true); + list_RemoveNode(my->mynode); + return; +} + +#define REVENANT_SKULL_BODY 2 +#define SKULL_FLOAT_X body->fskill[2] +#define SKULL_FLOAT_Y body->fskill[3] +#define SKULL_FLOAT_Z body->fskill[4] +#define SKULL_FLOAT_ATK body->fskill[5] +#define SKULL_CIRCLE_ANIM body->fskill[6] +#define SKULL_CIRCLE_SCALE body->fskill[7] +#define SKULL_BOB_ANIM body->fskill[8] +#define SKULL_FLIP body->fskill[9] +#define SKULL_BOBS body->skill[3] +#define SKULL_CIRCLES body->skill[4] +#define SKULL_CIRCLES_DECREMENT_MODE body->skill[5] +#define SKULL_NEXTBOB body->skill[6] +#define SKULL_IDLE_TIMER body->skill[7] + +void revenantSkullAnimate(Entity* my, Stat* myStats, double dist) +{ + node_t* node; + Entity* entity = nullptr; + int bodypart; + + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + + my->sizex = 4; + my->sizey = 4; + + Monster monsterType = my->sprite == 1804 ? FLAME_ELEMENTAL : (my->sprite == 1797 ? MONSTER_ADORCISED_WEAPON : REVENANT_SKULL); + + my->focalx = limbs[monsterType][0][0]; + my->focaly = limbs[monsterType][0][1]; + my->focalz = limbs[monsterType][0][2]; + if ( multiplayer != CLIENT ) + { + my->z = limbs[monsterType][5][2]; + if ( myStats && !myStats->getEffectActive(EFF_LEVITATING) ) + { + myStats->setEffectActive(EFF_LEVITATING, 1); + myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; + } + + my->creatureHandleLiftZ(); + + if ( monsterType == REVENANT_SKULL && myStats ) + { + if ( myStats->getAttribute("revenant_skull") != "" ) + { + if ( my->parent != 0 ) + { + Entity* parent = uidToEntity(my->parent); + if ( !parent ) + { + my->setHP(0); + my->setObituary(Language::get(6806)); + } + } + + int lifetime = std::stoi(myStats->getAttribute("revenant_skull")); + --lifetime; + if ( lifetime <= 0 ) + { + my->setHP(0); + my->setObituary(Language::get(6806)); + } + else + { + myStats->setAttribute("revenant_skull", std::to_string(lifetime)); + } + } + } + else if ( monsterType == MONSTER_ADORCISED_WEAPON && myStats ) + { + if ( myStats->getAttribute("spirit_weapon") != "" ) + { + my->flags[PASSABLE] = true; + } + + if ( !myStats->weapon ) + { + my->setHP(0); + char buf[120]; + snprintf(buf, sizeof(buf), Language::get(6620)); + my->setObituary(buf); + } + else + { + if ( myStats->getAttribute("spirit_weapon") != "" ) + { + if ( my->parent != 0 ) + { + Entity* parent = uidToEntity(my->parent); + if ( !parent ) + { + my->setHP(0); + if ( myStats->weapon ) + { + my->setObituary(Language::get(6619)); + } + } + } + if ( my->ticks > std::stoi(myStats->getAttribute("spirit_weapon")) ) + { + my->setHP(0); + if ( myStats->weapon ) + { + my->setObituary(Language::get(6619)); + } + } + } + if ( myStats->getAttribute("adorcised_weapon") != "" ) + { + if ( my->ticks > std::stoi(myStats->getAttribute("adorcised_weapon")) ) + { + my->setHP(0); + if ( myStats->weapon ) + { + my->setObituary(Language::get(6619)); + } + } + } + } + } + } + + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_g] ) + { + keystatus[SDLK_g] = 0; + MONSTER_ATTACK = MONSTER_POSE_MELEE_WINDUP1; + MONSTER_ATTACKTIME = 0; + } + if ( keystatus[SDLK_h] ) + { + keystatus[SDLK_h] = 0; + MONSTER_ATTACK = MONSTER_POSE_MAGIC_WINDUP1; + MONSTER_ATTACKTIME = 0; + } + if ( keystatus[SDLK_j] && myStats ) + { + keystatus[SDLK_j] = 0; + myStats->setEffectValueUnsafe(EFF_PARALYZED, myStats->getEffectActive(EFF_PARALYZED) ? 0 : 1); + } + } + + bool adorcisedWeapon = monsterType == MONSTER_ADORCISED_WEAPON; + bool poke = false; + + Entity* body = nullptr; + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, ++bodypart ) + { + if ( bodypart < REVENANT_SKULL_BODY ) + { + continue; + } + + entity = (Entity*)node->element; + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + entity->yaw = my->yaw; + + if ( bodypart == REVENANT_SKULL_BODY ) + { + if ( adorcisedWeapon ) + { + if ( entity->sprite == items[QUARTERSTAFF].index + || entity->sprite == items[IRON_SPEAR].index + || entity->sprite == items[STEEL_HALBERD].index + || entity->sprite == items[ARTIFACT_SPEAR].index + || entity->sprite == items[CRYSTAL_SPEAR].index + || entity->sprite == items[BLACKIRON_TRIDENT].index + || entity->sprite == items[LANCE_SPEAR].index + || entity->sprite == items[BONE_SPEAR].index + || entity->sprite == items[SILVER_GLAIVE].index ) + { + poke = true; + } + if ( entity->sprite == items[BRONZE_SWORD].index + || entity->sprite == items[IRON_SWORD].index + || entity->sprite == items[STEEL_SWORD].index + || entity->sprite == items[ARTIFACT_SWORD].index + || entity->sprite == items[CRYSTAL_SWORD].index + || entity->sprite == items[BLACKIRON_SWORD].index + || entity->sprite == items[SILVER_SWORD].index + || entity->sprite == items[BONE_SWORD].index + || entity->sprite == items[CLAYMORE_SWORD].index + || entity->sprite == items[ANELACE_SWORD].index + || entity->sprite == items[RAPIER].index ) + { + poke = true; + } + } + + body = entity; + entity->fskill[0] = fmod(entity->fskill[0], 2 * PI); + while ( entity->fskill[0] >= PI ) + { + entity->fskill[0] -= 2 * PI; + } + while ( entity->fskill[0] < -PI ) + { + entity->fskill[0] += 2 * PI; + } + + real_t basePitchSetpoint = (limbs[monsterType][14][0] * PI / 180.0); + if ( MONSTER_ATTACK == 0 ) + { + real_t speed = -0.05; + if ( adorcisedWeapon ) + { + speed = -0.2; + } + real_t setpoint = basePitchSetpoint; + if ( limbAngleWithinRange(entity->fskill[0], speed, setpoint) ) + { + entity->fskill[0] = setpoint; + } + else if ( entity->fskill[0] < (setpoint - 0.01) ) + { + entity->fskill[0] += -speed; + entity->fskill[0] = std::min(entity->fskill[0], setpoint); + } + else + { + entity->fskill[0] += speed; + entity->fskill[0] = std::max(entity->fskill[0], setpoint); + } + } + + if ( !adorcisedWeapon ) + { + if ( MONSTER_ATTACK > 0 ) + { + entity->fskill[0] = basePitchSetpoint; + } + } + + if ( MONSTER_ATTACK == 0 ) + { + SKULL_FLOAT_ATK = 0.0; + } + + if ( multiplayer != CLIENT ) + { + if ( SKULL_IDLE_TIMER == 0 ) + { + if ( my->ticks < TICKS_PER_SECOND / 10 ) + { + SKULL_IDLE_TIMER = TICKS_PER_SECOND / 10; + } + else + { + SKULL_IDLE_TIMER = (local_rng.rand() % 7 + 5) * TICKS_PER_SECOND; + } + } + else if ( MONSTER_ATTACK == 0 ) + { + if ( SKULL_IDLE_TIMER > 0 ) + { + --SKULL_IDLE_TIMER; + if ( SKULL_IDLE_TIMER == 0 ) + { + int pick = MONSTER_POSE_RANGED_WINDUP1 + local_rng.rand() % 3; + if ( my->ticks < TICKS_PER_SECOND * 5 ) + { + pick = MONSTER_POSE_RANGED_WINDUP1; + } + + if ( pick == MONSTER_POSE_RANGED_WINDUP2 ) + { + if ( adorcisedWeapon && myStats->getAttribute("spirit_weapon") != "" ) + { + pick = MONSTER_POSE_RANGED_WINDUP3; + } + else if ( my->monsterState != MONSTER_STATE_WAIT ) + { + pick = MONSTER_POSE_RANGED_WINDUP3; + } + } + + if ( pick == MONSTER_POSE_RANGED_WINDUP2 ) + { + my->setEffect(EFF_STUNNED, true, 4 * TICKS_PER_SECOND, false); + } + my->attack(pick, 0, nullptr); + } + } + } + } + + if ( MONSTER_ATTACK == MONSTER_POSE_RANGED_WINDUP1 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + if ( SKULL_FLIP <= 0.0 ) + { + SKULL_FLIP = 1.0; + } + if ( SKULL_BOBS == 0 ) + { + SKULL_BOBS = 5; + SKULL_BOB_ANIM = 0.0; + } + else + { + SKULL_NEXTBOB = 5; + } + } + MONSTER_ATTACK = 0; + } + else if ( MONSTER_ATTACK == MONSTER_POSE_RANGED_WINDUP2 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + if ( SKULL_FLIP <= 0.0 ) + { + SKULL_FLIP = 1.0; + } + if ( SKULL_CIRCLES == 0 ) + { + SKULL_CIRCLES = 2; + SKULL_CIRCLE_ANIM = 0.0; + SKULL_CIRCLE_SCALE = 0.0; + SKULL_CIRCLES_DECREMENT_MODE = 0; + } + if ( SKULL_BOBS == 0 ) + { + SKULL_BOBS = 5; + SKULL_BOB_ANIM = 0.0; + } + else + { + SKULL_NEXTBOB = 5; + } + } + MONSTER_ATTACK = 0; + } + else if ( MONSTER_ATTACK == MONSTER_POSE_RANGED_WINDUP3 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + if ( SKULL_BOBS == 0 ) + { + SKULL_BOBS = 5; + SKULL_BOB_ANIM = 0.0; + } + else + { + SKULL_NEXTBOB = 5; + } + } + MONSTER_ATTACK = 0; + } + else if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP1 || MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 + || MONSTER_ATTACK == 1 + || MONSTER_ATTACK == MONSTER_POSE_MAGIC_CAST1 ) + { + int delay = (MONSTER_ATTACK == MONSTER_POSE_MAGIC_CAST1 || MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1) ? 25 : 0; + if ( MONSTER_ATTACKTIME == 0 ) + { + entity->fskill[0] = basePitchSetpoint; + entity->skill[1] = 0; + SKULL_FLOAT_ATK = 0.0; + + if ( MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + if ( multiplayer != CLIENT ) + { + my->setEffect(EFF_STUNNED, true, delay, false); + } + createParticleDot(my); + // play casting sound + playSoundEntityLocal(my, 170, 64); + if ( SKULL_CIRCLES == 0 ) + { + SKULL_CIRCLE_ANIM = 0.0; + SKULL_CIRCLE_SCALE = 0.0; + } + SKULL_CIRCLES = std::min(2, SKULL_CIRCLES + 2); + if ( SKULL_BOBS == 0 ) + { + SKULL_BOBS = 3; + SKULL_BOB_ANIM = 0.0; + } + else + { + SKULL_NEXTBOB = 3; + } + } + } + else + { + if ( MONSTER_ATTACKTIME >= (int)limbs[monsterType][15][0] + delay ) + { + if ( MONSTER_ATTACKTIME == (int)limbs[monsterType][15][0] + delay ) + { + SKULL_CIRCLES = 0; + + if ( multiplayer != CLIENT ) + { + const Sint32 temp = MONSTER_ATTACKTIME; + my->attack(MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ? MONSTER_POSE_MAGIC_CAST1 : 1, 0, nullptr); // slop + MONSTER_ATTACKTIME = temp; + + if ( adorcisedWeapon && myStats->getAttribute("spirit_weapon") != "" ) + { + // knockback to lunge forward + if ( my->setEffect(EFF_KNOCKBACK, true, 25, false) ) + { + real_t pushbackMultiplier = 1.5; + real_t tangent = my->yaw; + my->vel_x = cos(tangent) * pushbackMultiplier; + my->vel_y = sin(tangent) * pushbackMultiplier; + my->monsterKnockbackVelocity = 0.025; + my->monsterKnockbackUID = 0; + my->monsterKnockbackTangentDir = tangent; + + if ( multiplayer != CLIENT ) + { + Entity* spellTimer = createParticleTimer(my, 25, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SPIRIT_WEAPON_ATTACK; + + playSoundEntity(my, 23 + local_rng.rand() % 5, 128); // whoosh noise + } + } + } + } + } + + if ( !adorcisedWeapon ) + { + if ( entity->skill[1] == 0 ) + { + real_t speed = limbs[monsterType][13][2]; + real_t setpoint = (limbs[monsterType][14][2] * PI / 180.0); + if ( limbAngleWithinRange(entity->fskill[0], -speed, setpoint) ) + { + entity->fskill[0] = setpoint; + entity->skill[1] = 1; + } + else + { + entity->fskill[0] -= speed; + entity->fskill[0] = std::max(entity->fskill[0], setpoint); + } + } + else + { + real_t speed = limbs[monsterType][13][1]; + entity->fskill[0] += speed; + entity->fskill[0] = std::min(entity->fskill[0], basePitchSetpoint); + } + } + else + { + if ( poke ) + { + static ConsoleVariable cvar_revenant_pokeset("/revenant_pokeset", 90); + static ConsoleVariable cvar_revenant_pokespd("/revenant_pokespd", 0.01); + static ConsoleVariable cvar_revenant_pokespd2("/revenant_pokespd2", -0.25); + if ( entity->skill[1] == 0 ) + { + real_t speed = *cvar_revenant_pokespd; + real_t setpoint = (*cvar_revenant_pokeset * PI / 180.0); + if ( limbAngleWithinRange(entity->fskill[0], -speed, setpoint) ) + { + entity->fskill[0] = setpoint; + entity->skill[1] = 1; + } + else + { + entity->fskill[0] -= speed; + } + } + else + { + real_t speed = *cvar_revenant_pokespd2; + if ( limbAngleWithinRange(entity->fskill[0], speed, basePitchSetpoint) ) + { + entity->fskill[0] = basePitchSetpoint; + } + else + { + entity->fskill[0] += speed; + } + } + } + else + { + if ( entity->skill[1] == 0 ) + { + real_t speed = limbs[monsterType][3][2]; + real_t setpoint = (limbs[monsterType][4][2] * PI / 180.0); + if ( limbAngleWithinRange(entity->fskill[0], -speed, setpoint) ) + { + entity->fskill[0] = setpoint; + entity->skill[1] = 1; + } + else + { + entity->fskill[0] -= speed; + entity->fskill[0] = std::max(entity->fskill[0], setpoint); + } + } + else + { + real_t speed = limbs[monsterType][3][1]; + entity->fskill[0] += speed; + entity->fskill[0] = std::min(entity->fskill[0], basePitchSetpoint); + } + } + } + } + else + { + if ( !adorcisedWeapon ) + { + real_t speed = limbs[monsterType][13][0]; + real_t setpoint = (limbs[monsterType][14][1] * PI / 180.0); + entity->fskill[0] -= speed; + if ( setpoint >= 0 ) + { + entity->fskill[0] = std::min(entity->fskill[0], setpoint); + } + else + { + entity->fskill[0] = std::max(entity->fskill[0], setpoint); + } + } + else + { + if ( poke ) + { + static ConsoleVariable cvar_revenant_pokeset1("/revenant_pokeset1", 110.0); + static ConsoleVariable cvar_revenant_pokespd1("/revenant_pokespd1", -0.25); + real_t speed = *cvar_revenant_pokespd1; + real_t setpoint = (*cvar_revenant_pokeset1 * PI / 180.0); + if ( limbAngleWithinRange(entity->fskill[0], -speed, setpoint) ) + { + entity->fskill[0] = setpoint; + } + else + { + entity->fskill[0] -= speed; + } + } + else + { + real_t speed = limbs[monsterType][3][0]; + real_t setpoint = (limbs[monsterType][4][1] * PI / 180.0); + entity->fskill[0] -= speed; + if ( setpoint >= 0 ) + { + entity->fskill[0] = std::min(entity->fskill[0], setpoint); + } + else + { + entity->fskill[0] = std::max(entity->fskill[0], setpoint); + } + } + } + } + + if ( MONSTER_ATTACKTIME >= (int)limbs[monsterType][18][0] + delay ) + { + if ( MONSTER_ATTACK == MONSTER_POSE_MAGIC_CAST1 || MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + SKULL_FLOAT_ATK += limbs[monsterType][12][1]; + SKULL_FLOAT_ATK = std::min(SKULL_FLOAT_ATK, (real_t)limbs[monsterType][12][2]); + } + else + { + SKULL_FLOAT_ATK -= limbs[monsterType][18][1]; + SKULL_FLOAT_ATK = std::max(SKULL_FLOAT_ATK, (real_t)limbs[monsterType][18][2]); + } + } + else if ( MONSTER_ATTACKTIME >= (int)limbs[monsterType][17][0] + delay ) + { + if ( MONSTER_ATTACK == MONSTER_POSE_MAGIC_CAST1 || MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ) + { + SKULL_FLOAT_ATK += limbs[monsterType][19][1]; + SKULL_FLOAT_ATK = std::min(SKULL_FLOAT_ATK, (real_t)limbs[monsterType][19][2]); + } + else + { + SKULL_FLOAT_ATK += limbs[monsterType][17][1]; + SKULL_FLOAT_ATK = std::min(SKULL_FLOAT_ATK, (real_t)limbs[monsterType][17][2]); + } + } + else if ( MONSTER_ATTACKTIME >= (int)limbs[monsterType][16][0] + delay ) + { + SKULL_FLOAT_ATK -= limbs[monsterType][16][1]; + SKULL_FLOAT_ATK = std::max(SKULL_FLOAT_ATK, (real_t)limbs[monsterType][16][2]); + } + } + + if ( MONSTER_ATTACKTIME >= (int)limbs[monsterType][15][1] + delay ) + { + MONSTER_ATTACK = 0; + } + } + } + + switch ( bodypart ) + { + case REVENANT_SKULL_BODY: + { + entity->x += limbs[monsterType][6][0] * cos(entity->yaw); + entity->y += limbs[monsterType][6][1] * sin(entity->yaw); + entity->z += limbs[monsterType][6][2]; + entity->focalx = limbs[monsterType][1][0]; + entity->focaly = limbs[monsterType][1][1]; + entity->focalz = limbs[monsterType][1][2]; + + entity->pitch = entity->fskill[0]; + + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_PLUS] ) + { + keystatus[SDLK_KP_PLUS] = 0; + entity->skill[0] = entity->skill[0] == 0 ? 1 : 0; + } + if ( keystatus[SDLK_KP_6] ) + { + keystatus[SDLK_KP_6] = 0; + SKULL_CIRCLES = 2; + SKULL_CIRCLE_ANIM = 0.0; + SKULL_CIRCLE_SCALE = 0.0; + SKULL_CIRCLES_DECREMENT_MODE = 0; + } + if ( keystatus[SDLK_KP_4] ) + { + keystatus[SDLK_KP_4] = 0; + SKULL_CIRCLES = 0; + } + if ( keystatus[SDLK_KP_2] ) + { + keystatus[SDLK_KP_2] = 0; + SKULL_FLIP = 1.0; + + } + if ( keystatus[SDLK_KP_3] ) + { + } + if ( keystatus[SDLK_KP_1] ) + { + } + if ( keystatus[SDLK_KP_0] ) + { + keystatus[SDLK_KP_0] = 0; + SKULL_BOBS = 5; + SKULL_BOB_ANIM = 0.0; + } + } + + entity->pitch -= PI * (-1 + sin(PI / 2 + SKULL_FLIP * PI)); + SKULL_FLIP -= 0.05; + SKULL_FLIP = std::max(0.0, SKULL_FLIP); + + if ( SKULL_CIRCLES > 0 ) + { + real_t prev = SKULL_CIRCLE_ANIM; + real_t mult = MONSTER_ATTACK == MONSTER_POSE_MAGIC_WINDUP1 ? 5.0 : 1.0; + SKULL_CIRCLE_ANIM += limbs[monsterType][8][2] * mult; + SKULL_CIRCLE_SCALE += limbs[monsterType][8][1] * mult; + SKULL_CIRCLE_SCALE = std::min(1.0, SKULL_CIRCLE_SCALE); + if ( prev < PI && SKULL_CIRCLE_ANIM >= PI ) + { + SKULL_CIRCLES--; + } + } + if ( SKULL_CIRCLES == 0 ) + { + if ( SKULL_CIRCLES_DECREMENT_MODE == 0 ) + { + SKULL_CIRCLE_SCALE -= limbs[monsterType][8][1]; + SKULL_CIRCLE_SCALE = std::max(0.0, SKULL_CIRCLE_SCALE); + if ( SKULL_CIRCLE_ANIM >= PI ) + { + SKULL_CIRCLE_ANIM += limbs[monsterType][8][2] * 4; + SKULL_CIRCLE_ANIM = std::min(SKULL_CIRCLE_ANIM, 2 * PI); + } + else + { + SKULL_CIRCLE_ANIM -= limbs[monsterType][8][2] * 4; + SKULL_CIRCLE_ANIM = std::max(SKULL_CIRCLE_ANIM, 0.0); + } + } + else + { + SKULL_CIRCLE_SCALE -= limbs[monsterType][8][1] * 2; + SKULL_CIRCLE_SCALE = std::max(0.0, SKULL_CIRCLE_SCALE); + + // spin fast + if ( SKULL_CIRCLE_SCALE <= 0.0 ) + { + SKULL_CIRCLE_ANIM = 0.0; + } + } + } + while ( SKULL_CIRCLE_ANIM >= 2 * PI ) + { + SKULL_CIRCLE_ANIM -= 2 * PI; + } + while ( SKULL_CIRCLE_ANIM < 0.0 ) + { + SKULL_CIRCLE_ANIM += 2 * PI; + } + if ( entity->skill[0] == 0 ) + { + entity->fskill[1] += 0.1; + } + + SKULL_FLOAT_X = limbs[monsterType][10][0] * sin(entity->fskill[1] * limbs[monsterType][11][0]) * cos(entity->yaw + PI / 2); + SKULL_FLOAT_Y = limbs[monsterType][10][1] * sin(entity->fskill[1] * limbs[monsterType][11][1]) * sin(entity->yaw + PI / 2); + SKULL_FLOAT_Z = limbs[monsterType][10][2] * sin(entity->fskill[1] * limbs[monsterType][11][2]); + real_t floatAtkZ = SKULL_FLOAT_ATK < 0 ? 2 * sin(SKULL_FLOAT_ATK * PI / 8) : 0.5 * sin(SKULL_FLOAT_ATK * PI / 8); + SKULL_FLOAT_Z += floatAtkZ; + + SKULL_FLOAT_X += SKULL_FLOAT_ATK * cos(entity->yaw); + SKULL_FLOAT_Y += SKULL_FLOAT_ATK * sin(entity->yaw); + + { + SKULL_FLOAT_X += SKULL_CIRCLE_SCALE * limbs[monsterType][8][0] * cos(entity->yaw + SKULL_CIRCLE_ANIM); + SKULL_FLOAT_Y += SKULL_CIRCLE_SCALE * limbs[monsterType][8][0] * sin(entity->yaw + SKULL_CIRCLE_ANIM); + + if ( SKULL_CIRCLES_DECREMENT_MODE == 1 ) + { + // spin fast + entity->yaw += SKULL_CIRCLE_SCALE * (PI / 2 + SKULL_CIRCLE_ANIM); + } + else + { + // ease to normal + entity->yaw += SKULL_CIRCLE_SCALE * PI / 2 + SKULL_CIRCLE_ANIM; + } + + if ( SKULL_BOBS > 0 ) + { + SKULL_BOB_ANIM += limbs[monsterType][9][0]; + + real_t scale = SKULL_BOBS * 0.5; + + SKULL_FLOAT_Z += scale * sin(PI / 4) - scale * sin(SKULL_BOB_ANIM * 2 * PI + PI / 4); + + if ( SKULL_BOB_ANIM >= 1.0 ) + { + SKULL_BOB_ANIM = 0.0; + if ( SKULL_NEXTBOB > 0 ) + { + SKULL_BOBS = SKULL_NEXTBOB; + SKULL_NEXTBOB = 0; + } + else + { + --SKULL_BOBS; + } + } + } + } + + entity->x += SKULL_FLOAT_X; + entity->y += SKULL_FLOAT_Y; + entity->z += SKULL_FLOAT_Z; + + if ( monsterType == FLAME_ELEMENTAL ) + { + entity->roll = (PI / 32) * sin(entity->fskill[1] * limbs[monsterType][11][0]); + entity->yaw += (PI / 32) * sin(entity->fskill[1] * limbs[monsterType][11][0]); + } + else if ( adorcisedWeapon ) + { + entity->roll = (PI / 32) * sin(entity->fskill[1] * limbs[monsterType][11][0]); + entity->yaw += (PI / 32) * sin(entity->fskill[1] * limbs[monsterType][11][0]); + + if ( multiplayer != CLIENT ) + { + if ( myStats->weapon ) + { + entity->sprite = itemModel(myStats->weapon); + } + if ( entity->ticks >= *cvar_entity_bodypart_sync_tick ) + { + bool updateBodypart = false; + if ( entity->skill[10] != entity->sprite ) + { + entity->skill[10] = entity->sprite; + updateBodypart = true; + } + if ( entity->getUID() % (TICKS_PER_SECOND * 10) == ticks % (TICKS_PER_SECOND * 10) ) + { + updateBodypart = true; + } + if ( updateBodypart ) + { + serverUpdateEntityBodypart(my, bodypart); + } + } + } + } + + if ( monsterType == FLAME_ELEMENTAL ) + { + if ( my->ticks % 5 == 0 ) + { + Entity* fx = spawnMagicParticleCustom(entity, 233, 0.5, 1.0); + fx->x -= 2.0 * cos(entity->yaw); + fx->y -= 2.0 * cos(entity->yaw); + fx->vel_x = 0.1 * cos(entity->yaw + PI); + fx->vel_y = 0.1 * sin(entity->yaw + PI); + fx->vel_z = -0.15; + fx->behavior = &actSprite; + fx->flags[SPRITE] = true; + fx->ditheringDisabled = true; + fx->skill[0] = 1; + fx->skill[1] = 12; + fx->skill[2] = 4; + fx->actSpriteVelXY = 1; + } + + const real_t squishRate = dist < 0.1 ? 1.5 : 3.0; + const real_t squishFactor = 0.1;// dist < 0.1 ? 0.05 : 0.3; + const real_t inc = squishRate * (PI / TICKS_PER_SECOND); + real_t& slimeBob = entity->fskill[24]; + slimeBob = fmod(slimeBob + inc, PI * 2); + entity->scalex = 0.9 - sin(slimeBob) * squishFactor; + entity->scaley = 0.9 - sin(slimeBob) * squishFactor; + entity->scalez = 0.9 + sin(slimeBob) * squishFactor; + } + else + { + Entity* fx = spawnMagicParticleCustom(entity, 96, 0.5, 1.0); + fx->vel_x = 0.25 * cos(entity->yaw + PI); + fx->vel_y = 0.25 * sin(entity->yaw + PI); + fx->vel_z = 0.3; + fx->flags[SPRITE] = true; + fx->ditheringDisabled = true; + } + break; + } + default: + break; + } + } + + if ( MONSTER_ATTACK > 0 ) + { + MONSTER_ATTACKTIME++; + } + else if ( MONSTER_ATTACK == 0 ) + { + MONSTER_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } +} + +void hologramDie(Entity* my) +{ + my->removeMonsterDeathNodes(); + spawnPoof(my->x, my->y, my->z, 1.0, true); + list_RemoveNode(my->mynode); + return; +} + +void initHologram(Entity* my, Stat* myStats) +{ + node_t* node; + + my->z = 0; + my->initMonster(1803); + my->flags[BURNABLE] = false; + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = -1; + MONSTER_SPOTVAR = 1; + MONSTER_IDLESND = -1; + MONSTER_IDLEVAR = 1; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + } + } + + // body + for ( int i = 0; i < 30; ++i ) + { + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = my->flags[USERFLAG2]; + entity->focalx = 0.0; + entity->focaly = 0.0; + entity->focalz = 0.0; + entity->behavior = &actHologramLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + } + + my->mistformGLRender = 2.0; +} + +void hologramAnimate(Entity* my, Stat* myStats, double dist) +{ + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + my->sizex = 4; + my->sizey = 4; + my->mistformGLRender = 2.0; + + Entity* hologramParent = nullptr; + if ( my->monsterSpecialState != 0 ) + { + hologramParent = uidToEntity(my->monsterSpecialState); + } + if ( multiplayer != CLIENT ) + { + if ( !hologramParent || (myStats && !myStats->getEffectActive(EFF_MIST_FORM)) ) + { + my->setHP(0); + my->setObituary(Language::get(6668)); + } + + my->z = 0.0; + my->creatureHandleLiftZ(); + } + + int bodypart = 0; + node_t* node = nullptr; + std::vector myLimbs; + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, ++bodypart ) + { + if ( bodypart < LIMB_HUMANOID_TORSO ) + { + continue; + } + + if ( Entity* entity = (Entity*)node->element ) + { + entity->flags[INVISIBLE] = true; + entity->flags[INVISIBLE_DITHER] = false; + entity->mistformGLRender = my->mistformGLRender; + myLimbs.push_back(entity); + } + } + + if ( hologramParent && (hologramParent->behavior == &actMonster || hologramParent->behavior == &actPlayer) ) + { + int parentOffset = 0; + if ( hologramParent->behavior == &actPlayer ) + { + parentOffset = -1; + } + std::vector limbsCopy; + limbsCopy.push_back(hologramParent); + + int listSize = list_Size(&hologramParent->children); + for ( int i = LIMB_HUMANOID_TORSO + parentOffset; i < listSize; ++i ) + { + if ( node_t* nodeCopy = list_Node(&hologramParent->children, i) ) + { + if ( Entity* limb = (Entity*)nodeCopy->element ) + { + limbsCopy.push_back(limb); + } + } + } + + int index = -1; + Entity* firstLimb = nullptr; + for ( auto entity : myLimbs ) + { + ++index; + if ( index == 0 ) + { + firstLimb = entity; + firstLimb->fskill[0] += 0.05; + } + if ( index < limbsCopy.size() ) + { + Entity* limb = limbsCopy.at(index); + entity->sprite = limb->sprite; + entity->flags[INVISIBLE] = limb->flags[INVISIBLE]; + entity->flags[INVISIBLE_DITHER] = limb->flags[INVISIBLE_DITHER]; + entity->yaw = limb->yaw + firstLimb->fskill[0]; + entity->pitch = limb->pitch; + entity->roll = limb->roll; + if ( index == 0 ) + { + entity->x = my->x; + entity->y = my->y; + } + else + { + real_t x = (hologramParent->x - limb->x); + real_t y = (hologramParent->y - limb->y); + real_t tangent = atan2(y, x); + real_t length = sqrt(x * x + y * y); + tangent += firstLimb->fskill[0]; + x = length * cos(tangent); + y = length * sin(tangent); + + entity->x = my->x - (hologramParent->x - limb->x); + entity->y = my->y - (hologramParent->y - limb->y); + entity->x = my->x - x; + entity->y = my->y - y; + } + entity->z = limb->z; + entity->focalx = limb->focalx; + entity->focaly = limb->focaly; + entity->focalz = limb->focalz; + entity->scalex = limb->scalex; + entity->scaley = limb->scaley; + entity->scalez = limb->scalez; + entity->sizex = limb->sizex; + entity->sizey = limb->sizey; + + + } + } + } +} + +void actEarthElementalDeathGib(Entity* my) +{ + if ( my->skill[0] == 0 ) + { + my->z += 0.05; + if ( my->z >= 7.5 ) + { + my->scalex -= 0.05; + my->scaley -= 0.05; + my->scalez -= 0.05; + if ( my->scalex <= 0.0 ) + { + list_RemoveNode(my->mynode); + return; + } + } + } + else + { + if ( my->skill[0] > 0 ) + { + --my->skill[0]; + } + + my->scalex = std::min(my->scalex + 0.0125, 0.5); + my->scaley = std::min(my->scaley + 0.0125, 0.5); + my->scalez = std::min(my->scalez + 0.0125, 0.5); + } +} + +void earthElementalDie(Entity* my) +{ + int index = -1; + for ( auto bodypart : my->bodyparts ) + { + ++index; + if ( index == 1 ) // eyes + { + continue; + } + Entity* entity = spawnGib(my, bodypart->sprite); + entity->x = bodypart->x; + entity->y = bodypart->y; + entity->z = bodypart->z; + if ( index == 0 ) + { + entity->skill[5] = 1; // poof + } + entity->vel_x *= 0.1; + entity->vel_y *= 0.1; + serverSpawnGibForClient(entity); + } + + Entity* spellTimer = createParticleTimer(nullptr, TICKS_PER_SECOND, -1); + spellTimer->x = my->x; + spellTimer->y = my->y; + spellTimer->z = my->z; + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL_DIE; + serverSpawnMiscParticlesAtLocation(spellTimer->x, spellTimer->y, spellTimer->z, PARTICLE_EFFECT_EARTH_ELEMENTAL_DIE, 0); + + playSoundEntity(my, 798, 64); + + my->removeMonsterDeathNodes(); + //spawnPoof(my->x, my->y, my->z, 1.0, true); + list_RemoveNode(my->mynode); + return; +} + +void actEarthElementalLimb(Entity* my) +{ + my->actMonsterLimb(false); +} + +void initEarthElemental(Entity* my, Stat* myStats) +{ + node_t* node; + + my->z = 0; + + int sprite = 1871; // default sprite, summon anim + if ( multiplayer != CLIENT ) + { + if ( MONSTER_INIT ) + { + sprite = 1876; // no summon anim + } + } + my->initMonster(sprite); + my->flags[BURNABLE] = false; + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + my->flags[PASSABLE] = true; + if ( multiplayer != CLIENT ) + { + MONSTER_SPOTSND = 797; + MONSTER_SPOTVAR = 1; + MONSTER_IDLESND = 800; + MONSTER_IDLEVAR = 3; + } + + if ( multiplayer != CLIENT && !MONSTER_INIT ) + { + auto& rng = my->entity_rng ? *my->entity_rng : local_rng; + + if ( myStats != nullptr ) + { + if ( !myStats->leader_uid ) + { + myStats->leader_uid = 0; + } + + // apply random stat increases if set in stat_shared.cpp or editor + setRandomMonsterStats(myStats, rng); + + // generate 6 items max, less if there are any forced items from boss variants + int customItemsToGenerate = ITEM_CUSTOM_SLOT_LIMIT; + + // generates equipment and weapons if available from editor + createMonsterEquipment(myStats, rng); + + // create any custom inventory items from editor if available + createCustomInventory(myStats, customItemsToGenerate, rng); + + // count if any custom inventory items from editor + int customItems = countCustomItems(myStats); //max limit of 6 custom items per entity. + + // count any inventory items set to default in edtior + int defaultItems = countDefaultItems(myStats); + + my->setHardcoreStats(*myStats); + + if ( myStats->getAttribute("SUMMONED_CREATURE") == "1" ) + { + // min 5, max 20 + myStats->HP = 40 + std::max(0, (myStats->LVL - 5)) * 5; //40 - 115 + myStats->MAXHP = myStats->HP; + myStats->OLDHP = myStats->HP; + myStats->STR = 5 + std::max(0, (myStats->LVL - 5)) * 1; //5-35 + myStats->DEX = myStats->LVL / 5; // 1-5 + myStats->CON = 5 + myStats->LVL; // 10-25 + myStats->PER = 5 + myStats->LVL / 4; // 6-10 + + if ( Entity* leader = my->monsterAllyGetPlayerLeader() ) + { + serverUpdateAllyStat(leader->skill[2], my->getUID(), myStats->LVL, myStats->HP, myStats->MAXHP, myStats->type); + } + } + } + } + + // body + Entity* entity = newEntity(1872, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[EARTH_ELEMENTAL][3][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][3][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][3][2]; + entity->behavior = &actEarthElementalLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + // eyes + entity = newEntity(1873, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[EARTH_ELEMENTAL][1][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][1][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][1][2]; + entity->behavior = &actEarthElementalLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + //fistleft + entity = newEntity(1875, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[EARTH_ELEMENTAL][6][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][6][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][6][2]; + entity->behavior = &actEarthElementalLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + //fistright + entity = newEntity(1875, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[EARTH_ELEMENTAL][6][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][6][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][6][2]; + entity->behavior = &actEarthElementalLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + //pebble1 + entity = newEntity(1874, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[EARTH_ELEMENTAL][9][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][9][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][9][2]; + entity->behavior = &actEarthElementalLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + //pebble1 + entity = newEntity(1874, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[EARTH_ELEMENTAL][9][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][9][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][9][2]; + entity->behavior = &actEarthElementalLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); + + //pebble1 + entity = newEntity(1874, 1, map.entities, nullptr); //Limb entity. + entity->sizex = 2; + entity->sizey = 2; + entity->skill[2] = my->getUID(); + entity->flags[PASSABLE] = true; + entity->flags[NOUPDATE] = true; + entity->yaw = my->yaw; + entity->z = 6; + entity->flags[USERFLAG2] = monsterChangesColorWhenAlly(myStats, my) ? my->flags[USERFLAG2] : false; + entity->focalx = limbs[EARTH_ELEMENTAL][9][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][9][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][9][2]; + entity->behavior = &actEarthElementalLimb; + entity->parent = my->getUID(); + node = list_AddNodeLast(&my->children); + node->element = entity; + node->deconstructor = &emptyDeconstructor; + node->size = sizeof(Entity*); + my->bodyparts.push_back(entity); +} + +#define EARTH_BODY 2 +#define EARTH_EYES 3 +#define EARTH_LEFTARM 4 +#define EARTH_RIGHTARM 5 +#define EARTH_PEBBLE1 6 +#define EARTH_PEBBLE2 7 +#define EARTH_PEBBLE3 8 +#define EARTH_LIMB_FSKILL_YAW entity->fskill[0] +#define EARTH_LIMB_FSKILL_PITCH entity->fskill[1] +#define EARTH_LIMB_FSKILL_ROLL entity->fskill[2] +#define EARTH_FLOAT_X body->fskill[3] +#define EARTH_FLOAT_Y body->fskill[4] +#define EARTH_FLOAT_Z body->fskill[5] +#define EARTH_FLOAT_ANIM body->fskill[6] +#define EARTH_PEBBLE_IDLE_ANIM entity->fskill[3] +#define EARTH_ATTACK_1 body->fskill[7] +#define EARTH_ATTACK_FLOAT body->fskill[8] +#define EARTH_ATTACK_2 body->fskill[9] +#define EARTH_ATTACK_3 body->fskill[10] +#define EARTH_DEFEND body->fskill[11] +#define EARTH_SPAWN_STATE body->skill[0] +#define EARTH_SPAWN_ANIM body->fskill[12] +#define EARTH_SPAWN_ANIM2 body->fskill[13] + +void earthElementalAnimate(Entity* my, Stat* myStats, double dist) +{ + node_t* node; + Entity* entity = nullptr; + Entity* head = nullptr; + int bodypart; + + my->flags[INVISIBLE] = true; // hide the "AI" bodypart + //my->flags[PASSABLE] = true; + + my->sizex = 4; + my->sizey = 4; + + my->focalx = limbs[EARTH_ELEMENTAL][0][0]; + my->focaly = limbs[EARTH_ELEMENTAL][0][1]; + my->focalz = limbs[EARTH_ELEMENTAL][0][2]; + if ( multiplayer != CLIENT ) + { + my->z = limbs[EARTH_ELEMENTAL][5][2]; + if ( !myStats->getEffectActive(EFF_LEVITATING) ) + { + myStats->setEffectActive(EFF_LEVITATING, 1); + myStats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; + } + + my->creatureHandleLiftZ(); + } + + //my->setEffect(EFF_STUNNED, true, -1, false); + //my->monsterLookDir = 0.0; + //my->yaw = 0.0; + //static ConsoleVariable cvar_ee_yaw("/ee_yaw", 0); + //static ConsoleVariable cvar_ee_pitch("/ee_pitch", 0); + //static ConsoleVariable cvar_ee_roll("/ee_roll", 0); + if ( enableDebugKeys && (svFlags & SV_FLAG_CHEATS) ) + { + if ( keystatus[SDLK_KP_5] ) + { + my->yaw += 0.05; + my->monsterLookDir = my->yaw; + } + if ( keystatus[SDLK_KP_4] ) + { + my->setEffect(EFF_STUNNED, true, -1, false); + } + if ( keystatus[SDLK_g] ) + { + keystatus[SDLK_g] = 0; + //MONSTER_ATTACK = mothGetAttackPose(my, MONSTER_POSE_MELEE_WINDUP1); + MONSTER_ATTACK = MONSTER_POSE_MELEE_WINDUP1;// mothGetAttackPose(my, MONSTER_POSE_MAGIC_WINDUP1); + MONSTER_ATTACKTIME = 0; + } + if ( keystatus[SDLK_h] ) + { + keystatus[SDLK_h] = 0; + //MONSTER_ATTACK = mothGetAttackPose(my, MONSTER_POSE_MELEE_WINDUP1); + MONSTER_ATTACK = MONSTER_POSE_MELEE_WINDUP3;// mothGetAttackPose(my, MONSTER_POSE_MAGIC_WINDUP1); + MONSTER_ATTACKTIME = 0; + } + if ( keystatus[SDLK_n] ) + { + keystatus[SDLK_n] = 0; + //MONSTER_ATTACK = mothGetAttackPose(my, MONSTER_POSE_MELEE_WINDUP1); + MONSTER_ATTACK = MONSTER_POSE_RANGED_WINDUP1;// mothGetAttackPose(my, MONSTER_POSE_MAGIC_WINDUP1); + MONSTER_ATTACKTIME = 0; + } + if ( keystatus[SDLK_y] ) + { + keystatus[SDLK_y] = 0; + //MONSTER_ATTACK = mothGetAttackPose(my, MONSTER_POSE_MELEE_WINDUP1); + MONSTER_ATTACK = MONSTER_POSE_MELEE_WINDUP2;// mothGetAttackPose(my, MONSTER_POSE_MAGIC_WINDUP1); + MONSTER_ATTACKTIME = 0; + } + // if ( keystatus[SDLK_h] ) + // { + // keystatus[SDLK_h] = 0; + // myStats->setEffectValueUnsafe(EFF_STUNNED, myStats->getEffectActive(EFF_STUNNED) ? 0 : 1); + // myStats->EFFECTS_TIMERS[EFF_STUNNED] = myStats->getEffectActive(EFF_STUNNED) ? -1 : 0; + // } + //} + } + + //Move bodyparts + Entity* body = nullptr; + for ( bodypart = 0, node = my->children.first; node != nullptr; node = node->next, ++bodypart ) + { + if ( bodypart < EARTH_BODY ) + { + continue; + } + + entity = (Entity*)node->element; + + if ( bodypart == EARTH_BODY ) + { + body = entity; + + if ( my->sprite == 1876 ) + { + // skip spawn anim + EARTH_SPAWN_ANIM = 0.0; + EARTH_SPAWN_STATE = 2; + EARTH_SPAWN_ANIM2 = 0.0; + } + if ( multiplayer != CLIENT ) + { + if ( EARTH_SPAWN_STATE < 2 ) + { + my->flags[PASSABLE] = true; + my->setEffect(EFF_STUNNED, true, 25, false); + } + else + { + if ( my->flags[PASSABLE] ) + { + my->flags[PASSABLE] = false; + serverUpdateEntityFlag(my, 12); + } + + if ( my->ticks == 2 * TICKS_PER_SECOND ) + { + my->flags[PASSABLE] = false; // reupdate for clients for new floor spawn + serverUpdateEntityFlag(my, 12); + } + } + } + //if ( keystatus[SDLK_u] ) + //{ + // keystatus[SDLK_u] = 0; + // //MONSTER_ATTACK = mothGetAttackPose(my, MONSTER_POSE_MELEE_WINDUP1); + // my->monsterDefend = my->monsterDefend ? 0 : 1; + // //EARTH_SPAWN_ANIM = 0.0; + // //EARTH_SPAWN_STATE = 0; + // //EARTH_SPAWN_ANIM2 = 0.0; + //} + } + + entity->x = my->x; + entity->y = my->y; + entity->z = my->z; + entity->yaw = my->yaw; + + entity->pitch = 0.0; + entity->roll = 0.0; + /*if ( *cvar_ee_yaw == bodypart ) + { + EARTH_LIMB_FSKILL_YAW += 0.05; + }*/ + entity->yaw += EARTH_LIMB_FSKILL_YAW; + /*if ( *cvar_ee_pitch == bodypart ) + { + EARTH_LIMB_FSKILL_PITCH += 0.05; + }*/ + entity->pitch = EARTH_LIMB_FSKILL_PITCH; + /*if ( *cvar_ee_roll == bodypart ) + { + EARTH_LIMB_FSKILL_ROLL += 0.05; + }*/ + entity->roll = EARTH_LIMB_FSKILL_ROLL; + + if ( bodypart == EARTH_BODY ) + { + if ( MONSTER_ATTACK == 0 || MONSTER_ATTACK == MONSTER_POSE_EARTH_ELEMENTAL_ROLL ) + { + { + real_t setpoint = 0.0; + if ( EARTH_DEFEND > 0.01 ) + { + setpoint = EARTH_DEFEND * PI / 16; + } + else + { + setpoint = -PI / 8; + } + + if ( EARTH_ATTACK_1 > setpoint ) + { + EARTH_ATTACK_1 -= limbs[EARTH_ELEMENTAL][16][0] / 2; + EARTH_ATTACK_1 = std::max(setpoint, EARTH_ATTACK_1); + } + else if ( EARTH_ATTACK_1 < setpoint ) + { + EARTH_ATTACK_1 += limbs[EARTH_ELEMENTAL][16][0] / 2; + EARTH_ATTACK_1 = std::min(setpoint, EARTH_ATTACK_1); + } + } + + if ( EARTH_ATTACK_2 > 0.0 ) + { + EARTH_ATTACK_2 -= limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_2 = std::max(0.0, EARTH_ATTACK_2); + } + else if ( EARTH_ATTACK_2 < 0.0 ) + { + EARTH_ATTACK_2 += limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_2 = std::min(0.0, EARTH_ATTACK_2); + } + + if ( EARTH_ATTACK_3 > 0.0 ) + { + EARTH_ATTACK_3 -= limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_3 = std::max(0.0, EARTH_ATTACK_3); + } + else if ( EARTH_ATTACK_3 < 0.0 ) + { + EARTH_ATTACK_3 += limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_3 = std::min(0.0, EARTH_ATTACK_3); + } + } + + if ( MONSTER_ATTACK == MONSTER_POSE_EARTH_ELEMENTAL_ROLL ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + EARTH_LIMB_FSKILL_YAW = 0.0; + if ( multiplayer != CLIENT ) + { + Entity* spellTimer = createParticleTimer(my, 35, -1); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL_ROLL; + } + } + else + { + EARTH_LIMB_FSKILL_YAW += -0.3; + EARTH_LIMB_FSKILL_YAW = std::max(EARTH_LIMB_FSKILL_YAW, -4 * PI); + + if ( MONSTER_ATTACKTIME >= 40 ) + { + EARTH_LIMB_FSKILL_YAW = 0.0; + MONSTER_ATTACK = 0; + } + } + } + + if ( !my->monsterDefend || MONSTER_ATTACK != 0 ) + { + if ( EARTH_DEFEND > 0.0 ) + { + EARTH_DEFEND -= limbs[EARTH_ELEMENTAL][16][0] * 3; + EARTH_DEFEND = std::max(0.0, EARTH_DEFEND); + } + else if ( EARTH_DEFEND < 0.0 ) + { + EARTH_DEFEND += limbs[EARTH_ELEMENTAL][16][0] * 3; + EARTH_DEFEND = std::min(0.0, EARTH_DEFEND); + } + } + else + { + EARTH_DEFEND += limbs[EARTH_ELEMENTAL][16][0]; + EARTH_DEFEND = std::min(1.0, EARTH_DEFEND); + } + + if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP1 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + EARTH_ATTACK_1 = 0.0; + EARTH_ATTACK_2 = 0.0; + EARTH_ATTACK_3 = 0.0; + EARTH_LIMB_FSKILL_YAW = 0.0; + } + else + { + if ( MONSTER_ATTACKTIME >= limbs[EARTH_ELEMENTAL][15][0] ) + { + //EARTH_ATTACK_1 += limbs[EARTH_ELEMENTAL][16][2]; + EARTH_LIMB_FSKILL_YAW += limbs[EARTH_ELEMENTAL][16][2]; + EARTH_LIMB_FSKILL_YAW = std::max(EARTH_LIMB_FSKILL_YAW, -4 * PI); + + EARTH_ATTACK_1 -= limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_1 = std::max(EARTH_ATTACK_1, -(real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + else + { + EARTH_ATTACK_1 += limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_1 = std::min(EARTH_ATTACK_1, (real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + + if ( MONSTER_ATTACKTIME >= 55 ) + { + MONSTER_ATTACK = 0; + EARTH_LIMB_FSKILL_YAW = 0.0; + } + else + { + if ( MONSTER_ATTACKTIME == 20 + || MONSTER_ATTACKTIME == 35 + || MONSTER_ATTACKTIME == 50 ) + { + if ( multiplayer != CLIENT ) + { + const Sint32 temp = MONSTER_ATTACKTIME; + const Sint32 temp2 = MONSTER_ATTACK; + my->attack(1, 0, nullptr); // slop + MONSTER_ATTACKTIME = temp; + MONSTER_ATTACK = temp2; + } + } + } + } + } + else if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP2 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + EARTH_ATTACK_1 = 0.0; + EARTH_ATTACK_2 = 0.0; + EARTH_ATTACK_3 = 0.0; + EARTH_LIMB_FSKILL_YAW = 0.0; + } + else + { + if ( MONSTER_ATTACKTIME >= limbs[EARTH_ELEMENTAL][18][2] ) + { + EARTH_ATTACK_3 = std::min(1.0, EARTH_ATTACK_3 + limbs[EARTH_ELEMENTAL][13][1]); + EARTH_ATTACK_2 = std::max(0.0, EARTH_ATTACK_2 - limbs[EARTH_ELEMENTAL][16][0]); + EARTH_ATTACK_1 -= limbs[EARTH_ELEMENTAL][16][0] * 4; + EARTH_ATTACK_1 = std::max(EARTH_ATTACK_1, -(real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + else if ( MONSTER_ATTACKTIME >= limbs[EARTH_ELEMENTAL][18][0] ) + { + EARTH_ATTACK_2 = std::min(1.0, EARTH_ATTACK_2 + limbs[EARTH_ELEMENTAL][13][1]); + EARTH_ATTACK_3 = std::max(-1.0, EARTH_ATTACK_3 - 0.1); + EARTH_ATTACK_1 += limbs[EARTH_ELEMENTAL][16][0] * 3; + EARTH_ATTACK_1 = std::min(EARTH_ATTACK_1, (real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + else + { + EARTH_ATTACK_2 = std::max(-1.0, EARTH_ATTACK_2 - 0.1); + EARTH_ATTACK_1 -= limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_1 = std::max(EARTH_ATTACK_1, -(real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + + if ( MONSTER_ATTACKTIME >= 55 ) + { + MONSTER_ATTACK = 0; + EARTH_LIMB_FSKILL_YAW = 0.0; + } + else + { + if ( MONSTER_ATTACKTIME == 20 || MONSTER_ATTACKTIME == 35 ) + { + if ( multiplayer != CLIENT ) + { + const Sint32 temp = MONSTER_ATTACKTIME; + const Sint32 temp2 = MONSTER_ATTACK; + my->attack(1, 0, nullptr); // slop + MONSTER_ATTACKTIME = temp; + MONSTER_ATTACK = temp2; + } + } + } + } + } + else if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP3 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + EARTH_ATTACK_1 = 0.0; + EARTH_ATTACK_2 = 0.0; + EARTH_ATTACK_3 = 0.0; + EARTH_LIMB_FSKILL_YAW = 0.0; + } + else + { + if ( MONSTER_ATTACKTIME >= limbs[EARTH_ELEMENTAL][18][2] ) + { + //EARTH_ATTACK_3 = std::min(1.0, EARTH_ATTACK_3 + limbs[EARTH_ELEMENTAL][13][1]); + EARTH_ATTACK_2 = std::max(0.0, EARTH_ATTACK_2 - limbs[EARTH_ELEMENTAL][16][0]); + EARTH_ATTACK_1 -= limbs[EARTH_ELEMENTAL][16][0] * 1; + EARTH_ATTACK_1 = std::max(EARTH_ATTACK_1, -(real_t)limbs[EARTH_ELEMENTAL][16][1] / 3); + } + else if ( MONSTER_ATTACKTIME >= limbs[EARTH_ELEMENTAL][18][0] ) + { + EARTH_ATTACK_2 = std::min(1.0, EARTH_ATTACK_2 + limbs[EARTH_ELEMENTAL][13][1]); + EARTH_ATTACK_1 += limbs[EARTH_ELEMENTAL][16][0] * 3; + EARTH_ATTACK_1 = std::min(EARTH_ATTACK_1, (real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + else + { + EARTH_ATTACK_2 = std::max(-1.0, EARTH_ATTACK_2 - 0.1); + EARTH_ATTACK_1 -= limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_1 = std::max(EARTH_ATTACK_1, -(real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + + if ( MONSTER_ATTACKTIME >= 40 ) + { + MONSTER_ATTACK = 0; + EARTH_LIMB_FSKILL_YAW = 0.0; + } + else + { + if ( MONSTER_ATTACKTIME == 20 ) + { + if ( multiplayer != CLIENT ) + { + const Sint32 temp = MONSTER_ATTACKTIME; + const Sint32 temp2 = MONSTER_ATTACK; + my->attack(1, 0, nullptr); // slop + MONSTER_ATTACKTIME = temp; + MONSTER_ATTACK = temp2; + } + } + } + } + } + else if ( MONSTER_ATTACK == MONSTER_POSE_RANGED_WINDUP1 ) + { + if ( MONSTER_ATTACKTIME == 0 ) + { + EARTH_ATTACK_1 = 0.0; + EARTH_ATTACK_2 = 0.0; + EARTH_ATTACK_3 = 0.0; + EARTH_LIMB_FSKILL_YAW = 0.0; + } + else + { + if ( MONSTER_ATTACKTIME >= limbs[EARTH_ELEMENTAL][18][2] ) + { + //EARTH_ATTACK_3 = std::min(1.0, EARTH_ATTACK_3 + limbs[EARTH_ELEMENTAL][13][1]); + EARTH_ATTACK_3 = std::max(0.0, EARTH_ATTACK_3 - limbs[EARTH_ELEMENTAL][16][0]); + EARTH_ATTACK_1 += limbs[EARTH_ELEMENTAL][16][0] * 1; + EARTH_ATTACK_1 = std::min(EARTH_ATTACK_1, (real_t)limbs[EARTH_ELEMENTAL][16][1] / 3); + } + else if ( MONSTER_ATTACKTIME >= limbs[EARTH_ELEMENTAL][18][0] ) + { + EARTH_ATTACK_3 = std::min(1.0, EARTH_ATTACK_3 + limbs[EARTH_ELEMENTAL][13][1]); + EARTH_ATTACK_1 -= limbs[EARTH_ELEMENTAL][16][0] * 3; + EARTH_ATTACK_1 = std::max(EARTH_ATTACK_1, -(real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + else + { + EARTH_ATTACK_3 = std::max(-1.0, EARTH_ATTACK_3 - 0.1); + EARTH_ATTACK_1 += limbs[EARTH_ELEMENTAL][16][0]; + EARTH_ATTACK_1 = std::min(EARTH_ATTACK_1, (real_t)limbs[EARTH_ELEMENTAL][16][1]); + } + + if ( MONSTER_ATTACKTIME >= 40 ) + { + MONSTER_ATTACK = 0; + EARTH_LIMB_FSKILL_YAW = 0.0; + } + else + { + if ( MONSTER_ATTACKTIME == 20 ) + { + if ( multiplayer != CLIENT ) + { + const Sint32 temp = MONSTER_ATTACKTIME; + const Sint32 temp2 = MONSTER_ATTACK; + my->attack(1, 0, nullptr); // slop + MONSTER_ATTACKTIME = temp; + MONSTER_ATTACK = temp2; + } + } + } + } + } + } + + switch ( bodypart ) + { + case EARTH_BODY: + { + entity->x += limbs[EARTH_ELEMENTAL][4][0] * cos(my->yaw) + limbs[EARTH_ELEMENTAL][4][1] * cos(my->yaw + PI / 2); + entity->y += limbs[EARTH_ELEMENTAL][4][0] * sin(my->yaw) + limbs[EARTH_ELEMENTAL][4][1] * sin(my->yaw + PI / 2); + entity->z += limbs[EARTH_ELEMENTAL][4][2]; + entity->focalx = limbs[EARTH_ELEMENTAL][3][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][3][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][3][2]; + + if ( EARTH_SPAWN_STATE >= 0 && EARTH_SPAWN_STATE <= 1 ) + { + EARTH_FLOAT_X = 0.0; + EARTH_FLOAT_Y = 0.0; + + real_t targetZ = 8.0; + real_t startZ = 64.0; + EARTH_FLOAT_Z = (targetZ - startZ) + EARTH_SPAWN_ANIM; + if ( EARTH_SPAWN_STATE == 0 ) + { + EARTH_SPAWN_ANIM2 = std::min(EARTH_SPAWN_ANIM2 + .1, 3.0); + } + else + { + EARTH_SPAWN_ANIM2 = std::min(EARTH_SPAWN_ANIM2 + .04, 3.0); + } + EARTH_SPAWN_ANIM = std::min(startZ, EARTH_SPAWN_ANIM + EARTH_SPAWN_ANIM2); + + EARTH_LIMB_FSKILL_PITCH += 0.04; + EARTH_LIMB_FSKILL_ROLL += 0.04; + + if ( EARTH_SPAWN_ANIM >= startZ ) + { + EARTH_SPAWN_STATE += 1; // bounce + EARTH_SPAWN_ANIM2 = -(EARTH_SPAWN_ANIM2 / 4); + if ( EARTH_SPAWN_STATE == 1 ) + { + if ( multiplayer != CLIENT && myStats ) + { + int damage = getSpellEffectDurationFromID(SPELL_EARTH_ELEMENTAL, my, nullptr, my); + damage += statGetCON(myStats, my); + createSpellExplosionArea(SPELL_EARTH_ELEMENTAL, my, my->x, my->y, my->z, 16.0, damage, my); + playSoundEntity(my, 181, 128); + } + } + else if ( EARTH_SPAWN_STATE == 2 ) + { + EARTH_SPAWN_ANIM = 1.0; + EARTH_SPAWN_ANIM2 = 0.0; + + EARTH_LIMB_FSKILL_PITCH = fmod(EARTH_LIMB_FSKILL_PITCH, 2 * PI); + while ( EARTH_LIMB_FSKILL_PITCH >= 2 * PI ) + { + EARTH_LIMB_FSKILL_PITCH -= 2 * PI; + } + while ( EARTH_LIMB_FSKILL_PITCH < 0 ) + { + EARTH_LIMB_FSKILL_PITCH += 2 * PI; + } + EARTH_LIMB_FSKILL_ROLL = fmod(EARTH_LIMB_FSKILL_ROLL, 2 * PI); + while ( EARTH_LIMB_FSKILL_ROLL >= 2 * PI ) + { + EARTH_LIMB_FSKILL_ROLL -= 2 * PI; + } + while ( EARTH_LIMB_FSKILL_ROLL < 0 ) + { + EARTH_LIMB_FSKILL_ROLL += 2 * PI; + } + } + } + + entity->x += 2.0 * cos(my->yaw); + entity->y += 2.0 * sin(my->yaw); + } + else + { + EARTH_SPAWN_ANIM = std::max(0.0, EARTH_SPAWN_ANIM - 0.05); + entity->x += EARTH_SPAWN_ANIM * 2.0 * cos(my->yaw); + entity->y += EARTH_SPAWN_ANIM * 2.0 * sin(my->yaw); + if ( EARTH_LIMB_FSKILL_ROLL > PI ) + { + real_t diff = std::max(0.05, (2 * PI - EARTH_LIMB_FSKILL_ROLL) / 10); + EARTH_LIMB_FSKILL_ROLL = std::min(2 * PI, EARTH_LIMB_FSKILL_ROLL + diff); + } + else + { + real_t diff = std::max(0.05, (EARTH_LIMB_FSKILL_ROLL) / 10); + EARTH_LIMB_FSKILL_ROLL = std::max(0.0, EARTH_LIMB_FSKILL_ROLL - diff); + } + + EARTH_LIMB_FSKILL_PITCH = fmod(EARTH_LIMB_FSKILL_PITCH, 2 * PI); + while ( EARTH_LIMB_FSKILL_PITCH >= 2 * PI ) + { + EARTH_LIMB_FSKILL_PITCH -= 2 * PI; + } + while ( EARTH_LIMB_FSKILL_PITCH < 0 ) + { + EARTH_LIMB_FSKILL_PITCH += 2 * PI; + } + if ( EARTH_LIMB_FSKILL_PITCH > PI ) + { + real_t diff = std::max(0.05, (2 * PI - EARTH_LIMB_FSKILL_PITCH) / 10); + EARTH_LIMB_FSKILL_PITCH = std::min(2 * PI, EARTH_LIMB_FSKILL_PITCH + diff); + } + else + { + real_t diff = std::max(0.05, (EARTH_LIMB_FSKILL_PITCH) / 10); + EARTH_LIMB_FSKILL_PITCH = std::max(0.0, EARTH_LIMB_FSKILL_PITCH - diff); + } + + real_t spawnRate = std::max(0.0, (1.0 - EARTH_SPAWN_ANIM)); + + real_t zAngle = EARTH_FLOAT_ANIM * limbs[EARTH_ELEMENTAL][11][2]; + zAngle = fmod(zAngle, 2 * PI); + while ( zAngle >= 2 * PI ) + { + zAngle -= 2 * PI; + } + while ( zAngle < 0 ) + { + zAngle += 2 * PI; + } + if ( zAngle >= PI / 2 ) + { + EARTH_FLOAT_ANIM += spawnRate * 0.2; + } + else + { + EARTH_FLOAT_ANIM += spawnRate * 0.1; + } + EARTH_FLOAT_X = spawnRate * limbs[EARTH_ELEMENTAL][12][0] * sin(EARTH_FLOAT_ANIM * limbs[EARTH_ELEMENTAL][11][0]) * cos(my->yaw); + EARTH_FLOAT_Y = spawnRate * limbs[EARTH_ELEMENTAL][12][1] * sin(EARTH_FLOAT_ANIM * limbs[EARTH_ELEMENTAL][11][0]) * sin(my->yaw); + + EARTH_FLOAT_Z = EARTH_SPAWN_ANIM * 8.0; + EARTH_FLOAT_Z += spawnRate * limbs[EARTH_ELEMENTAL][12][2] * sin(zAngle); + } + + real_t pitchAngle = EARTH_FLOAT_ANIM * 1.0 * limbs[EARTH_ELEMENTAL][11][2] + PI / 4; + while ( pitchAngle >= 2 * PI ) + { + pitchAngle -= 2 * PI; + } + while ( pitchAngle < 0 ) + { + pitchAngle += 2 * PI; + } + entity->yaw += sin(EARTH_ATTACK_1); + entity->pitch += limbs[EARTH_ELEMENTAL][13][0] * sin(pitchAngle); + + entity->x += EARTH_FLOAT_X; + entity->y += EARTH_FLOAT_Y; + entity->z += EARTH_FLOAT_Z; + + if ( EARTH_DEFEND > 0.0 ) + { + entity->x -= EARTH_DEFEND * cos(my->yaw); + entity->y -= EARTH_DEFEND * sin(my->yaw); + entity->pitch += 0.25 * sin(EARTH_DEFEND * PI / 2); + } + + + if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP1 || MONSTER_ATTACK == MONSTER_POSE_EARTH_ELEMENTAL_ROLL ) + { + if ( EARTH_LIMB_FSKILL_YAW > -3 * PI ) + { + EARTH_ATTACK_FLOAT = sin((PI / 2) * EARTH_LIMB_FSKILL_YAW / (-3 * PI)); + } + else + { + EARTH_ATTACK_FLOAT = sin((PI / 2) * (-4 * PI - EARTH_LIMB_FSKILL_YAW) / -PI); + } + } + EARTH_ATTACK_FLOAT = -2.0 * sin(EARTH_ATTACK_1 / 2); + entity->z -= EARTH_ATTACK_FLOAT; + break; + } + case EARTH_EYES: + { + entity->x += limbs[EARTH_ELEMENTAL][2][0] * cos(my->yaw) + limbs[EARTH_ELEMENTAL][2][1] * cos(my->yaw + PI / 2); + entity->y += limbs[EARTH_ELEMENTAL][2][0] * sin(my->yaw) + limbs[EARTH_ELEMENTAL][2][1] * sin(my->yaw + PI / 2); + entity->z += limbs[EARTH_ELEMENTAL][2][2]; + entity->focalx = limbs[EARTH_ELEMENTAL][1][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][1][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][1][2]; + + if ( body ) + { + entity->x += EARTH_FLOAT_X; + entity->y += EARTH_FLOAT_Y; + + if ( EARTH_SPAWN_STATE <= 1 ) + { + entity->z += EARTH_FLOAT_Z; + entity->z = std::min(7.5, entity->z); + entity->x += 4.0 * cos(my->yaw); + entity->y += 4.0 * sin(my->yaw); + } + else + { + entity->z += 7.5 * EARTH_SPAWN_ANIM; + entity->x += 4.0 * EARTH_SPAWN_ANIM * cos(my->yaw); + entity->y += 4.0 * EARTH_SPAWN_ANIM * sin(my->yaw); + + //entity->roll = sin(EARTH_SPAWN_ANIM * PI); + } + + real_t zAngle = EARTH_FLOAT_ANIM * limbs[EARTH_ELEMENTAL][11][2] + 3 * PI / 4; + zAngle = fmod(zAngle, 2 * PI); + while ( zAngle >= 2 * PI ) + { + zAngle -= 2 * PI; + } + while ( zAngle < 0 ) + { + zAngle += 2 * PI; + } + real_t zMag = 0.5 * sin(zAngle); + entity->z -= zMag; + + if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP1 || MONSTER_ATTACK == MONSTER_POSE_EARTH_ELEMENTAL_ROLL ) + { + if ( MONSTER_ATTACKTIME >= limbs[EARTH_ELEMENTAL][15][0] ) + { + entity->fskill[7] = std::min(1.0, entity->fskill[7] + 0.1); // extra raise + } + } + else + { + entity->fskill[7] = std::max(0.0, entity->fskill[7] - 0.1); + } + entity->z -= EARTH_ATTACK_FLOAT; + entity->z -= 1.0 * sin(entity->fskill[7] * PI / 2); + } + break; + } + case EARTH_LEFTARM: + { + real_t yaw = my->yaw; + if ( body ) + { + yaw = body->yaw; + if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP1 ) + { + real_t bodyRotate = fmod(body->fskill[0], 2 * PI); + while ( bodyRotate >= 2 * PI ) + { + bodyRotate -= 2 * PI; + } + while ( bodyRotate < 0 ) + { + bodyRotate += 2 * PI; + } + if ( bodyRotate < PI ) + { + entity->x += 4.0 * sin(bodyRotate) * cos(my->yaw); + entity->y += 4.0 * sin(bodyRotate) * sin(my->yaw); + } + } + if ( EARTH_ATTACK_2 < 0.0 ) + { + entity->x += 2.0 * sin(EARTH_ATTACK_2 * PI / 2) * cos(my->yaw); + entity->y += 2.0 * sin(EARTH_ATTACK_2 * PI / 2) * sin(my->yaw); + } + else + { + entity->x += 4.0 * std::max(0.0, sin(EARTH_ATTACK_2 * PI / 2)) * cos(my->yaw); + entity->y += 4.0 * std::max(0.0, sin(EARTH_ATTACK_2 * PI / 2)) * sin(my->yaw); + } + } + + real_t sideDist = (1.0 - 0.5 * std::max(0.0, EARTH_ATTACK_2)); + sideDist = std::max(0.25, sideDist - EARTH_DEFEND); + sideDist *= limbs[EARTH_ELEMENTAL][7][1]; + entity->x += 2.0 * EARTH_DEFEND * cos(yaw); + entity->y += 2.0 * EARTH_DEFEND * sin(yaw); + + if ( EARTH_SPAWN_STATE <= 1 ) + { + entity->x += 2.0 * cos(my->yaw + 1 * PI / 6); + entity->y += 2.0 * sin(my->yaw + 1 * PI / 6); + } + else + { + entity->x += EARTH_SPAWN_ANIM * cos(my->yaw + 1 * PI / 6); + entity->y += EARTH_SPAWN_ANIM * sin(my->yaw + 1 * PI / 6); + } + + entity->x += limbs[EARTH_ELEMENTAL][7][0] * cos(yaw) + sideDist * cos(yaw + PI / 2); + entity->y += limbs[EARTH_ELEMENTAL][7][0] * sin(yaw) + sideDist * sin(yaw + PI / 2); + entity->z += limbs[EARTH_ELEMENTAL][7][2]; + entity->focalx = limbs[EARTH_ELEMENTAL][6][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][6][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][6][2]; + + if ( body ) + { + entity->x += EARTH_FLOAT_X; + entity->y += EARTH_FLOAT_Y; + entity->z += EARTH_FLOAT_Z; + entity->pitch = body->pitch; + + real_t pitchAngle = EARTH_FLOAT_ANIM * 1.0 * limbs[EARTH_ELEMENTAL][11][2] + PI / 2; + while ( pitchAngle >= 2 * PI ) + { + pitchAngle -= 2 * PI; + } + while ( pitchAngle < 0 ) + { + pitchAngle += 2 * PI; + } + entity->x += 1.0 * sin(-pitchAngle) * cos(my->yaw); + entity->y += 1.0 * sin(-pitchAngle) * sin(my->yaw); + entity->z += 0.5 * cos(-pitchAngle); + + entity->z -= EARTH_ATTACK_FLOAT; + } + break; + } + case EARTH_RIGHTARM: + { + real_t yaw = my->yaw; + if ( body ) + { + yaw = body->yaw; + if ( MONSTER_ATTACK == MONSTER_POSE_MELEE_WINDUP1 ) + { + real_t bodyRotate = fmod(body->fskill[0], 2 * PI); + while ( bodyRotate >= 2 * PI ) + { + bodyRotate -= 2 * PI; + } + while ( bodyRotate < 0 ) + { + bodyRotate += 2 * PI; + } + if ( bodyRotate >= PI ) + { + entity->x -= 4.0 * sin(bodyRotate) * cos(my->yaw); + entity->y -= 4.0 * sin(bodyRotate) * sin(my->yaw); + } + } + + if ( EARTH_ATTACK_3 < 0.0 ) + { + entity->x += 2.0 * sin(EARTH_ATTACK_3 * PI / 2) * cos(my->yaw); + entity->y += 2.0 * sin(EARTH_ATTACK_3 * PI / 2) * sin(my->yaw); + } + else + { + entity->x += 4.0 * std::max(0.0, sin(EARTH_ATTACK_3 * PI / 2)) * cos(my->yaw); + entity->y += 4.0 * std::max(0.0, sin(EARTH_ATTACK_3 * PI / 2)) * sin(my->yaw); + } + } + + real_t sideDist = (1.0 - 0.5 * std::max(0.0, EARTH_ATTACK_3)); + sideDist = std::max(0.25, sideDist - EARTH_DEFEND); + sideDist *= limbs[EARTH_ELEMENTAL][8][1]; + entity->x += 2.0 * EARTH_DEFEND * cos(yaw); + entity->y += 2.0 * EARTH_DEFEND * sin(yaw); + + if ( EARTH_SPAWN_STATE <= 1 ) + { + entity->x += 2.0 * cos(my->yaw + 4 * PI / 6); + entity->y += 2.0 * sin(my->yaw + 4 * PI / 6); + } + else + { + entity->x += EARTH_SPAWN_ANIM * cos(my->yaw + 4 * PI / 6); + entity->y += EARTH_SPAWN_ANIM * sin(my->yaw + 4 * PI / 6); + } + + entity->x += limbs[EARTH_ELEMENTAL][8][0] * cos(yaw) + sideDist * cos(yaw + PI / 2); + entity->y += limbs[EARTH_ELEMENTAL][8][0] * sin(yaw) + sideDist * sin(yaw + PI / 2); + entity->z += limbs[EARTH_ELEMENTAL][8][2]; + entity->focalx = limbs[EARTH_ELEMENTAL][6][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][6][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][6][2]; + + if ( body ) + { + entity->x += EARTH_FLOAT_X; + entity->y += EARTH_FLOAT_Y; + entity->z += EARTH_FLOAT_Z; + entity->pitch = body->pitch; + + real_t pitchAngle = EARTH_FLOAT_ANIM * 1.0 * limbs[EARTH_ELEMENTAL][11][2] + PI / 8 + PI / 2; + while ( pitchAngle >= 2 * PI ) + { + pitchAngle -= 2 * PI; + } + while ( pitchAngle < 0 ) + { + pitchAngle += 2 * PI; + } + entity->x += 1.0 * sin(-pitchAngle) * cos(my->yaw); + entity->y += 1.0 * sin(-pitchAngle) * sin(my->yaw); + entity->z += 0.75 * cos(-pitchAngle); + + entity->z -= EARTH_ATTACK_FLOAT; + } + break; + } + case EARTH_PEBBLE1: + { + entity->x += limbs[EARTH_ELEMENTAL][10][0] * cos(my->yaw) + limbs[EARTH_ELEMENTAL][10][1] * cos(my->yaw + PI / 2); + entity->y += limbs[EARTH_ELEMENTAL][10][0] * sin(my->yaw) + limbs[EARTH_ELEMENTAL][10][1] * sin(my->yaw + PI / 2); + entity->z += limbs[EARTH_ELEMENTAL][10][2]; + entity->focalx = limbs[EARTH_ELEMENTAL][9][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][9][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][9][2]; + + if ( body ) + { + entity->x += EARTH_FLOAT_X; + entity->y += EARTH_FLOAT_Y; + entity->z += EARTH_FLOAT_Z; + entity->pitch = body->pitch; + } + + if ( EARTH_SPAWN_STATE <= 1 ) + { + entity->x += 2.0 * cos(my->yaw + 3 * PI / 6); + entity->y += 2.0 * sin(my->yaw + 3 * PI / 6); + entity->z = std::min(7.5, entity->z); + } + else + { + EARTH_PEBBLE_IDLE_ANIM += MONSTER_ATTACK > 0 ? 0.2 : 0.1; + entity->x += EARTH_SPAWN_ANIM * cos(my->yaw + 3 * PI / 6); + entity->y += EARTH_SPAWN_ANIM * sin(my->yaw + 3 * PI / 6); + entity->z += 2.0; + } + entity->x += 1.0 * cos(EARTH_PEBBLE_IDLE_ANIM); + entity->y += 1.0 * sin(EARTH_PEBBLE_IDLE_ANIM); + break; + } + case EARTH_PEBBLE2: + { + entity->x += limbs[EARTH_ELEMENTAL][10][0] * cos(my->yaw) + limbs[EARTH_ELEMENTAL][10][1] * cos(my->yaw + PI / 2); + entity->y += limbs[EARTH_ELEMENTAL][10][0] * sin(my->yaw) + limbs[EARTH_ELEMENTAL][10][1] * sin(my->yaw + PI / 2); + entity->z += limbs[EARTH_ELEMENTAL][10][2]; + entity->focalx = limbs[EARTH_ELEMENTAL][9][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][9][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][9][2]; + + if ( body ) + { + entity->x += EARTH_FLOAT_X; + entity->y += EARTH_FLOAT_Y; + entity->z += EARTH_FLOAT_Z; + entity->pitch = body->pitch + PI / 32; + } + + if ( EARTH_SPAWN_STATE <= 1 ) + { + entity->x += 2.0 * cos(my->yaw + 5 * PI / 6); + entity->y += 2.0 * sin(my->yaw + 5 * PI / 6); + entity->z = std::min(7.5, entity->z); + } + else + { + EARTH_PEBBLE_IDLE_ANIM += MONSTER_ATTACK > 0 ? 0.4 : 0.2; + entity->z += 1.0; + entity->x += EARTH_SPAWN_ANIM * cos(my->yaw + 5 * PI / 6); + entity->y += EARTH_SPAWN_ANIM * sin(my->yaw + 5 * PI / 6); + } + entity->x += 1.5 * (cos(my->yaw + PI / 2 + EARTH_PEBBLE_IDLE_ANIM)); + entity->y += 1.5 * (sin(my->yaw + PI / 2 + EARTH_PEBBLE_IDLE_ANIM)); + break; + } + case EARTH_PEBBLE3: + { + entity->x += limbs[EARTH_ELEMENTAL][10][0] * cos(my->yaw) + limbs[EARTH_ELEMENTAL][10][1] * cos(my->yaw + PI / 2); + entity->y += limbs[EARTH_ELEMENTAL][10][0] * sin(my->yaw) + limbs[EARTH_ELEMENTAL][10][1] * sin(my->yaw + PI / 2); + entity->z += limbs[EARTH_ELEMENTAL][10][2]; + entity->focalx = limbs[EARTH_ELEMENTAL][9][0]; + entity->focaly = limbs[EARTH_ELEMENTAL][9][1]; + entity->focalz = limbs[EARTH_ELEMENTAL][9][2]; + + if ( body ) + { + entity->x += EARTH_FLOAT_X; + entity->y += EARTH_FLOAT_Y; + entity->z += EARTH_FLOAT_Z; + entity->pitch = body->pitch - PI / 32; + } + + if ( EARTH_SPAWN_STATE <= 1 ) + { + entity->z = std::min(7.5, entity->z); + entity->x += 2.0 * cos(my->yaw + 2 * PI / 6); + entity->y += 2.0 * sin(my->yaw + 2 * PI / 6); + } + else + { + EARTH_PEBBLE_IDLE_ANIM += MONSTER_ATTACK > 0 ? 0.3 : 0.15; + entity->x += EARTH_SPAWN_ANIM * cos(my->yaw + 2 * PI / 6); + entity->y += EARTH_SPAWN_ANIM * sin(my->yaw + 2 * PI / 6); + } + entity->x += 1.0 * (cos(my->yaw + EARTH_PEBBLE_IDLE_ANIM)); + entity->y += 1.0 * (sin(my->yaw + EARTH_PEBBLE_IDLE_ANIM)); + entity->x += 2.0 * (cos(my->yaw + PI / 2 + EARTH_PEBBLE_IDLE_ANIM)); + entity->y += 2.0 * (sin(my->yaw + PI / 2 + EARTH_PEBBLE_IDLE_ANIM)); + break; + } + default: + break; + } + } + + if ( MONSTER_ATTACK > 0 ) + { + MONSTER_ATTACKTIME++; + } + else if ( MONSTER_ATTACK == 0 ) + { + MONSTER_ATTACKTIME = 0; + } + else + { + // do nothing, don't reset attacktime or increment it. + } +} \ No newline at end of file diff --git a/src/monster_troll.cpp b/src/monster_troll.cpp index c40b3732b..91e34787f 100644 --- a/src/monster_troll.cpp +++ b/src/monster_troll.cpp @@ -91,9 +91,9 @@ void initTroll(Entity* my, Stat* myStats) } // random effects - if ( rng.rand() % 4 == 0 ) + if ( rng.rand() % 4 == 0 && myStats->getAttribute("spawn_no_sleep") == "" ) { - myStats->EFFECTS[EFF_ASLEEP] = true; + myStats->setEffectActive(EFF_ASLEEP, 1); myStats->EFFECTS_TIMERS[EFF_ASLEEP] = 1800 + rng.rand() % 3600; } @@ -129,7 +129,7 @@ void initTroll(Entity* my, Stat* myStats) int i = 1 + rng.rand() % 3; for ( c = 0; c < i; c++ ) { - Category cat = static_cast(rng.rand() % (NUMCATEGORIES - 1)); + Category cat = static_cast(rng.rand() % (Category::CATEGORY_MAX - 2)); newItem(static_cast(itemLevelCurve(cat, 0, currentlevel + 10, rng)), static_cast(1 + rng.rand() % 4), -1 + rng.rand() % 3, 1, rng.rand(), false, &myStats->inventory); } } @@ -279,7 +279,7 @@ void trollMoveBodyparts(Entity* my, Stat* myStats, double dist) // set invisibility //TODO: isInvisible()? if ( multiplayer != CLIENT ) { - if ( myStats->EFFECTS[EFF_INVISIBLE] == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -332,7 +332,7 @@ void trollMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 1.5; } @@ -340,6 +340,7 @@ void trollMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->z = -1.5; } + my->creatureHandleLiftZ(); } //Move bodyparts diff --git a/src/monster_vampire.cpp b/src/monster_vampire.cpp index 832772153..abe3587e8 100644 --- a/src/monster_vampire.cpp +++ b/src/monster_vampire.cpp @@ -114,7 +114,7 @@ void initVampire(Entity* my, Stat* myStats) strcpy(myStats->name, MonsterData_t::getSpecialNPCName(*myStats).c_str()); my->sprite = MonsterData_t::getSpecialNPCBaseModel(*myStats); myStats->sex = MALE; - myStats->EFFECTS[EFF_VAMPIRICAURA] = true; + myStats->setEffectActive(EFF_VAMPIRICAURA, 1); myStats->EFFECTS_TIMERS[EFF_VAMPIRICAURA] = -1; my->setEffect(EFF_MAGICRESIST, true, -1, true); //-1 duration, never expires. } @@ -499,7 +499,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) wearingring = true; } } - if ( myStats->EFFECTS[EFF_INVISIBLE] == true || wearingring == true ) + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring == true ) { my->flags[INVISIBLE] = true; my->flags[BLOCKSIGHT] = false; @@ -552,7 +552,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) } // sleeping - if ( myStats->EFFECTS[EFF_ASLEEP] ) + if ( myStats->getEffectActive(EFF_ASLEEP) ) { my->z = 1.5; my->pitch = PI / 4; @@ -572,6 +572,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) { my->z -= 1; // floating } + my->creatureHandleLiftZ(); } Entity* shieldarm = nullptr; @@ -729,7 +730,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) playSoundEntityLocal(my, MONSTER_SPOTSND, 128); if ( multiplayer != CLIENT ) { - myStats->EFFECTS[EFF_PARALYZED] = true; + myStats->setEffectActive(EFF_PARALYZED, 1); myStats->EFFECTS_TIMERS[EFF_PARALYZED] = 50; } } @@ -788,15 +789,21 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) { // torso case LIMB_HUMANOID_TORSO: + entity->scalex = 1.0; + entity->scaley = 1.0; + entity->scalez = 1.0; + entity->focalx = limbs[VAMPIRE][1][0]; + entity->focaly = limbs[VAMPIRE][1][1]; + entity->focalz = limbs[VAMPIRE][1][2]; if ( multiplayer != CLIENT ) { - if ( myStats->breastplate == nullptr ) + if ( myStats->breastplate == nullptr || !itemModel(myStats->breastplate, false, my) ) { entity->sprite = 438; } else { - entity->sprite = itemModel(myStats->breastplate); + entity->sprite = itemModel(myStats->breastplate, false, my); } if ( multiplayer == SERVER ) { @@ -1055,7 +1062,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_WEAPON: if ( multiplayer != CLIENT ) { - if ( myStats->weapon == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->weapon == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1128,7 +1135,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->handleQuiverThirdPersonModel(*myStats); } } - if ( myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1172,7 +1179,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) case LIMB_HUMANOID_CLOAK: if ( multiplayer != CLIENT ) { - if ( myStats->cloak == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->cloak == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1230,7 +1237,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) if ( multiplayer != CLIENT ) { entity->sprite = itemModel(myStats->helmet); - if ( myStats->helmet == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring ) //TODO: isInvisible()? + if ( myStats->helmet == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1283,17 +1290,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) entity->roll = PI / 2; if ( multiplayer != CLIENT ) { - bool hasSteelHelm = false; - /*if ( myStats->helmet ) - { - if ( myStats->helmet->type == STEEL_HELM - || myStats->helmet->type == CRYSTAL_HELM - || myStats->helmet->type == ARTIFACT_HELM ) - { - hasSteelHelm = true; - } - }*/ - if ( myStats->mask == nullptr || myStats->EFFECTS[EFF_INVISIBLE] || wearingring || hasSteelHelm ) //TODO: isInvisible()? + if ( myStats->mask == nullptr || myStats->getEffectActive(EFF_INVISIBLE) || wearingring ) //TODO: isInvisible()? { entity->flags[INVISIBLE] = true; } @@ -1359,7 +1356,7 @@ void vampireMoveBodyparts(Entity* my, Stat* myStats, double dist) my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); } - else if ( EquipmentModelOffsets.modelOffsetExists(VAMPIRE, entity->sprite) ) + else if ( EquipmentModelOffsets.modelOffsetExists(VAMPIRE, entity->sprite, my->sprite) ) { my->setHelmetLimbOffset(entity); my->setHelmetLimbOffsetWithMask(helmet, entity); @@ -1454,7 +1451,7 @@ void Entity::vampireChooseWeapon(const Entity* target, double dist) { node_t* node = nullptr; bool chooseAura = false; - if ( !myStats->EFFECTS[EFF_VAMPIRICAURA] ) + if ( !myStats->getEffectActive(EFF_VAMPIRICAURA) ) { if ( myStats->HP <= myStats->MAXHP * 0.4) { diff --git a/src/net.cpp b/src/net.cpp index 241636f3c..84d0382f0 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -508,7 +508,17 @@ void sendEntityUDP(Entity* entity, int c, bool guarantee) net_packet->data[27] = (Sint8)(entity->focalx * 8); net_packet->data[28] = (Sint8)(entity->focaly * 8); net_packet->data[29] = (Sint8)(entity->focalz * 8); - SDLNet_Write32(entity->skill[2], &net_packet->data[30]); + if ( entity->behavior == &actDeathGhost ) + { + Uint32 flags = entity->skill[2]; + flags |= ((entity->monsterSpecialState) & 0xFF) << 8; + flags |= ((entity->skill[10]) & 0xFFFF) << 16; + SDLNet_Write32(flags, &net_packet->data[30]); + } + else + { + SDLNet_Write32(entity->skill[2], &net_packet->data[30]); + } net_packet->data[34] = 0; net_packet->data[35] = 0; for (j = 0; j < 16; j++) @@ -825,7 +835,7 @@ Spawns misc particle effects for all clients -------------------------------------------------------------------------------*/ -void serverSpawnMiscParticles(Entity* entity, int particleType, int particleSprite, Uint32 optionalUid) +void serverSpawnMiscParticles(Entity* entity, int particleType, int particleSprite, Uint32 optionalUid, Uint32 duration, Uint32 optionalData) { int c; if ( multiplayer != SERVER ) @@ -843,9 +853,11 @@ void serverSpawnMiscParticles(Entity* entity, int particleType, int particleSpri net_packet->data[8] = particleType; SDLNet_Write16(particleSprite, &net_packet->data[9]); SDLNet_Write32(optionalUid, &net_packet->data[11]); + SDLNet_Write32(duration, &net_packet->data[15]); + SDLNet_Write32(optionalData, &net_packet->data[19]); + net_packet->len = 23; net_packet->address.host = net_clients[c - 1].host; net_packet->address.port = net_clients[c - 1].port; - net_packet->len = 16; sendPacketSafe(net_sock, -1, net_packet, c - 1); } } @@ -858,7 +870,8 @@ Spawns misc particle effects for all clients at given coordinates. -------------------------------------------------------------------------------*/ -void serverSpawnMiscParticlesAtLocation(Sint16 x, Sint16 y, Sint16 z, int particleType, int particleSprite) +void serverSpawnMiscParticlesAtLocation(Sint16 x, Sint16 y, Sint16 z, int particleType, + int particleSprite, Uint32 duration, Uint32 optionalData, Uint32 optionalUID) { int c; if ( multiplayer != SERVER ) @@ -877,9 +890,12 @@ void serverSpawnMiscParticlesAtLocation(Sint16 x, Sint16 y, Sint16 z, int partic SDLNet_Write16(z, &net_packet->data[8]); net_packet->data[10] = particleType; SDLNet_Write16(particleSprite, &net_packet->data[11]); + SDLNet_Write32(duration, &net_packet->data[13]); + SDLNet_Write32(optionalData, &net_packet->data[17]); + SDLNet_Write32(optionalUID, &net_packet->data[21]); + net_packet->len = 25; net_packet->address.host = net_clients[c - 1].host; net_packet->address.port = net_clients[c - 1].port; - net_packet->len = 14; sendPacketSafe(net_sock, -1, net_packet, c - 1); } } @@ -916,6 +932,32 @@ void serverUpdateEntityFlag(Entity* entity, int flag) } } +void serverUpdateMapTileFlag(Sint16 x, Sint16 y, int layer, Uint32 flagSet, Uint32 flagRemove) +{ + int c; + if ( multiplayer != SERVER ) + { + return; + } + for ( c = 1; c < MAXPLAYERS; c++ ) + { + if ( client_disconnected[c] || players[c]->isLocalPlayer() ) + { + continue; + } + strcpy((char*)net_packet->data, "MAPT"); + SDLNet_Write16(x, &net_packet->data[4]); + SDLNet_Write16(y, &net_packet->data[6]); + SDLNet_Write32(flagSet, &net_packet->data[8]); + SDLNet_Write32(flagRemove, &net_packet->data[12]); + net_packet->data[16] = layer; + net_packet->address.host = net_clients[c - 1].host; + net_packet->address.port = net_clients[c - 1].port; + net_packet->len = 17; + sendPacketSafe(net_sock, -1, net_packet, c - 1); + } +} + /*------------------------------------------------------------------------------- serverUpdateEffects @@ -943,38 +985,49 @@ void serverUpdateEffects(int player) } strcpy((char*)net_packet->data, "UPEF"); - net_packet->data[4] = 0; - net_packet->data[5] = 0; - net_packet->data[6] = 0; - net_packet->data[7] = 0; - net_packet->data[8] = 0; - net_packet->data[9] = 0; - net_packet->data[10] = 0; - net_packet->data[11] = 0; - - net_packet->data[12] = 0; - net_packet->data[13] = 0; - net_packet->data[14] = 0; - net_packet->data[15] = 0; - net_packet->data[16] = 0; - net_packet->data[17] = 0; - net_packet->data[18] = 0; - net_packet->data[19] = 0; + int numBytes = NUMEFFECTS / 8; + for ( int i = 0; i < numBytes; ++i ) + { + net_packet->data[4 + i] = 0; + net_packet->data[4 + numBytes + i] = 0; + } + + std::vector> effectStrengths; for (j = 0; j < NUMEFFECTS; j++) { - if ( stats[player]->EFFECTS[j] == true ) + Uint8 effectValue = stats[player]->getEffectActive(j); + if ( effectValue > 0 ) { net_packet->data[4 + j / 8] |= power(2, j - (j / 8) * 8); + if ( effectValue > 1 ) + { + // effect index, then value + effectStrengths.push_back(std::make_pair(static_cast(j & 0xFF), effectValue)); + } } if ( stats[player]->EFFECTS_TIMERS[j] < TICKS_PER_SECOND * 5 && stats[player]->EFFECTS_TIMERS[j] > 0 ) { // use these bits to denote if duration is low. - net_packet->data[12 + j / 8] |= power(2, j - (j / 8) * 8); + net_packet->data[4 + numBytes + j / 8] |= power(2, j - (j / 8) * 8); } } + + net_packet->data[4 + numBytes * 2] = (Uint8)effectStrengths.size(); + net_packet->len = 4 + numBytes * 2 + 1; + for ( auto& pair : effectStrengths ) + { + if ( net_packet->len + 1 >= NET_PACKET_SIZE ) + { + // no more room + break; + } + net_packet->data[net_packet->len + 0] = pair.first; + net_packet->data[net_packet->len + 1] = pair.second; + net_packet->len += 2; + } + net_packet->address.host = net_clients[player - 1].host; net_packet->address.port = net_clients[player - 1].port; - net_packet->len = 20; sendPacketSafe(net_sock, -1, net_packet, player - 1); } @@ -1161,6 +1214,35 @@ void serverUpdatePlayerGameplayStats(int player, int gameplayStat, int changeval gameStatistics[gameplayStat] |= shifted; } } + else if ( gameplayStat == STATISTICS_FLAVORTOWN ) + { + gameStatistics[gameplayStat] |= changeval; + } + else if ( gameplayStat == STATISTICS_BARDIC_INSPIRATION ) + { + if ( changeval == 0 ) + { + gameStatistics[gameplayStat] = 0; + } + else + { + gameStatistics[gameplayStat] += changeval; + } + } + else if ( gameplayStat == STATISTICS_PARRY_TANK ) + { + if ( changeval == 0 ) + { + if ( gameStatistics[gameplayStat] < 20 ) + { + gameStatistics[gameplayStat] = 0; + } + } + else + { + gameStatistics[gameplayStat] += changeval; + } + } else { gameStatistics[gameplayStat] += changeval; @@ -1364,7 +1446,7 @@ void serverUpdateAllyHP(int player, Uint32 uidToUpdate, int HP, int MAXHP, bool } } -void sendMinimapPing(Uint8 player, Uint8 x, Uint8 y, Uint8 pingType) +void sendMinimapPing(Uint8 player, Uint8 x, Uint8 y, Uint8 pingType, bool radius) { if ( multiplayer == CLIENT ) { @@ -1374,10 +1456,11 @@ void sendMinimapPing(Uint8 player, Uint8 x, Uint8 y, Uint8 pingType) net_packet->data[5] = x; net_packet->data[6] = y; net_packet->data[7] = pingType; + net_packet->data[8] = radius ? 1 : 0; net_packet->address.host = net_server.host; net_packet->address.port = net_server.port; - net_packet->len = 8; + net_packet->len = 9; sendPacket(net_sock, -1, net_packet, 0); } else @@ -1390,7 +1473,7 @@ void sendMinimapPing(Uint8 player, Uint8 x, Uint8 y, Uint8 pingType) } if ( players[c]->isLocalPlayer() ) { - minimapPingAdd(player, c, MinimapPing(ticks, player, x, y, false, (MinimapPing::PingType)pingType)); + minimapPingAdd(player, c, MinimapPing(ticks, player, x, y, radius, (MinimapPing::PingType)pingType)); continue; } @@ -1402,10 +1485,11 @@ void sendMinimapPing(Uint8 player, Uint8 x, Uint8 y, Uint8 pingType) net_packet->data[5] = x; net_packet->data[6] = y; net_packet->data[7] = pingType; + net_packet->data[8] = radius ? 1 : 0; net_packet->address.host = net_clients[c - 1].host; net_packet->address.port = net_clients[c - 1].port; - net_packet->len = 8; + net_packet->len = 9; sendPacketSafe(net_sock, -1, net_packet, c - 1); } } @@ -1684,7 +1768,7 @@ Entity* receiveEntity(Entity* entity) // because voxel-animated creatures (like rats and slimes) // need to move vertically for their animation. const auto monsterType = entity->getMonsterTypeFromSprite(); - const bool excludeForAnimation = + bool excludeForAnimation = !newentity && entity->behavior == &actMonster && (monsterType == SLIME || ((monsterType == RAT || monsterType == SCARAB) && @@ -1739,6 +1823,15 @@ Entity* receiveEntity(Entity* entity) } } + if ( entity->behavior == &actItem && entity->itemFollowUID != 0 ) + { + excludeForAnimation = true; + } + const bool excludeYaw = + entity->behavior == &actMagiclightBall + || (entity->behavior == &actLeafPile) + || (entity->behavior == &actItem && entity->itemFollowUID != 0); + entity->lastupdate = ticks; entity->lastupdateserver = (Uint32)SDLNet_Read32(&net_packet->data[36]); entity->setUID((int)SDLNet_Read32(&net_packet->data[4])); // remember who I am @@ -1754,7 +1847,7 @@ Entity* receiveEntity(Entity* entity) entity->scaley = ((Uint8)net_packet->data[19]) / 128.f; entity->scalez = ((Uint8)net_packet->data[20]) / 128.f; } - if ( newentity || entity->behavior != &actMagiclightBall ) + if ( newentity || !excludeYaw ) { entity->new_yaw = ((Sint16)SDLNet_Read16(&net_packet->data[21])) / 256.0; } @@ -1817,12 +1910,16 @@ void clientActions(Entity* entity) case 1167: case 1168: case 1169: + case 1631: case 1: entity->behavior = &actDoorFrame; break; case 2: entity->behavior = &actDoor; break; + case 1162: + entity->behavior = &actIronDoor; + break; case 3: entity->behavior = &actTorch; entity->flags[NOUPDATE] = 1; @@ -1860,6 +1957,7 @@ void clientActions(Entity* entity) entity->behavior = &actGate; break; case 216: + case 1790: entity->behavior = &actChestLid; break; case 254: @@ -1925,13 +2023,52 @@ void clientActions(Entity* entity) case 1484: entity->behavior = &actAssistShrine; break; + case 1622: + entity->behavior = &actCauldron; + break; + case 1617: + entity->behavior = &actWorkbench; + break; + case 1619: + case 1620: + entity->behavior = &actMailbox; + break; + case 1585: + case 1586: + case 1587: + case 1588: + case 1589: + case 1590: + case 1591: + case 1592: + // wall lock keys + entity->behavior = &actEmpty; + entity->flags[NOUPDATE] = true; + break; + case 1786: + entity->behavior = &actGreasePuddleSpawner; + entity->flags[NOUPDATE] = true; + break; + case 1151: + case 1152: + // wall buttons + entity->behavior = &actEmpty; + entity->flags[NOUPDATE] = true; + break; + case 1809: + entity->behavior = &actParticleDemesneDoor; + entity->flags[NOUPDATE] = true; + break; + case 1913: + entity->behavior = &actLeafPile; + break; case Player::Ghost_t::GHOST_MODEL_P1: case Player::Ghost_t::GHOST_MODEL_P2: case Player::Ghost_t::GHOST_MODEL_P3: case Player::Ghost_t::GHOST_MODEL_P4: case Player::Ghost_t::GHOST_MODEL_PX: // player ghosts - playernum = SDLNet_Read32(&net_packet->data[30]); + playernum = 0xFF & SDLNet_Read32(&net_packet->data[30]); if ( playernum >= 0 && playernum < MAXPLAYERS ) { if ( players[playernum] ) @@ -1951,6 +2088,16 @@ void clientActions(Entity* entity) entity->flags[PASSABLE] = true; entity->flags[INVISIBLE] = true; entity->flags[GENIUS] = true; + Uint32 specialFlags = (SDLNet_Read32(&net_packet->data[30]) >> 8) & 0xFFFFFF; + if ( (specialFlags & 0xFF) ) + { + entity->monsterSpecialState = (specialFlags & 0xFF); + } + if ( (specialFlags >> 8) & 0xFFFF ) + { + int cosmeticSprite = (specialFlags >> 8) & 0xFFFF; + entity->skill[10] = cosmeticSprite; + } entity->sizex = 2; entity->sizey = 2; } @@ -2025,6 +2172,10 @@ void clientActions(Entity* entity) case -16: entity->behavior = &actBoulder; break; + case -18: + entity->behavior = &actParticleFloorMagic; + entity->flags[NOUPDATE] = true; + break; default: if ( static_cast(c & 0xFF) == 17 ) { @@ -2033,6 +2184,63 @@ void clientActions(Entity* entity) entity->arrowDropOffEquipmentModifier = dropOffModifier - 8; entity->behavior = &actArrow; } + else if ( static_cast(c & 0xFF) == 19 ) + { + entity->particleTimerDuration = (c >> 8) & 0xFFF; + entity->particleTimerCountdownAction = (c >> 20) & 0xFF; + entity->behavior = &actParticleTimer; + } + else if ( static_cast(c & 0xFF) == 20 ) + { + entity->behavior = &actParticleFloorMagic; + entity->skill[2] = c; + floorMagicClientReceive(entity); + } + else if ( static_cast(c & 0xFF) == 21 ) + { + entity->behavior = &actParticleFloorMagic; + entity->skill[2] = c; + entity->flags[NOUPDATE] = true; + floorMagicClientReceive(entity); + } + else if ( static_cast(c & 0xFF) == 22 ) + { + entity->behavior = &actParticleWave; + entity->skill[2] = c; + entity->flags[NOUPDATE] = true; + particleWaveClientReceive(entity); + } + else if ( static_cast(c & 0xFF) == 23 ) + { + entity->behavior = &actWind; + entity->skill[2] = c; + entity->flags[NOUPDATE] = true; + particleWaveClientReceive(entity); + } + else if ( static_cast(c & 0xFF) == 24 ) + { + entity->behavior = &actRadiusMagic; + entity->skill[2] = c; + radiusMagicClientReceive(entity); + } + else if ( static_cast(c & 0xFF) == 25 ) + { + entity->behavior = &actColliderDecoration; + entity->skill[2] = c; + entity->flags[NOUPDATE] = true; + entity->colliderDamageTypes = (c >> 8) & 0xFF; + entity->colliderSpellEvent = (c >> 16) & 0xFF; + Entity::colliderAssignProperties(entity, false, &map); + } + else if ( static_cast(c & 0xFF) == 26 ) + { + entity->behavior = &actTeleporter; + entity->skill[2] = c; + entity->flags[NOUPDATE] = true; + int duration = (c >> 8) & 0xFFFF; + int dir = (c >> 24) & 0xF; + tunnelPortalSetAttributes(entity, duration, dir); + } break; } } @@ -2069,6 +2277,7 @@ static void changeLevel() { players[i]->hud.weapon = nullptr; players[i]->hud.magicLeftHand = nullptr; players[i]->hud.magicRightHand = nullptr; + players[i]->hud.magicRangefinder = nullptr; players[i]->ghost.reset(); FollowerMenu[i].recentEntity = nullptr; FollowerMenu[i].followerToCommand = nullptr; @@ -2095,6 +2304,7 @@ static void changeLevel() { { soundNotification_group->stop(); } + ensembleSounds.stopPlaying(true); VoiceChat.deinitRecording(false); #elif defined USE_OPENAL if ( sound_group ) @@ -2194,10 +2404,13 @@ static void changeLevel() { camera.globalLightModifierActive = GLOBAL_LIGHT_MODIFIER_STOPPED; camera.luminance = defaultLuminance; players[i]->hud.followerBars.clear(); + spellcastingAnimationManager_deactivate(&cast_animation[i]); } EnemyHPDamageBarHandler::dumpCache(); + AOEIndicators_t::cleanup(); monsterAllyFormations.reset(); particleTimerEmitterHitEntities.clear(); + particleTimerEffects.clear(); monsterTrapIgnoreEntities.clear(); minimapHighlights.clear(); @@ -2213,6 +2426,11 @@ static void changeLevel() { std::atomic_bool loading_done {false}; auto loading_task = std::async(std::launch::async, [&loading_done](){ gameplayCustomManager.readFromFile(); + if ( gameplayCustomManager.inUse() ) + { + conductGameChallenges[CONDUCT_MODDED] = 1; + Mods::disableSteamAchievements = true; + } updateLoadingScreen(10); int checkMapHash = -1; @@ -2341,6 +2559,60 @@ static void changeLevel() { Player::Minimap_t::mapDetails.push_back(std::make_pair("map_flag_disable_hunger", "")); } + if ( !died ) + { + if ( stats[clientnum]->type == MYCONID && stats[clientnum]->playerRace == RACE_MYCONID && stats[clientnum]->stat_appearance == 0 + && stats[clientnum]->helmet && gameStatistics[STATISTICS_NO_CAP] >= 0 ) + { + gameStatistics[STATISTICS_NO_CAP]++; + if ( gameStatistics[STATISTICS_NO_CAP] >= 5 ) + { + steamAchievement("BARONY_ACH_NO_CAP"); + } + } + if ( stats[clientnum]->getEffectActive(EFF_GROWTH) >= 2 + && ((stats[clientnum]->type == MYCONID && stats[clientnum]->playerRace == RACE_MYCONID) + || (stats[clientnum]->type == DRYAD && stats[clientnum]->playerRace == RACE_DRYAD)) && stats[clientnum]->stat_appearance == 0 + && !stats[clientnum]->helmet && gameStatistics[STATISTICS_DONT_TOUCH_HAIR] >= 0 ) + { + gameStatistics[STATISTICS_DONT_TOUCH_HAIR]++; + if ( gameStatistics[STATISTICS_DONT_TOUCH_HAIR] >= 25 ) + { + steamAchievement("BARONY_ACH_DONT_TOUCH_HAIR"); + } + } + if ( stats[clientnum]->type == SALAMANDER && stats[clientnum]->playerRace == RACE_SALAMANDER && stats[clientnum]->stat_appearance == 0 + && stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) >= 3 && stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) <= 4 + && gameStatistics[STATISTICS_GARGOYLES_QUEST] >= 0 ) + { + gameStatistics[STATISTICS_GARGOYLES_QUEST]++; + if ( gameStatistics[STATISTICS_GARGOYLES_QUEST] >= 10 ) + { + steamAchievement("BARONY_ACH_GARGOYLES_QUEST"); + } + } + if ( stats[clientnum]->type == SALAMANDER && stats[clientnum]->playerRace == RACE_SALAMANDER && stats[clientnum]->stat_appearance == 0 + && stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) >= 1 && stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) <= 2 + && gameStatistics[STATISTICS_FIRE_FIGHTER] >= 0 ) + { + gameStatistics[STATISTICS_FIRE_FIGHTER]++; + if ( gameStatistics[STATISTICS_FIRE_FIGHTER] >= 5 ) + { + steamAchievement("BARONY_ACH_FIRE_FIGHTER"); + } + } + if ( stats[clientnum]->type == SALAMANDER && stats[clientnum]->playerRace == RACE_SALAMANDER && stats[clientnum]->stat_appearance == 0 + && !stats[clientnum]->getEffectActive(EFF_SALAMANDER_HEART) + && gameStatistics[STATISTICS_DISCIPLINE] >= 0 ) + { + gameStatistics[STATISTICS_DISCIPLINE]++; + if ( gameStatistics[STATISTICS_DISCIPLINE] >= 25 ) + { + steamAchievement("BARONY_ACH_DISCIPLINE"); + } + } + } + Compendium_t::Events_t::onLevelChangeEvent(clientnum, prevcurrentlevel, prevsecretfloor, prevmapname, died); for ( int i = 0; i < MAXPLAYERS; ++i ) { @@ -2470,7 +2742,8 @@ static std::unordered_map clientPacketHandlers = { const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); if ( players[player]->ghost.my ) { - players[player]->ghost.my->skill[3] = net_packet->data[5]; + players[player]->ghost.my->skill[3] = net_packet->data[5] & (1 << 0); + players[player]->ghost.my->skill[11] = net_packet->data[5] & (1 << 1) ? 1 : 0; } }}, @@ -2489,7 +2762,7 @@ static std::unordered_map clientPacketHandlers = { * Packet breakdown: * [0][1][2][3]: "EFFE" * [4][5][6][7]: Entity's UID. - * [8][9][10][11]: Entity's effects. + * [8][9][10][11][12][13][14][15]: Entity's effects. */ Uint32 uid = static_cast(SDLNet_Read32(&net_packet->data[4])); @@ -2519,12 +2792,31 @@ static std::unordered_map clientPacketHandlers = { { if ( net_packet->data[8 + i / 8] & power(2, i - (i / 8) * 8) ) { - stats->EFFECTS[i] = true; + stats->setEffectValueUnsafe(i, 1); } else { - stats->EFFECTS[i] = false; + stats->clearEffect(i); + } + } + + int numBytes = NUMEFFECTS / 8; + + int numEffectStrengths = net_packet->data[8 + numBytes]; + int index = 0; + while ( numEffectStrengths > 0 ) + { + int currentIndex = 8 + numBytes + 1 + index; + if ( currentIndex + 1 >= NET_PACKET_SIZE || (currentIndex + 1 >= net_packet->len) ) + { + // too much data to read, abort + break; } + int effectIndex = net_packet->data[currentIndex + 0]; + Uint8 effectStrength = net_packet->data[currentIndex + 1]; + stats->setEffectValueUnsafe(effectIndex, effectStrength); + index += 2; + --numEffectStrengths; } } }}, @@ -2543,7 +2835,7 @@ static std::unordered_map clientPacketHandlers = { Entity *entity = uidToEntity((int)SDLNet_Read32(&net_packet->data[4])); if ( entity ) { - entity->fskill[net_packet->data[8]] = (SDLNet_Read16(&net_packet->data[9]) / 256.0); + entity->fskill[net_packet->data[8]] = static_cast(SDLNet_Read16(&net_packet->data[9])) / 256.0; } }}, @@ -2687,6 +2979,16 @@ static std::unordered_map clientPacketHandlers = { entity->skill[12] = static_cast((statusBeatitudeQuantityAppearance >> 16) & 0xFF); // beatitude entity->skill[13] = static_cast((statusBeatitudeQuantityAppearance >> 8) & 0xFF); // quantity entity->skill[14] = static_cast((statusBeatitudeQuantityAppearance) & 0xFF); // appearance + if ( net_packet->len >= 16 ) + { + if ( entity->skill[10] >= 0 && entity->skill[10] < NUMITEMS ) + { + if ( items[entity->skill[10]].category == TOME_SPELL ) + { + entity->skill[14] = SDLNet_Read16(&net_packet->data[16]); + } + } + } entity->itemReceivedDetailsFromServer = 1; } }}, @@ -2798,6 +3100,33 @@ static std::unordered_map clientPacketHandlers = { } }}, + // attract item + { 'ATTI', []() { + Uint32 uid = SDLNet_Read32(&net_packet->data[4]); + Entity* entity = uidToEntity(uid); + if ( entity ) + { + entity->itemNotMoving = 0; + entity->itemNotMovingClient = 0; + entity->flags[USERFLAG1] = false; // enable collision + entity->flags[UPDATENEEDED] = true; + entity->flags[NOUPDATE] = false; + + entity->itemFollowUID = ((Uint32)SDLNet_Read32(&net_packet->data[20])); + + entity->x = ((Sint16)SDLNet_Read16(&net_packet->data[8])) / 32.0; + entity->y = ((Sint16)SDLNet_Read16(&net_packet->data[10])) / 32.0; + entity->z = ((Sint16)SDLNet_Read16(&net_packet->data[12])) / 32.0; + entity->new_z = entity->z; + entity->itemLevitate = 1.0; + entity->itemLevitateStartZ = entity->z; + + entity->vel_x = ((Sint16)SDLNet_Read16(&net_packet->data[14])) / 32.0; + entity->vel_y = ((Sint16)SDLNet_Read16(&net_packet->data[16])) / 32.0; + entity->vel_z = ((Sint16)SDLNet_Read16(&net_packet->data[18])) / 32.0; + } + } }, + // spawn an explosion {'EXPL', [](){ Sint16 x = (Sint16)SDLNet_Read16(&net_packet->data[4]); @@ -2830,7 +3159,8 @@ static std::unordered_map clientPacketHandlers = { Sint16 z = (Sint16)SDLNet_Read16(&net_packet->data[8]); Sint16 sprite = (Sint16)SDLNet_Read16(&net_packet->data[10]); Entity* gib = spawnGibClient(x, y, z, sprite); - gib->flags[SPRITE] = net_packet->data[12]; + gib->flags[SPRITE] = net_packet->data[12] & (1 << 0); + gib->skill[5] = net_packet->data[12] & (1 << 1); // poof if ( !spawn_blood && !gib->flags[SPRITE] && gib->sprite != 5 ) { gib->flags[INVISIBLE] = true; @@ -2897,6 +3227,9 @@ static std::unordered_map clientPacketHandlers = { case PARTICLE_EFFECT_ABILITY_ROCK: createParticleRock(entity, sprite); break; + case PARTICLE_EFFECT_SPIN: + createParticleSpin(entity); + break; case PARTICLE_EFFECT_SHATTERED_GEM: createParticleShatteredGem(entity->x, entity->y, 7.5, sprite, entity); break; @@ -2933,6 +3266,15 @@ static std::unordered_map clientPacketHandlers = { spellTimer->particleTimerPreDelay = 0; } break; + case PARTICLE_EFFECT_DESTINY_TELEPORT: + { + Uint32 duration = SDLNet_Read32(&net_packet->data[11]); + Entity* spellTimer = createParticleTimer(entity, duration, sprite); + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_SHOOT_PARTICLES; + spellTimer->particleTimerCountdownSprite = sprite; + spellTimer->particleTimerPreDelay = 0; + } + break; case PARTICLE_EFFECT_TELEPORT_PULL: { Entity* spellTimer = createParticleTimer(entity, 40, sprite); @@ -2958,9 +3300,118 @@ static std::unordered_map clientPacketHandlers = { createParticleShadowTag(entity, uid, 60 * TICKS_PER_SECOND); break; } + case PARTICLE_EFFECT_PINPOINT: + { + Uint32 uid = SDLNet_Read32(&net_packet->data[11]); + if ( sprite >= PINPOINT_PARTICLE_START && sprite < PINPOINT_PARTICLE_END ) + { + if ( net_packet->len >= 23 ) + { + int duration = SDLNet_Read32(&net_packet->data[15]); + int spellID = SDLNet_Read32(&net_packet->data[19]); + createParticleSpellPinpointTarget(entity, uid, sprite, duration, spellID); + } + } + break; + } + case PARTICLE_EFFECT_REVENANT_CURSE: + { + int duration = SDLNet_Read32(&net_packet->data[15]); + if ( Entity* fx = createParticleAestheticOrbit(entity, sprite, duration, PARTICLE_EFFECT_REVENANT_CURSE) ) + { + fx->z = 7.5; + fx->yaw = entity->yaw; + fx->ditheringOverride = 6; + } + break; + } case PARTICLE_EFFECT_SPELL_WEB_ORBIT: createParticleAestheticOrbit(entity, 863, 400, PARTICLE_EFFECT_SPELL_WEB_ORBIT); break; + case PARTICLE_EFFECT_SMITE_PINPOINT: + { + for ( int i = 0; i < 3; ++i ) + { + Entity* fx1 = createParticleAestheticOrbit(entity, 2401, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_SMITE_PINPOINT); + fx1->yaw = entity->yaw + PI / 2 + 2 * i * PI / 3; + fx1->fskill[4] = entity->x; + fx1->fskill[5] = entity->y; + fx1->x = entity->x; + fx1->y = entity->y; + fx1->fskill[6] = fx1->yaw; + fx1->skill[3] = 0; + if ( i != 0 ) + { + fx1->actmagicNoLight = 1; + } + } + break; + } + case PARTICLE_EFFECT_TURN_UNDEAD: + { + if ( Entity* fx1 = createParticleAestheticOrbit(entity, 2401, 3 * TICKS_PER_SECOND, PARTICLE_EFFECT_TURN_UNDEAD) ) + { + fx1->yaw = entity->yaw; + fx1->fskill[4] = entity->x; + fx1->fskill[5] = entity->y; + fx1->x = entity->x; + fx1->y = entity->y; + fx1->fskill[6] = fx1->yaw; + fx1->skill[3] = 0; + } + break; + } + case PARTICLE_EFFECT_HOLY_FIRE: + { + int duration = SDLNet_Read32(&net_packet->data[15]); + if ( Entity* fx = createParticleAestheticOrbit(entity, 288, duration, PARTICLE_EFFECT_HOLY_FIRE) ) + { + fx->flags[SPRITE] = true; + fx->flags[INVISIBLE] = true; + } + break; + } + case PARTICLE_EFFECT_DEFY_FLESH_ORBIT: + { + int duration = SDLNet_Read32(&net_packet->data[15]); + if ( Entity* fx = createParticleAestheticOrbit(entity, 2363, duration, PARTICLE_EFFECT_DEFY_FLESH_ORBIT) ) + { + fx->flags[INVISIBLE] = true; + } + break; + } + case PARTICLE_EFFECT_DEFY_FLESH: + { + int duration = SDLNet_Read32(&net_packet->data[15]); + Sint32 dir = SDLNet_Read32(&net_packet->data[19]); + if ( Entity* fx = createParticleAestheticOrbit(entity, 2363, duration, PARTICLE_EFFECT_DEFY_FLESH) ) + { + fx->yaw = dir / 256.0; + fx->flags[INVISIBLE] = true; + + fx->pitch = PI / 2; + fx->fskill[0] = fx->yaw; + fx->fskill[1] = PI / 4 - PI / 8; + fx->fskill[2] = entity->z; + fx->x = entity->x - 8.0 * cos(fx->yaw); + fx->y = entity->y - 8.0 * sin(fx->yaw); + fx->z = entity->z; + fx->scalex = 0.0; + fx->scaley = 0.0; + fx->scalez = 0.0; + } + break; + } + case PARTICLE_EFFECT_FOCI_LIGHT: + { + createParticleFociLight(entity, sprite, false); + break; + } + case PARTICLE_EFFECT_FOCI_DARK: + { + createParticleFociDark(entity, sprite, false); + break; + } case PARTICLE_EFFECT_PORTAL_SPAWN: { Entity* spellTimer = createParticleTimer(entity, 100, sprite); @@ -3012,6 +3463,132 @@ static std::unordered_map clientPacketHandlers = { } } break; + case PARTICLE_EFFECT_ENSEMBLE_OTHER_CAST: + createEnsembleTargetParticleCircling(entity); + break; + case PARTICLE_EFFECT_ENSEMBLE_SELF_CAST: + createEnsembleHUDParticleCircling(entity); + break; + case PARTICLE_EFFECT_IGNITE: + createParticleIgnite(entity); + break; + case PARTICLE_EFFECT_SHATTER_OBJECTS: + createParticleShatterObjects(entity); + break; + case PARTICLE_EFFECT_LIGHTNING_SEQ: + floorMagicCreateLightningSequence(entity, entity->ticks + 1); + break; + case PARTICLE_EFFECT_STATIC_ORBIT: + { + Entity* fx = createParticleAestheticOrbit(entity, sprite, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5; + fx->actmagicOrbitDist = 20; + fx->actmagicNoLight = 1; + break; + } + case PARTICLE_EFFECT_STATIC_MAXIMISE: + { + for ( int i = 0; i < 3; ++i ) + { + Entity* fx = createParticleAestheticOrbit(entity, sprite, 2 * TICKS_PER_SECOND, PARTICLE_EFFECT_STATIC_ORBIT); + fx->z = 7.5 - 2.0 * i; + fx->scalex = 1.0; + fx->scaley = 1.0; + fx->scalez = 1.0; + fx->actmagicOrbitDist = 20; + fx->yaw += i * 2 * PI / 3; + fx->actmagicNoLight = (i == 0 ? 0 : 1); + } + break; + } + case PARTICLE_EFFECT_CONTROL: + for ( int i = 0; i < 4; ++i ) + { + Entity* fx = spawnMagicParticle(entity); + fx->sprite = sprite; + fx->yaw = entity->yaw + i * PI / 2; + fx->scalex = 0.7; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->vel_x = 0.5 * cos(entity->yaw + i * PI / 2); + fx->vel_y = 0.5 * sin(entity->yaw + i * PI / 2); + } + break; + case PARTICLE_EFFECT_FLAMES: + { + int duration = SDLNet_Read32(&net_packet->data[15]); + if( Entity* fx = createParticleAestheticOrbit(entity, 233, duration, PARTICLE_EFFECT_IGNITE_ORBIT)) + { + fx->flags[SPRITE] = true; + fx->x = entity->x; + fx->y = entity->y; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->vel_z = -0.05; + fx->actmagicOrbitDist = 2; + fx->fskill[2] = entity->yaw + (local_rng.rand() % 8) * PI / 4.0; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + } + break; + } + case PARTICLE_EFFECT_HEAT_ORBIT_SPIN: + { + Uint32 particle = SDLNet_Read32(&net_packet->data[11]); + int duration = SDLNet_Read32(&net_packet->data[15]); + for ( int i = 0; i < 2; ++i ) + { + if ( Entity* fx = createParticleAestheticOrbit(entity, sprite, duration, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->x = entity->x; + fx->y = entity->y; + fx->z = 7.5; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->vel_z = -0.5; + fx->actmagicOrbitDist = 5; + fx->fskill[2] = entity->yaw + PI / 4.0 + i * PI; + fx->yaw = fx->fskill[2]; + fx->fskill[4] = 0.25; + if ( particle == 1 ) + { + fx->lightBonus = vec4{ 0.f, 0.f, 0.f, 0.f }; + fx->actmagicNoLight = 1; + } + } + } + break; + } + case PARTICLE_EFFECT_SUMMON_FLAMES: + { + int duration = SDLNet_Read32(&net_packet->data[15]); + for ( int i = 0; i < 3; ++i ) + { + if ( Entity* fx = createParticleAestheticOrbit(entity, 233, duration, PARTICLE_EFFECT_IGNITE_ORBIT) ) + { + fx->flags[SPRITE] = true; + fx->x = entity->x; + fx->y = entity->y; + fx->fskill[0] = fx->x; + fx->fskill[1] = fx->y; + fx->z = -7.5; + fx->vel_z = 0.25; + fx->actmagicOrbitDist = 4; + fx->fskill[2] = entity->yaw + (i) * 2 * PI / 3.0; + fx->yaw = fx->fskill[2]; + fx->actmagicNoLight = 1; + + } + } + break; + } + case PARTICLE_EFFECT_BOLAS: + { + Uint32 duration = SDLNet_Read32(&net_packet->data[15]); + createParticleBolas(entity, sprite, duration, nullptr); + } + break; default: break; } @@ -3065,7 +3642,7 @@ static std::unordered_map clientPacketHandlers = { case PARTICLE_EFFECT_TELEPORT_PULL_TARGET_LOCATION: { Entity* spellTimer = createParticleTimer(nullptr, 40, 593); - spellTimer->particleTimerCountdownAction = PARTICLE_EFFECT_TELEPORT_PULL_TARGET_LOCATION; + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_TELEPORT_PULL_TARGET_LOCATION; spellTimer->particleTimerCountdownSprite = 593; spellTimer->x = particle_x * 16.0 + 8; spellTimer->y = particle_y * 16.0 + 8; @@ -3078,13 +3655,165 @@ static std::unordered_map clientPacketHandlers = { case PARTICLE_EFFECT_SHATTERED_GEM: createParticleShatteredGem(particle_x, particle_y, 7.5, sprite, nullptr); break; - default: + case PARTICLE_EFFECT_ERUPT: + createParticleErupt(particle_x, particle_y, sprite); break; - } - }}, - - // enemy hp bar - {'ENHP', [](){ + case PARTICLE_EFFECT_BOOBY_TRAP: + createParticleBoobyTrapExplode(nullptr, particle_x, particle_y); + break; + case PARTICLE_EFFECT_MISC_PUDDLE: + spawnMiscPuddle(nullptr, particle_x, particle_y, sprite); + break; + case PARTICLE_EFFECT_BLOOD_BUBBLE: + { + for ( int i = 0; i < 4; ++i ) + { + if ( Entity* gib = spawnGibClient(particle_x, particle_y, particle_z, 5) ) + { + gib->sprite = 5; + } + + Entity* fx = createParticleAestheticOrbit(nullptr, 283, 1.5 * TICKS_PER_SECOND + i * 10, PARTICLE_EFFECT_BLOOD_BUBBLE); + real_t dir = (local_rng.rand() % 360) * PI / 180.f; + fx->x = particle_x + 4.0 * cos(dir); + fx->y = particle_y + 4.0 * sin(dir); + fx->z = particle_z - (local_rng.rand() % 5); + fx->flags[SPRITE] = true; + + fx->fskill[2] = 2 * PI * (local_rng.rand() % 10) / 10.0; + fx->fskill[3] = 0.025; // speed osc + fx->scalex = 0.0125; + fx->scaley = fx->scalex; + fx->scalez = fx->scalex; + fx->actmagicOrbitDist = 2; + fx->actmagicOrbitStationaryX = particle_x; + fx->actmagicOrbitStationaryY = particle_y; + } + break; + } + case PARTICLE_EFFECT_SPORE_BOMB: + for ( int i = 0; i < 16; ++i ) + { + Entity* gib = spawnGibClient(particle_x, particle_y, particle_z, sprite); + gib->sprite = sprite; + gib->yaw = i * PI / 4 + (-2 + local_rng.rand() % 5) * PI / 64; + gib->vel_x = 1.75 * cos(gib->yaw); + gib->vel_y = 1.75 * sin(gib->yaw); + gib->scalex = 0.5; + gib->scaley = 0.5; + gib->scalez = 0.5; + gib->z = local_rng.uniform(8, particle_z - 4); + gib->lightBonus = vec4(0.25, 0.25, 0.25, 0.f); + } + break; + case PARTICLE_EFFECT_WINDGATE: + { + int duration = static_cast(SDLNet_Read32(&net_packet->data[13])); + Uint32 data = SDLNet_Read32(&net_packet->data[17]); + + int wallDir = (data & 0xF); + int length = (data >> 4) & 0xF; + Uint32 casterUid = SDLNet_Read32(&net_packet->data[21]); + createWindMagic(casterUid, particle_x, particle_y, duration, wallDir, length); + break; + } + case PARTICLE_EFFECT_DEMESNE_DOOR: + createParticleDemesneDoor(particle_x, particle_y, particle_z / 256.0); + break; + case PARTICLE_EFFECT_NULL_PARTICLE: + { + Entity* fx = createParticleAestheticOrbit(nullptr, sprite, TICKS_PER_SECOND / 4, PARTICLE_EFFECT_NULL_PARTICLE); + fx->x = particle_x; + fx->y = particle_y; + fx->z = particle_z; + Sint32 dir = SDLNet_Read32(&net_packet->data[17]); + fx->yaw = dir / 256.0; + fx->actmagicOrbitDist = 0; + fx->actmagicNoLight = 0; + break; + } + case PARTICLE_EFFECT_AREA_EFFECT: + { + int radius = SDLNet_Read32(&net_packet->data[13]); + createSpellExplosionArea(sprite, nullptr, particle_x, particle_y, particle_z, radius, 0, nullptr); + break; + } + case PARTICLE_EFFECT_EARTH_ELEMENTAL_DIE: + { + Entity* spellTimer = createParticleTimer(nullptr, TICKS_PER_SECOND, -1); + spellTimer->x = particle_x; + spellTimer->y = particle_y; + spellTimer->z = particle_z; + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_EARTH_ELEMENTAL_DIE; + break; + } + case PARTICLE_EFFECT_DUCK_SPAWN_FEATHER: + { + duckSpawnFeather(sprite, particle_x, particle_y, particle_z, nullptr); + break; + } + case PARTICLE_EFFECT_SABOTAGE_TRAP: + { + Entity* spellTimer = createParticleTimer(nullptr, TICKS_PER_SECOND, -1); + spellTimer->x = particle_x; + spellTimer->y = particle_y; + spellTimer->z = particle_z; + spellTimer->particleTimerCountdownAction = PARTICLE_TIMER_ACTION_TRAP_SABOTAGED; + break; + } + case PARTICLE_EFFECT_EARTH_ELEMENTAL_SUMMON_AOE: + { + int radius = SDLNet_Read32(&net_packet->data[13]); + Uint32 color = SDLNet_Read32(&net_packet->data[17]); + if ( Entity* fx = createParticleAOEIndicator(nullptr, particle_x, particle_y, 0.0, TICKS_PER_SECOND, radius) ) + { + fx->actSpriteFollowUID = 0; + fx->actSpriteCheckParentExists = 0; + if ( auto indicator = AOEIndicators_t::getIndicator(fx->skill[10]) ) + { + indicator->indicatorColor = color; + indicator->loop = false; + indicator->gradient = 4; + indicator->framesPerTick = 2; + indicator->ticksPerUpdate = 1; + indicator->delayTicks = 0; + } + } + break; + } + case PARTICLE_EFFECT_BASTION_MUSHROOM: + { + Uint32 casterUid = SDLNet_Read32(&net_packet->data[21]); + createMushroomSpellEffect(uidToEntity(casterUid), particle_x, particle_y); + break; + } + case PARTICLE_EFFECT_METEOR_STATIONARY_ORBIT: + { + int duration = static_cast(SDLNet_Read32(&net_packet->data[13])); + Sint32 dir = SDLNet_Read32(&net_packet->data[17]); + if ( Entity* fx = createParticleAestheticOrbit(nullptr, 2210, duration, PARTICLE_EFFECT_METEOR_STATIONARY_ORBIT) ) + { + fx->x = particle_x; + fx->y = particle_y; + fx->z = particle_z; + fx->yaw = (dir / 256.0) + PI / 4; + } + if ( Entity* fx = createParticleAestheticOrbit(nullptr, 2211, duration, PARTICLE_EFFECT_METEOR_STATIONARY_ORBIT) ) + { + fx->x = particle_x; + fx->y = particle_y; + fx->z = particle_z; + fx->yaw = (dir / 256.0) - PI / 4; + } + break; + } + default: + break; + } + }}, + + // enemy hp bar + {'ENHP', [](){ Sint16 enemy_hp = SDLNet_Read16(&net_packet->data[4]); Sint16 enemy_maxhp = SDLNet_Read16(&net_packet->data[6]); Sint16 oldhp = SDLNet_Read16(&net_packet->data[8]); @@ -3107,15 +3836,21 @@ static std::unordered_map clientPacketHandlers = { } } char enemy_name[128] = ""; - strcpy(enemy_name, (char*)(&net_packet->data[31])); + strcpy(enemy_name, (char*)(&net_packet->data[55])); auto details = enemyHPDamageBarHandler[clientnum].addEnemyToList(static_cast(enemy_hp), static_cast(enemy_maxhp), static_cast(oldhp), uid, enemy_name, lowPriorityTick, gib); if ( details ) { details->enemy_statusEffects1 = SDLNet_Read32(&net_packet->data[15]); details->enemy_statusEffects2 = SDLNet_Read32(&net_packet->data[19]); - details->enemy_statusEffectsLowDuration1 = SDLNet_Read32(&net_packet->data[23]); - details->enemy_statusEffectsLowDuration2 = SDLNet_Read32(&net_packet->data[27]); + details->enemy_statusEffects3 = SDLNet_Read32(&net_packet->data[23]); + details->enemy_statusEffects4 = SDLNet_Read32(&net_packet->data[27]); + details->enemy_statusEffects5 = SDLNet_Read32(&net_packet->data[31]); + details->enemy_statusEffectsLowDuration1 = SDLNet_Read32(&net_packet->data[35]); + details->enemy_statusEffectsLowDuration2 = SDLNet_Read32(&net_packet->data[39]); + details->enemy_statusEffectsLowDuration3 = SDLNet_Read32(&net_packet->data[43]); + details->enemy_statusEffectsLowDuration4 = SDLNet_Read32(&net_packet->data[47]); + details->enemy_statusEffectsLowDuration5 = SDLNet_Read32(&net_packet->data[51]); } }}, @@ -3134,6 +3869,10 @@ static std::unordered_map clientPacketHandlers = { { displayType = DamageGibDisplayType::DMG_GIB_SPRITE; } + else if ( net_packet->data[11] == 3 ) + { + displayType = DamageGibDisplayType::DMG_GIB_GUARD; + } spawnDamageGib(uidToEntity(uid), dmg, gib, displayType); }}, @@ -3206,6 +3945,54 @@ static std::unordered_map clientPacketHandlers = { } }}, + // project spirit player + { 'PROJ', []() { + if ( players[clientnum] == nullptr || !players[clientnum]->entity || stats[clientnum]->HP <= 0 ) + { + return; + } + + players[clientnum]->ghost.initTeleportLocations(players[clientnum]->entity->x / 16, players[clientnum]->entity->y / 16); + players[clientnum]->ghost.spawnGhost(); + //node_t* nextnode = nullptr; + //for ( auto node = map.entities->first; node; node = nextnode ) + //{ + // nextnode = node->next; + // if ( Entity* entity = (Entity*)node->element ) + // { + // if ( entity->behavior == &actProjectSpiritCam && entity->skill[2] == clientnum ) + // { + // entity->removeLightField(); + // list_RemoveNode(entity->mynode); + // } + // } + //} + + //Uint32 uid = SDLNet_Read32(&net_packet->data[4]); + //if ( Entity* targetEntity = uidToEntity(uid) ) + //{ + // // projectcam + // Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Deathcam entity. + // entity->x = targetEntity->x; + // entity->y = targetEntity->y; + // entity->z = -2; + // entity->flags[NOUPDATE] = true; + // entity->flags[PASSABLE] = true; + // entity->flags[INVISIBLE] = true; + // entity->behavior = &actProjectSpiritCam; + // entity->skill[1] = targetEntity->getUID(); + // entity->skill[2] = clientnum; + // entity->yaw = targetEntity->yaw; + // entity->pitch = PI / 8; + // players[clientnum]->entity->skill[3] = 2; + // if ( multiplayer != CLIENT ) + // { + // entity_uids--; + // } + // entity->setUID(-3); + //} + }}, + // teleport player {'TELE', [](){ if (players[clientnum] == nullptr || !Player::getPlayerInteractEntity(clientnum) ) @@ -3290,7 +4077,7 @@ static std::unordered_map clientPacketHandlers = { { if ( effect != EFF_VAMPIRICAURA && effect != EFF_WITHDRAWAL && effect != EFF_SHAPESHIFT ) { - stats[j]->EFFECTS[effect] = false; + stats[j]->clearEffect(effect); stats[j]->EFFECTS_TIMERS[effect] = 0; } } @@ -3312,13 +4099,13 @@ static std::unordered_map clientPacketHandlers = { list_RemoveNode(entity->mynode); // inform the server that we deleted the entity - strcpy((char*)net_packet->data, "ENTD"); - net_packet->data[4] = clientnum; - SDLNet_Write32(entity2->getUID(), &net_packet->data[5]); - net_packet->address.host = net_server.host; - net_packet->address.port = net_server.port; - net_packet->len = 9; - sendPacket(net_sock, -1, net_packet, 0); + //strcpy((char*)net_packet->data, "ENTD"); + //net_packet->data[4] = clientnum; + //SDLNet_Write32(entity2->getUID(), &net_packet->data[5]); + //net_packet->address.host = net_server.host; + //net_packet->address.port = net_server.port; + //net_packet->len = 9; + //sendPacket(net_sock, -1, net_packet, 0); } } }}, @@ -3329,6 +4116,31 @@ static std::unordered_map clientPacketHandlers = { cameravars[clientnum].shakey += ((Sint8)(net_packet->data[5])); }}, + // no mana flash + { 'NOMP', []() { + messagePlayer(clientnum, MESSAGE_MISC, Language::get(375)); + playSound(563, 64); + if ( players[clientnum]->magic.noManaProcessedOnTick == 0 ) + { + players[clientnum]->magic.flashNoMana(); + } + if ( net_packet->len >= 8 ) + { + if ( stats[clientnum]->defending && stats[clientnum]->shield ) + { + ItemType itemType = static_cast(SDLNet_Read32(&net_packet->data[4])); + if ( stats[clientnum]->shield->type == itemType ) + { + Input& input = Input::inputs[clientnum]; + if ( input.binaryToggle("Defend") ) + { + input.consumeBinaryToggle("Defend"); + } + } + } + } + } }, + // a torch burns out {'TORC', [](){ ItemType itemType = static_cast(SDLNet_Read16(&net_packet->data[4])); @@ -3437,7 +4249,13 @@ static std::unordered_map clientPacketHandlers = { item = NULL; break; } - if ( item != NULL ) + + int checkType = -1; + if ( net_packet->len >= 8 ) + { + checkType = SDLNet_Read16(&net_packet->data[6]); + } + if ( item != NULL && (checkType <= -1 || (checkType >= 0 && item->type == checkType)) ) { if ( item->count > 1 ) { @@ -3701,6 +4519,25 @@ static std::unordered_map clientPacketHandlers = { case 506: case 507: case 508: + case 830: + case 831: + case 832: + case 833: + case 834: + case 835: + case 836: + case 837: + case 838: + case 839: + case 840: + case 841: + case 842: + case 843: + case 844: + case 845: + case 846: + case 847: + case 848: // return early, don't play monster noises from players. return; default: @@ -3800,6 +4637,34 @@ static std::unordered_map clientPacketHandlers = { } } } + else if ( pickedUp && pickedUp->type == TOOL_DUCK && !stats[clientnum]->shield ) + { + bool shapeshifted = false; + if ( players[clientnum] && players[clientnum]->entity && players[clientnum]->entity->effectShapeshift != NOTHING ) + { + shapeshifted = true; + } + + if ( !shapeshifted && !intro ) + { + useItem(pickedUp, clientnum); + + auto& hotbar_t = players[clientnum]->hotbar; + auto& hotbar = hotbar_t.slots(); + if ( hotbar_t.magicDuckHotbarSlot >= 0 ) + { + hotbar[hotbar_t.magicDuckHotbarSlot].item = pickedUp->uid; + for ( int i = 0; i < NUM_HOTBAR_SLOTS; ++i ) + { + if ( i != hotbar_t.magicDuckHotbarSlot && hotbar[i].item == pickedUp->uid ) + { + hotbar[i].item = 0; + hotbar[i].resetLastItem(); + } + } + } + } + } } }}, @@ -3950,6 +4815,7 @@ static std::unordered_map clientPacketHandlers = { } else { // anonymous monster Monster monster = (Monster)SDLNet_Read32(&net_packet->data[9]); stats[clientnum]->killer_monster = monster; + stats[clientnum]->killer_name = ""; } } else if (killer == KilledBy::ITEM) { ItemType item = (ItemType)SDLNet_Read32(&net_packet->data[8]); @@ -4035,8 +4901,16 @@ static std::unordered_map clientPacketHandlers = { { continue; } - if ( item->type == ARTIFACT_ORB_PURPLE ) + if ( item->type == ARTIFACT_ORB_PURPLE || item->type == TOOL_DUCK ) { + Item** slot = itemSlot(stats[clientnum], item); + if ( slot != nullptr ) + { + *slot = nullptr; + } + + players[clientnum]->paperDoll.updateSlots(); + strcpy((char*)net_packet->data, "DIEI"); SDLNet_Write32((Uint32)item->type, &net_packet->data[4]); SDLNet_Write32((Uint32)item->status, &net_packet->data[8]); @@ -4052,7 +4926,6 @@ static std::unordered_map clientPacketHandlers = { net_packet->len = 28; sendPacketSafe(net_sock, -1, net_packet, 0); list_RemoveNode(node); - break; } } } @@ -4062,7 +4935,10 @@ static std::unordered_map clientPacketHandlers = { Entity* mapCreature = (Entity*)mapNode->element; if ( mapCreature ) { - mapCreature->monsterEntityRenderAsTelepath = 0; // do a final pass to undo any telepath rendering. + if ( mapCreature->monsterEntityRenderAsTelepath == 1 ) + { + mapCreature->monsterEntityRenderAsTelepath = 0; // do a final pass to undo any telepath rendering. + } } } }}, @@ -4128,7 +5004,7 @@ static std::unordered_map clientPacketHandlers = { if ( !(c == EFF_VAMPIRICAURA && stats[clientnum]->EFFECTS_TIMERS[c] == -2) && c != EFF_WITHDRAWAL && c != EFF_SHAPESHIFT ) { - stats[clientnum]->EFFECTS[c] = false; + stats[clientnum]->clearEffect(c); stats[clientnum]->EFFECTS_TIMERS[c] = 0; } } @@ -4164,25 +5040,47 @@ static std::unordered_map clientPacketHandlers = { // update effects flags {'UPEF', [](){ + int numBytes = NUMEFFECTS / 8; for (int c = 0; c < NUMEFFECTS; c++) { if ( net_packet->data[4 + c / 8]&power(2, c - (c / 8) * 8) ) { - stats[clientnum]->EFFECTS[c] = true; - if ( net_packet->data[12 + c / 8] & power(2, c - (c / 8) * 8) ) // use these bits to denote if duration is low. + stats[clientnum]->setEffectValueUnsafe(c, 1); + if ( net_packet->data[4 + numBytes + c / 8] & power(2, c - (c / 8) * 8) ) // use these bits to denote if duration is low. { stats[clientnum]->EFFECTS_TIMERS[c] = 1; } + else if ( stats[clientnum]->EFFECTS_TIMERS[c] > 0 ) + { + stats[clientnum]->EFFECTS_TIMERS[c] = 0; + } } else { - stats[clientnum]->EFFECTS[c] = false; + stats[clientnum]->clearEffect(c); if ( stats[clientnum]->EFFECTS_TIMERS[c] > 0 ) { stats[clientnum]->EFFECTS_TIMERS[c] = 0; } } } + + int numEffectStrengths = net_packet->data[4 + numBytes * 2]; + int index = 0; + while ( numEffectStrengths > 0 ) + { + int currentIndex = (4 + numBytes * 2 + 1) + index; + if ( currentIndex + 1 >= NET_PACKET_SIZE || ((currentIndex + 1) >= net_packet->len) ) + { + // too much data to read, abort + break; + } + int effectIndex = net_packet->data[currentIndex + 0]; + Uint8 effectStrength = net_packet->data[currentIndex + 1]; + stats[clientnum]->setEffectValueUnsafe(effectIndex, effectStrength); + index += 2; + --numEffectStrengths; + } }}, // update entity stat flag @@ -4414,6 +5312,35 @@ static std::unordered_map clientPacketHandlers = { gameStatistics[gameplayStat] |= shifted; } } + else if ( gameplayStat == STATISTICS_FLAVORTOWN ) + { + gameStatistics[gameplayStat] |= changeval; + } + else if ( gameplayStat == STATISTICS_BARDIC_INSPIRATION ) + { + if ( changeval == 0 ) + { + gameStatistics[gameplayStat] = 0; + } + else + { + gameStatistics[gameplayStat] += changeval; + } + } + else if ( gameplayStat == STATISTICS_PARRY_TANK ) + { + if ( changeval == 0 ) + { + if ( gameStatistics[gameplayStat] < 20 ) + { + gameStatistics[gameplayStat] = 0; + } + } + else + { + gameStatistics[gameplayStat] += changeval; + } + } else { gameStatistics[gameplayStat] += changeval; @@ -4437,7 +5364,10 @@ static std::unordered_map clientPacketHandlers = { // the server's just doing a routine check return; } - + if ( net_packet->data[13] == 0 ) + { + return; // dont warp back to start level + } changeLevel(); }}, @@ -4764,7 +5694,8 @@ static std::unordered_map clientPacketHandlers = { chestInv[clientnum].last = nullptr; players[clientnum]->openStatusScreen(GUI_MODE_INVENTORY, INVENTORY_MODE_ITEM); players[clientnum]->GUI.activateModule(Player::GUI_t::MODULE_CHEST); - players[clientnum]->inventoryUI.chestGUI.openChest(); + bool voidChest = net_packet->data[8] == 0 ? false : true; + players[clientnum]->inventoryUI.chestGUI.openChest(voidChest); } }}, @@ -4823,15 +5754,33 @@ static std::unordered_map clientPacketHandlers = { } }}, + {'FXSP', []() { + int spellID = SDLNet_Read32(&net_packet->data[6]); + if ( net_packet->data[4] == 1 ) // spellbook + { + int beatitude = static_cast(net_packet->data[5]); + GenericGUI[clientnum].openGUI(GUI_TYPE_ITEMFX, nullptr, beatitude, getSpellbookFromSpellID(spellID), spellID); + } + else + { + GenericGUI[clientnum].openGUI(GUI_TYPE_ITEMFX, nullptr, 0, SPELL_ITEM, spellID); + } + }}, + //Add a spell to the channeled spells list. {'CHAN', [](){ - spell_t* thespell = getSpellFromID(SDLNet_Read32(&net_packet->data[5])); - auto node = list_AddNodeLast(&channeledSpells[clientnum]); - node->element = thespell; - node->size = sizeof(spell_t); - //node->deconstructor = &spellDeconstructor_Channeled; - node->deconstructor = &emptyDeconstructor; - ((spell_t*)(node->element))->sustain_node = node; + if ( auto spell = getSpellFromID(SDLNet_Read32(&net_packet->data[5])) ) + { + if ( spell_t* thespell = copySpell(spell) ) + { + auto node = list_AddNodeLast(&channeledSpells[clientnum]); + node->element = thespell; + node->size = sizeof(spell_t); + //node->deconstructor = &spellDeconstructor_Channeled; + node->deconstructor = &spellChanneledClientDeconstructor; + ((spell_t*)(node->element))->sustain_node = node; + } + } }}, //Remove a spell from the channeled spells list. @@ -4855,7 +5804,10 @@ static std::unordered_map clientPacketHandlers = { //Map the magic. I mean magic the map. I mean magically map the level (client). {'MMAP', [](){ - spell_magicMap(clientnum); + int radius = SDLNet_Read16(&net_packet->data[4]); + int x = SDLNet_Read16(&net_packet->data[6]); + int y = SDLNet_Read16(&net_packet->data[8]); + spell_magicMap(clientnum, radius, x, y); }}, {'MFOD', [](){ @@ -4866,6 +5818,25 @@ static std::unordered_map clientPacketHandlers = { GenericGUI[clientnum].tinkeringKitDegradeOnUse(clientnum); }}, + // leaf pile + { 'LEAF', []() { + Uint32 uid = SDLNet_Read32(&net_packet->data[4]); + if ( Entity* entity = uidToEntity(uid) ) + { + if ( net_packet->data[8] == 1 ) + { + entity->skill[3] = (Sint32)net_packet->data[9]; + entity->skill[4] = (Sint32)net_packet->data[10]; + playSoundEntityLocal(entity, 754 + local_rng.rand() % 2, 64); + } + else if ( net_packet->data[8] == 2 ) + { + entity->skill[7] = (Sint32)net_packet->data[9]; + entity->skill[8] = (Sint32)net_packet->data[10]; + } + } + } }, + // boss death {'BDTH', [](){ for ( auto node = map.entities->first; node != nullptr; node = node->next ) @@ -4948,12 +5919,21 @@ static std::unordered_map clientPacketHandlers = { case RACE_TROLL: victoryType = 3; break; case RACE_SPIDER: victoryType = 3; break; case RACE_IMP: victoryType = 5; break; + case RACE_DRYAD: victoryType = 4; break; + case RACE_MYCONID: victoryType = 4; break; + case RACE_SALAMANDER: victoryType = 4; break; + case RACE_GREMLIN: victoryType = 5; break; + case RACE_GNOME: victoryType = 4; break; } victory = victoryType; if (net_packet->data[5] == 0) { // full ending switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::EndingHuman); break; case RACE_AUTOMATON: @@ -4968,6 +5948,7 @@ static std::unordered_map clientPacketHandlers = { case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::EndingEvil); break; } @@ -4977,6 +5958,10 @@ static std::unordered_map clientPacketHandlers = { switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::ClassicEndingHuman); break; case RACE_AUTOMATON: @@ -4991,6 +5976,7 @@ static std::unordered_map clientPacketHandlers = { case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::ClassicEndingEvil); break; } @@ -5000,6 +5986,10 @@ static std::unordered_map clientPacketHandlers = { switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::ClassicBaphometEndingHuman); break; case RACE_AUTOMATON: @@ -5014,6 +6004,7 @@ static std::unordered_map clientPacketHandlers = { case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::ClassicBaphometEndingEvil); break; } @@ -5051,6 +6042,7 @@ static std::unordered_map clientPacketHandlers = { switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: MainMenu::beginFade(MainMenu::FadeDestination::HerxMidpointHuman); break; case RACE_AUTOMATON: @@ -5059,12 +6051,16 @@ static std::unordered_map clientPacketHandlers = { case RACE_GOATMAN: case RACE_GOBLIN: case RACE_INSECTOID: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::HerxMidpointBeast); break; case RACE_SKELETON: case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::HerxMidpointEvil); break; } @@ -5073,6 +6069,7 @@ static std::unordered_map clientPacketHandlers = { switch ( race ) { default: case RACE_HUMAN: + case RACE_GNOME: MainMenu::beginFade(MainMenu::FadeDestination::BaphometMidpointHuman); break; case RACE_AUTOMATON: @@ -5081,12 +6078,16 @@ static std::unordered_map clientPacketHandlers = { case RACE_GOATMAN: case RACE_GOBLIN: case RACE_INSECTOID: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: MainMenu::beginFade(MainMenu::FadeDestination::BaphometMidpointBeast); break; case RACE_SKELETON: case RACE_VAMPIRE: case RACE_SUCCUBUS: case RACE_INCUBUS: + case RACE_GREMLIN: MainMenu::beginFade(MainMenu::FadeDestination::BaphometMidpointEvil); break; } @@ -5101,7 +6102,7 @@ static std::unordered_map clientPacketHandlers = { MinimapPing newPing(ticks, net_packet->data[4], net_packet->data[5], net_packet->data[6], - false, + net_packet->data[8] ? true : false, (MinimapPing::PingType)net_packet->data[7]); for ( int c = 0; c < MAXPLAYERS; ++c ) { @@ -5140,6 +6141,24 @@ static std::unordered_map clientPacketHandlers = { } }}, + { 'OVRC', []() { + if ( players[clientnum] && players[clientnum]->entity && stats[clientnum] ) + { + cast_animation[clientnum].overcharge_init = net_packet->data[4]; + } + } }, + + { 'KINE', []() { + if ( players[clientnum] && players[clientnum]->entity && stats[clientnum] ) + { + real_t vel = sqrt(pow(players[clientnum]->entity->vel_y, 2) + pow(players[clientnum]->entity->vel_x, 2)); + players[clientnum]->entity->monsterKnockbackVelocity = std::min(2.25, std::max(1.0, vel)); + + real_t dir = (SDLNet_Read32(&net_packet->data[4]) / 256.0); + players[clientnum]->entity->monsterKnockbackTangentDir = dir; + } +} }, + // get item {'ITEQ', [](){ auto item = newItem( @@ -5231,6 +6250,7 @@ static std::unordered_map clientPacketHandlers = { { deleteSaveGame(multiplayer); } + printlog("Received order to restart game"); MainMenu::beginFade(MainMenu::FadeDestination::GameStart); pauseGame(2, 0); }}, @@ -5498,60 +6518,246 @@ static std::unordered_map clientPacketHandlers = { } } }, - { 'VOIP',[]() { -#ifdef USE_FMOD - VoiceChat.receivePacket(net_packet); -#endif + { 'CAUO', []() { + // server order to open cauldron gui + Uint32 uid = SDLNet_Read32(&net_packet->data[4]); + if ( auto entity = uidToEntity(uid) ) + { + if ( entity->behavior == &::actCauldron ) + { + GenericGUI[clientnum].openGUI(GUI_TYPE_ALCHEMY, entity); + } + } } }, -}; - -void clientHandlePacket() -{ - if (handleSafePacket()) - { - return; - } - - Uint32 packetId = SDLNet_Read32(&net_packet->data[0]); - -#ifdef PACKETINFO - char packetinfo[NET_PACKET_SIZE]; - strncpy( packetinfo, (char*)net_packet->data, net_packet->len ); - packetinfo[net_packet->len] = 0; - printlog("info: client packet: %s\n", packetinfo); -#endif - if ( logCheckMainLoopTimers ) - { - char packetinfo[NET_PACKET_SIZE]; - memcpy(packetinfo, (char*)net_packet->data, net_packet->len); - packetinfo[net_packet->len] = '\0'; - - char packetHeader[5]; - memcpy(packetHeader, packetinfo, 4); - packetHeader[4] = '\0'; - std::string tmp = packetHeader; - unsigned long hash = djb2Hash(packetHeader); - auto find = DebugStats.networkPackets.find(hash); - if ( find != DebugStats.networkPackets.end() ) + // server order to close cauldron + { 'CAUC', []() { + int player = net_packet->data[4]; + if ( player == clientnum ) { - ++DebugStats.networkPackets[hash].second; + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + if ( Entity* cauldron = uidToEntity(uid) ) + { + GenericGUI[clientnum].alchemyGUI.closeAlchemyMenu(); + } } - else + } }, + + { 'WRKO', []() { + // server order to open workbench gui + Uint32 uid = SDLNet_Read32(&net_packet->data[4]); + if ( auto entity = uidToEntity(uid) ) { - DebugStats.networkPackets.insert(std::make_pair(hash, std::make_pair(tmp, 0))); - messagePlayer(clientnum, MESSAGE_DEBUG, "%s", tmp.c_str()); + if ( entity->behavior == &::actWorkbench ) + { + GenericGUI[clientnum].openGUI(GUI_TYPE_TINKERING, entity); + } } - if ( packetId == 'ENTU' ) + } }, + + // server order to close workbench + { 'WRKC', []() { + int player = net_packet->data[4]; + if ( player == clientnum ) { - int sprite = 0; - Uint32 uidpacket = static_cast(SDLNet_Read32(&net_packet->data[4])); - if ( uidToEntity(uidpacket) ) + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + if ( Entity* cauldron = uidToEntity(uid) ) { - sprite = uidToEntity(uidpacket)->sprite; - auto find = DebugStats.entityUpdatePackets.find(sprite); - if ( find != DebugStats.entityUpdatePackets.end() ) - { + GenericGUI[clientnum].tinkerGUI.closeTinkerMenu(); + } + } + } }, + + { 'MBXO', []() { + // server order to open mailbox gui + Uint32 uid = SDLNet_Read32(&net_packet->data[4]); + if ( auto entity = uidToEntity(uid) ) + { + if ( entity->behavior == &::actMailbox ) + { + GenericGUI[clientnum].openGUI(GUI_TYPE_MAILBOX, entity); + } + } + } }, + + // server order to close mailbox + { 'MBXC', []() { + int player = net_packet->data[4]; + if ( player == clientnum ) + { + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + if ( Entity* cauldron = uidToEntity(uid) ) + { + GenericGUI[clientnum].mailboxGUI.closeMailMenu(); + } + } + } }, + + // server order to consume key for lock + { 'LKEY', []() { + const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); + bool success = false; + + // reply got packet + strcpy((char*)net_packet->data, "OKEY"); + net_packet->data[4] = clientnum; + + if ( player == clientnum ) + { + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + Entity* entity = uidToEntity(uid); + if ( entity && entity->behavior == &actWallLock ) + { + Item* key = players[clientnum]->inventoryUI.hasKeyForWallLock(*entity); + if ( key ) + { + SDLNet_Write16((Uint16)key->type, &net_packet->data[10]); + consumeItem(key, clientnum); + success = true; + } + } + } + + net_packet->data[9] = success ? 1 : 0; + net_packet->len = 12; + net_packet->address.host = net_server.host; + net_packet->address.port = net_server.port; + sendPacketSafe(net_sock, -1, net_packet, 0); + } }, + + // server ensemble music update + { 'ENSM', []() { + for ( int i = 4; i < net_packet->len; i += 4 ) + { + Uint32 data = SDLNet_Read32(&net_packet->data[i]); + int player = ((data & 0x7F) - 1); + if ( player >= 0 && player < MAXPLAYERS ) + { + players[player]->mechanics.ensembleDataUpdate = data; + } + } + } }, + + { 'VOIP',[]() { +#ifdef USE_FMOD + VoiceChat.receivePacket(net_packet); +#endif + } }, + + { 'MAPT',[]() { + int x = SDLNet_Read16(&net_packet->data[4]); + int y = SDLNet_Read16(&net_packet->data[6]); + Uint32 flagSet = SDLNet_Read32(&net_packet->data[8]); + Uint32 flagRemove = SDLNet_Read32(&net_packet->data[12]); + int layer = net_packet->data[16]; + if ( x >= 0 && x < map.width && y >= 0 && y < map.height && layer >= 0 && layer < MAPLAYERS ) + { + if ( flagSet ) + { + if ( !map.tileHasAttribute(x, y, layer, flagSet) ) + { + map.tileAttributes[layer + (y * MAPLAYERS) + (x * MAPLAYERS * map.height)] |= flagSet; + } + } + if ( flagRemove ) + { + if ( map.tileHasAttribute(x, y, layer, flagRemove) ) + { + map.tileAttributes[layer + (y * MAPLAYERS) + (x * MAPLAYERS * map.height)] &= ~flagRemove; + } + } + } + }}, + + // command spell + { 'COMD',[]() { + Uint32 uid = SDLNet_Read32(&net_packet->data[4]); + if ( Entity* target = uidToEntity(uid) ) + { + if ( !target->clientsHaveItsStats ) + { + target->giveClientStats(); + } + FollowerMenu[clientnum].followerToCommand = target; + FollowerMenu[clientnum].initfollowerMenuGUICursor(true); // set gui_mode to follower menu + } + } }, + + { 'FOCI',[]() { + Uint32 uid = SDLNet_Read32(&net_packet->data[4]); + real_t x = SDLNet_Read16(&net_packet->data[8]) / 32.0; + real_t y = SDLNet_Read16(&net_packet->data[10]) / 32.0; + real_t z = SDLNet_Read16(&net_packet->data[12]) / 32.0; + real_t dir = SDLNet_Read16(&net_packet->data[14]) / 256.0; + int sprite = SDLNet_Read16(&net_packet->data[16]); + Uint32 seed = SDLNet_Read32(&net_packet->data[18]); + real_t velocityBonus = SDLNet_Read16(&net_packet->data[22]) / 256.0; + if ( Entity* gib = spawnFociGib(x, y, z, dir, velocityBonus, uid, sprite, seed) ) + { + gib->setUID(uid); + } + } }, + + { 'SANM',[]() { // player spellcast animation + int player = net_packet->data[4]; + int pose = net_packet->data[5]; + int charge = SDLNet_Read16(&net_packet->data[6]); + spellcastAnimationUpdateReceive(player, pose, charge); + } }, + + // update breakable counter + { 'GBRK', []() { + players[clientnum]->mechanics.gremlinBreakableCounter = net_packet->data[4]; + } }, +}; + +void clientHandlePacket() +{ + if (handleSafePacket()) + { + return; + } + + Uint32 packetId = SDLNet_Read32(&net_packet->data[0]); + +#ifdef PACKETINFO + char packetinfo[NET_PACKET_SIZE]; + strncpy( packetinfo, (char*)net_packet->data, net_packet->len ); + packetinfo[net_packet->len] = 0; + printlog("info: client packet: %s\n", packetinfo); +#endif + if ( logCheckMainLoopTimers ) + { + char packetinfo[NET_PACKET_SIZE]; + memcpy(packetinfo, (char*)net_packet->data, net_packet->len); + packetinfo[net_packet->len] = '\0'; + + char packetHeader[5]; + memcpy(packetHeader, packetinfo, 4); + packetHeader[4] = '\0'; + + std::string tmp = packetHeader; + unsigned long hash = djb2Hash(packetHeader); + auto find = DebugStats.networkPackets.find(hash); + if ( find != DebugStats.networkPackets.end() ) + { + ++DebugStats.networkPackets[hash].second; + } + else + { + DebugStats.networkPackets.insert(std::make_pair(hash, std::make_pair(tmp, 0))); + messagePlayer(clientnum, MESSAGE_DEBUG, "%s", tmp.c_str()); + } + if ( packetId == 'ENTU' ) + { + int sprite = 0; + Uint32 uidpacket = static_cast(SDLNet_Read32(&net_packet->data[4])); + if ( uidToEntity(uidpacket) ) + { + sprite = uidToEntity(uidpacket)->sprite; + auto find = DebugStats.entityUpdatePackets.find(sprite); + if ( find != DebugStats.entityUpdatePackets.end() ) + { ++DebugStats.entityUpdatePackets[sprite]; } else @@ -5791,13 +6997,29 @@ static std::unordered_map serverPacketHandlers = { { appearance = (entity->skill[14] & 0xF) % items[entity->skill[10]].variations; } + else if ( entity->skill[10] == ENCHANTED_FEATHER ) + { + appearance = entity->skill[14] % ENCHANTED_FEATHER_MAX_DURABILITY; + } + else if ( entity->skill[10] == MAGICSTAFF_SCEPTER ) + { + appearance = entity->skill[14] % MAGICSTAFF_SCEPTER_CHARGE_MAX; + } statusBeatitudeQuantityAppearance |= (static_cast(appearance) & 0xFF); // appearance - SDLNet_Write32(statusBeatitudeQuantityAppearance, &net_packet->data[12]); + net_packet->len = 16; + if ( entity->skill[10] >= 0 && entity->skill[10] < NUMITEMS ) + { + if ( items[entity->skill[10]].category == TOME_SPELL ) + { + SDLNet_Write16(entity->skill[14] % TOME_APPEARANCE_MAX, &net_packet->data[16]); + net_packet->len = 18; + } + } + net_packet->address.host = net_clients[x - 1].host; net_packet->address.port = net_clients[x - 1].port; - net_packet->len = 16; sendPacketSafe(net_sock, -1, net_packet, x - 1); } }}, @@ -6128,7 +7350,7 @@ static std::unordered_map serverPacketHandlers = { }}, // client deleted entity - {'ENTD', [](){ + /*{'ENTD', [](){ const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); for ( auto node = entitiesToDelete[player].first; node != NULL; node = node->next ) { @@ -6139,7 +7361,7 @@ static std::unordered_map serverPacketHandlers = { break; } } - }}, + }},*/ // clicked entity in range {'CKIR', [](){ @@ -6175,6 +7397,110 @@ static std::unordered_map serverPacketHandlers = { } }}, + // clicked wall lock entity in range with key + {'LKEY', []() { + const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); + client_keepalive[player] = ticks; + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + Entity* entity = uidToEntity(uid); + if ( entity && entity->behavior == &actWallLock ) + { + if ( players[player]->entity ) + { + client_selected[player] = entity; + inrange[player] = true; + if ( entity->wallLockState == Entity::WallLockStates::LOCK_NO_KEY ) + { + if ( entity->wallLockPlayerInteracting == 0 ) + { + entity->wallLockPlayerInteracting = players[player]->entity->getUID(); + } + else if ( entity->wallLockPlayerInteracting == players[player]->entity->getUID() ) + { + // client has already queued up an action, drop this interaction + client_selected[player] = nullptr; + inrange[player] = false; + } + } + } + } + }}, + + // clicked wall lock entity in range without key + {'LNOK', []() { + const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); + client_keepalive[player] = ticks; + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + Entity* entity = uidToEntity(uid); + if ( entity && entity->behavior == &actWallLock ) + { + if ( players[player]->entity ) + { + client_selected[player] = entity; + inrange[player] = true; + if ( entity->wallLockState == Entity::WallLockStates::LOCK_NO_KEY ) + { + if ( entity->wallLockPlayerInteracting == players[player]->entity->getUID() ) + { + // client has already queued up an action, drop this interaction + client_selected[player] = nullptr; + inrange[player] = false; + } + } + } + } + }}, + + // client checked valid key for the lock + { 'OKEY', []() { + const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); + client_keepalive[player] = ticks; + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + Entity* entity = uidToEntity(uid); + if ( entity && entity->behavior == &actWallLock ) + { + if ( entity->wallLockState == Entity::WallLockStates::LOCK_NO_KEY && net_packet->data[9] != 0 ) // success from client + { + Uint16 key = SDLNet_Read16(&net_packet->data[10]); + if ( key >= WOODEN_SHIELD && key < NUMITEMS ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6378), items[key].getIdentifiedName()); + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY, "wall locks", 1); + Compendium_t::Events_t::eventUpdate(player, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY, (ItemType)key, 1); + if ( key == KEY_IRON ) + { + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY_IRON, "wall locks", 1); + } + else if ( key == KEY_SILVER ) + { + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY_SILVER, "wall locks", 1); + steamStatisticUpdateClient(player, STEAM_STAT_PREMIUM_LOOTBOX, STEAM_STAT_INT, 1); + } + else if ( key == KEY_GOLD ) + { + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY_GOLD, "wall locks", 1); + steamStatisticUpdateClient(player, STEAM_STAT_PREMIUM_LOOTBOX, STEAM_STAT_INT, 1); + } + else if ( key == KEY_BRONZE ) + { + Compendium_t::Events_t::eventUpdateWorld(player, Compendium_t::CPDM_KEYLOCK_UNLOCKED_KEY_BRONZE, "wall locks", 1); + steamStatisticUpdateClient(player, STEAM_STAT_PREMIUM_LOOTBOX, STEAM_STAT_INT, 1); + } + } + + entity->wallLockState = Entity::WallLockStates::LOCK_KEY_START; + serverUpdateEntitySkill(entity, 0); + } + else if ( entity->wallLockState == Entity::WallLockStates::LOCK_NO_KEY && net_packet->data[9] == 0 ) + { + messagePlayer(player, MESSAGE_INTERACTION, Language::get(6379)); + playSoundEntity(entity, 152, 64); + } + entity->wallLockClientInteractDelay = 0; + entity->wallLockPlayerInteracting = 0; + } + }}, + // rat feed {'RATF', [](){ const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); @@ -6361,7 +7687,7 @@ static std::unordered_map serverPacketHandlers = { net_packet->data[24], &stats[player]->inventory); playerGreasyDropItem(player, item); - if ( net_packet->data[27] == 1 ) + if ( net_packet->data[26] == 1 ) { // shield if ( stats[player]->shield ) @@ -6377,7 +7703,7 @@ static std::unordered_map serverPacketHandlers = { stats[player]->shield = nullptr; } } - else if ( net_packet->data[27] == 0 ) + else if ( net_packet->data[26] == 0 ) { // weapon if ( stats[player]->weapon ) @@ -6395,6 +7721,33 @@ static std::unordered_map serverPacketHandlers = { } } }, + // duck throw + { 'DCKA', []() { + const int player = std::min(net_packet->data[25], (Uint8)(MAXPLAYERS - 1)); + client_keepalive[player] = ticks; + auto item = newItem(static_cast(SDLNet_Read32(&net_packet->data[4])), + static_cast(SDLNet_Read32(&net_packet->data[8])), + SDLNet_Read32(&net_packet->data[12]), + SDLNet_Read32(&net_packet->data[16]), + SDLNet_Read32(&net_packet->data[20]), + net_packet->data[24], + &stats[player]->inventory); + playerThrowDuck(player, item, net_packet->data[26]); + // shield + if ( stats[player]->shield && stats[player]->shield->type == TOOL_DUCK ) + { + if ( stats[player]->shield->node ) + { + list_RemoveNode(stats[player]->shield->node); + } + else + { + free(stats[player]->shield); + } + stats[player]->shield = nullptr; + } + } }, + // item drop (on death) {'DIEI', [](){ const int player = std::min(net_packet->data[25], (Uint8)(MAXPLAYERS - 1)); @@ -6484,7 +7837,8 @@ static std::unordered_map serverPacketHandlers = { const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); if ( players[player]->ghost.my ) { - players[player]->ghost.my->skill[3] = net_packet->data[5]; + players[player]->ghost.my->skill[3] = net_packet->data[5] & (1 << 0); + players[player]->ghost.my->skill[11] = net_packet->data[5] & (1 << 1) ? 1 : 0; for ( int c = 1; c < MAXPLAYERS; ++c ) { // relay packet to other players if ( client_disconnected[c] || c == player ) { @@ -6589,11 +7943,28 @@ static std::unordered_map serverPacketHandlers = { } else { - if ( rand() % 100 <= (std::max(10, buyValue)) ) // 20% to 100% from 1-100 gold + if ( local_rng.rand() % 100 <= (std::max(10, buyValue)) ) // 20% to 100% from 1-100 gold { increaseSkill = true; } } + + if ( increaseSkill && entity ) + { + if ( !strcmp(map.name, "Mages Guild") ) + { + int increases = hamletShopkeeperSkillLimit[client][entity->getUID()]; + if ( increases >= hamletTradingSkillLimit ) + { + increaseSkill = false; + if ( local_rng.rand() % 2 ) + { + messagePlayer(client, MESSAGE_HINT | MESSAGE_INTERACTION, Language::get(6868)); + } + } + } + } + if ( increaseSkill ) { if ( buyValue <= 1 ) @@ -6601,11 +7972,25 @@ static std::unordered_map serverPacketHandlers = { if ( stats[client]->getProficiency(PRO_TRADING) < SKILL_LEVEL_SKILLED ) { players[client]->entity->increaseSkill(PRO_TRADING); + if ( entity ) + { + if ( !strcmp(map.name, "Mages Guild") ) + { + hamletShopkeeperSkillLimit[client][entity->getUID()]++; + } + } } } else { players[client]->entity->increaseSkill(PRO_TRADING); + if ( entity ) + { + if ( !strcmp(map.name, "Mages Guild") ) + { + hamletShopkeeperSkillLimit[client][entity->getUID()]++; + } + } } } //if ( local_rng.rand() % 2 ) @@ -6780,6 +8165,50 @@ static std::unordered_map serverPacketHandlers = { equipItem(item, &stats[client]->shield, client, false); }}, + // consume torch item shield slot + { 'COOK', []() { + const int client = std::min(net_packet->data[25], (Uint8)(MAXPLAYERS - 1)); + auto item = newItem( + static_cast(SDLNet_Read32(&net_packet->data[4])), + static_cast(SDLNet_Read32(&net_packet->data[8])), + SDLNet_Read32(&net_packet->data[12]), + SDLNet_Read32(&net_packet->data[16]), + SDLNet_Read32(&net_packet->data[20]), + net_packet->data[24], + &stats[client]->inventory); + if ( stats[client]->shield ) + { + // deselect shield + if ( stats[client]->shield->node ) + { + list_RemoveNode(stats[client]->shield->node); + } + else + { + free(stats[client]->shield); + } + stats[client]->shield = nullptr; + } + if ( item->count > 0 ) + { + bool oldIntro = intro; + intro = true; + equipItem(item, &stats[client]->shield, client, false); + intro = oldIntro; + } + else + { + if ( item->node ) + { + list_RemoveNode(item->node); + } + else + { + free(item); + } + } + } }, + // equip item (any other slot) {'EQUM', [](){ const int client = std::min(net_packet->data[25], (Uint8)(MAXPLAYERS - 1)); @@ -6886,9 +8315,11 @@ static std::unordered_map serverPacketHandlers = { { item->identified = slot->identified; } + Uint32 newAppearance = item->appearance; + item->appearance = slot->appearance; if ( !itemCompare(item, slot, false, false) ) { - slot->appearance = item->appearance; + slot->appearance = newAppearance; if ( onIdentify ) { slot->identified = true; @@ -6900,6 +8331,70 @@ static std::unordered_map serverPacketHandlers = { item = nullptr; } }, + // update itemType of item + { 'EQUT', []() { + const int client = std::min(net_packet->data[25], (Uint8)(MAXPLAYERS - 1)); + auto item = newItem( + static_cast(SDLNet_Read32(&net_packet->data[4])), + static_cast(SDLNet_Read32(&net_packet->data[8])), + SDLNet_Read32(&net_packet->data[12]), + SDLNet_Read32(&net_packet->data[16]), + SDLNet_Read32(&net_packet->data[20]), + net_packet->data[24], + nullptr); + + Item* slot = nullptr; + ItemType newType = static_cast(SDLNet_Read32(&net_packet->data[27])); + + switch ( net_packet->data[26] ) + { + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_WEAPON: + slot = stats[client]->weapon; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_SHIELD: + slot = stats[client]->shield; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_MASK: + slot = stats[client]->mask; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_HELM: + slot = stats[client]->helmet; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_GLOVES: + slot = stats[client]->gloves; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BOOTS: + slot = stats[client]->shoes; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_BREASTPLATE: + slot = stats[client]->breastplate; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_CLOAK: + slot = stats[client]->cloak; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_AMULET: + slot = stats[client]->amulet; + break; + case ItemEquippableSlot::EQUIPPABLE_IN_SLOT_RING: + slot = stats[client]->ring; + break; + default: + break; + } + + if ( slot ) + { + if ( !itemCompare(item, slot, false, false) ) + { + slot->type = newType; + slot->appearance = item->appearance; + } + } + + free(item); + item = nullptr; + } }, + // apply item to entity {'APIT', [](){ const int client = std::min(net_packet->data[25], (Uint8)(MAXPLAYERS - 1)); @@ -6945,7 +8440,41 @@ static std::unordered_map serverPacketHandlers = { const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); if (players[player] && players[player]->entity) { - players[player]->entity->attack(net_packet->data[5], net_packet->data[6], nullptr); + ItemType type = static_cast(SDLNet_Read32(&net_packet->data[7])); + Uint32 appearance = static_cast(SDLNet_Read32(&net_packet->data[11])); + if ( stats[player]->weapon && stats[player]->weapon->type == type ) + { + if ( type == MAGICSTAFF_SCEPTER ) + { + stats[player]->weapon->appearance = appearance; + } + } + if ( type == TOOL_DUCK ) + { + if ( stats[player]->weapon && stats[player]->weapon->type != TOOL_DUCK ) + { + return; + } + } + if ( type == GEM_JEWEL ) + { + if ( stats[player]->weapon && stats[player]->weapon->type != GEM_JEWEL ) + { + return; + } + } + int pose = net_packet->data[5]; + if ( pose == PLAYER_POSE_GOLEM_SMASH ) + { + Item* tmp = stats[player]->weapon; + stats[player]->weapon = nullptr; + players[player]->entity->attack(pose, net_packet->data[6], nullptr); + stats[player]->weapon = tmp; + } + else + { + players[player]->entity->attack(pose, net_packet->data[6], nullptr); + } } }}, @@ -6958,6 +8487,14 @@ static std::unordered_map serverPacketHandlers = { } }}, + //Multiplayer duck code (server). + { 'DUCK', []() { + const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); + const int duck = net_packet->data[5]; + players[player]->mechanics.pendingDucks.push_back( + std::make_pair(duck, ticks + (3 + (local_rng.rand() % 30)) * TICKS_PER_SECOND)); + } }, + //The client failed some alchemy. {'BOOM', [](){ const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); @@ -6994,13 +8531,23 @@ static std::unordered_map serverPacketHandlers = { spell_t* thespell = getSpellFromID(SDLNet_Read32(&net_packet->data[5])); if ( players[player] && players[player]->entity ) { - if ( net_packet->data[9] == 1 ) + bool spellbookCast = net_packet->data[9] == 1; + if ( net_packet->len > 10 ) { - castSpell(players[player]->entity->getUID(), thespell, false, false, true); + CastSpellProps_t castSpellProps; + castSpellProps.caster_x = (SDLNet_Read32(&net_packet->data[10]) / 256.0); + castSpellProps.caster_y = (SDLNet_Read32(&net_packet->data[14]) / 256.0); + castSpellProps.target_x = (SDLNet_Read32(&net_packet->data[18]) / 256.0); + castSpellProps.target_y = (SDLNet_Read32(&net_packet->data[22]) / 256.0); + castSpellProps.targetUID = (SDLNet_Read32(&net_packet->data[26])); + castSpellProps.wallDir = net_packet->data[30]; + castSpellProps.optionalData = net_packet->data[31]; + castSpellProps.overcharge = net_packet->data[32]; + castSpell(players[player]->entity->getUID(), thespell, false, false, spellbookCast, &castSpellProps); } else { - castSpell(players[player]->entity->getUID(), thespell, false, false); + castSpell(players[player]->entity->getUID(), thespell, false, false, spellbookCast); } } }}, @@ -7019,7 +8566,7 @@ static std::unordered_map serverPacketHandlers = { //The client added an item to the chest. {'CITM', [](){ const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); - if (!openedChest[player]) + if ( net_packet->data[27] == 0 && !openedChest[player]) { return; } @@ -7032,13 +8579,28 @@ static std::unordered_map serverPacketHandlers = { newitem->appearance = SDLNet_Read32(&net_packet->data[21]); newitem->identified = net_packet->data[25]; bool forceNewStack = net_packet->data[26] ? true : false; - openedChest[player]->addItemToChestServer(newitem, forceNewStack, nullptr); + if ( net_packet->data[27] == 0 ) + { + Item* chestItem = openedChest[player]->addItemToChestServer(newitem, forceNewStack, nullptr); + if ( chestItem != newitem ) + { + free(newitem); + } + } + else + { + Item* chestItem = Entity::addItemToVoidChestServer(player, newitem, forceNewStack, nullptr); + if ( chestItem != newitem ) + { + free(newitem); + } + } }}, //The client removed an item from the chest. {'RCIT', [](){ const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); - if (!openedChest[player]) + if ( net_packet->data[27] == 0 && !openedChest[player]) { return; } @@ -7051,7 +8613,15 @@ static std::unordered_map serverPacketHandlers = { item->appearance = SDLNet_Read32(&net_packet->data[21]); item->identified = net_packet->data[25]; - openedChest[player]->removeItemFromChestServer(item, item->count); + if ( net_packet->data[27] == 0 ) + { + openedChest[player]->removeItemFromChestServer(item, item->count); + } + else + { + Entity::removeItemFromVoidChestServer(player, item, item->count); + } + free(item); }}, // the client removed a curse on his equipment @@ -7298,6 +8868,7 @@ static std::unordered_map serverPacketHandlers = { entity->flags[PASSABLE] = true; entity->flags[UPDATENEEDED] = true; entity->behavior = &actGoldBag; + entity->goldDroppedByPlayer = player + 1; } }}, @@ -7305,9 +8876,10 @@ static std::unordered_map serverPacketHandlers = { {'EMOT', [](){ const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); const int sfx = SDLNet_Read16(&net_packet->data[5]); + const int vol = std::min(92, (int)(net_packet->data[7])); if ( players[player] && players[player]->entity ) { - playSoundEntityLocal(players[player]->entity, sfx, 92); + playSoundEntityLocal(players[player]->entity, sfx, vol); for ( int c = 1; c < MAXPLAYERS; ++c ) { // send to all other players @@ -7316,7 +8888,7 @@ static std::unordered_map serverPacketHandlers = { strcpy((char*)net_packet->data, "SNEL"); SDLNet_Write16(sfx, &net_packet->data[4]); SDLNet_Write32((Uint32)players[player]->entity->getUID(), &net_packet->data[6]); - SDLNet_Write16(92, &net_packet->data[10]); + SDLNet_Write16(vol, &net_packet->data[10]); net_packet->address.host = net_clients[c - 1].host; net_packet->address.port = net_clients[c - 1].port; net_packet->len = 12; @@ -7353,7 +8925,7 @@ static std::unordered_map serverPacketHandlers = { MinimapPing newPing(ticks, net_packet->data[4], net_packet->data[5], net_packet->data[6], - false, + net_packet->data[8] ? true : false, (MinimapPing::PingType)net_packet->data[7]); sendMinimapPing(net_packet->data[4], newPing.x, newPing.y, newPing.pingType); // relay self and to other clients. }}, @@ -7370,7 +8942,7 @@ static std::unordered_map serverPacketHandlers = { if ( players[player] && players[player]->entity && stats[player] ) { if ( client_classes[player] == CLASS_ACCURSED && - stats[player]->EFFECTS[EFF_VAMPIRICAURA] && players[player]->entity->playerVampireCurse == 1 ) + stats[player]->getEffectActive(EFF_VAMPIRICAURA) && players[player]->entity->playerVampireCurse == 1 ) { players[player]->entity->setEffect(EFF_VAMPIRICAURA, true, 1, true); messagePlayerColor(player, MESSAGE_STATUS, uint32ColorGreen, Language::get(3241)); @@ -7433,6 +9005,80 @@ static std::unordered_map serverPacketHandlers = { item_FoodAutomaton(item, player); }}, + // adorcise item + { 'ADOR', []() { + const int player = std::min(net_packet->data[25], (Uint8)(MAXPLAYERS - 1)); + auto item = newItem( + static_cast(SDLNet_Read32(&net_packet->data[4])), + static_cast(SDLNet_Read32(&net_packet->data[8])), + SDLNet_Read32(&net_packet->data[12]), + SDLNet_Read32(&net_packet->data[16]), + SDLNet_Read32(&net_packet->data[20]), + net_packet->data[24], nullptr); + + real_t spawn_x = SDLNet_Read16(&net_packet->data[26]) * 16.0 + 8.0; + real_t spawn_y = SDLNet_Read16(&net_packet->data[28]) * 16.0 + 8.0; + bool spawned = false; + if ( players[player]->entity ) + { + if ( Entity* monster = spellEffectAdorcise(*players[player]->entity, spellElementMap[SPELL_ADORCISM], + spawn_x, spawn_y, item) ) + { + spawned = true; + } + } + + if ( !spawned ) + { + messagePlayer(player, MESSAGE_MISC, Language::get(6578)); + + // refund the item at the player, or spawn location if dead + bool dropped = false; + if ( players[player]->entity ) + { + // no room to spawn! + auto item2 = newItem(item->type, + item->status, + item->beatitude, + item->count, + item->appearance, + item->identified, + &stats[player]->inventory); + dropped = dropItem(item2, player, true, true); + } + + if ( !dropped ) + { + Entity* entity = newEntity(-1, 1, map.entities, nullptr); //Item entity. + entity->flags[INVISIBLE] = true; + entity->flags[UPDATENEEDED] = true; + entity->x = players[player]->player_last_x; + entity->y = players[player]->player_last_y; + entity->sizex = 4; + entity->sizey = 4; + entity->yaw = local_rng.rand() % 360 * (PI / 180.0); + entity->vel_x = 0.0; + entity->vel_y = 0.0; + entity->vel_z = (-10 - local_rng.rand() % 20) * .01; + entity->flags[PASSABLE] = true; + entity->behavior = &actItem; + entity->skill[10] = item->type; + entity->skill[11] = item->status; + entity->skill[12] = item->beatitude; + entity->skill[13] = item->count; + entity->skill[14] = item->appearance; + entity->skill[15] = item->identified; + entity->parent = 0; + entity->itemOriginalOwner = 0; + + playSoundPos(players[player]->player_last_x, players[player]->player_last_y, 47 + local_rng.rand() % 3, 64); + } + messagePlayer(player, MESSAGE_MISC, Language::get(6621), item->getName()); + } + + free(item); + } }, + // broke a mirror { 'MIRR', []() { const int player = std::min(net_packet->data[4], (Uint8)(MAXPLAYERS - 1)); @@ -7527,6 +9173,57 @@ static std::unordered_map serverPacketHandlers = { } }}, + // client closed cauldron + { 'CAUC', []() { + int player = net_packet->data[4]; + if ( player >= 0 && player < MAXPLAYERS ) + { + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + if ( Entity* cauldron = uidToEntity(uid) ) + { + if ( achievementObserver.playerUids[player] == (Uint32)cauldron->skill[6] ) + { + cauldron->skill[6] = 0; + serverUpdateEntitySkill(cauldron, 6); + } + } + } + } }, + + // client closed workbench + { 'WRKC', []() { + int player = net_packet->data[4]; + if ( player >= 0 && player < MAXPLAYERS ) + { + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + if ( Entity* workbench = uidToEntity(uid) ) + { + if ( achievementObserver.playerUids[player] == (Uint32)workbench->skill[6] ) + { + workbench->skill[6] = 0; + serverUpdateEntitySkill(workbench, 6); + } + } + } + } }, + + // client closed mailbox + { 'MBXC', []() { + int player = net_packet->data[4]; + if ( player >= 0 && player < MAXPLAYERS ) + { + Uint32 uid = SDLNet_Read32(&net_packet->data[5]); + if ( Entity* mailbox = uidToEntity(uid) ) + { + if ( achievementObserver.playerUids[player] == (Uint32)mailbox->skill[6] ) + { + mailbox->skill[6] = 0; + serverUpdateEntitySkill(mailbox, 6); + } + } + } + } }, + // client claimed some assist items { 'ASSI', []() { int player = net_packet->data[4]; @@ -7570,6 +9267,93 @@ static std::unordered_map serverPacketHandlers = { VoiceChat.receivePacket(net_packet); #endif } }, + + { 'FXGD',[]() { + int player = net_packet->data[4]; + if ( player >= 1 && player < MAXPLAYERS && !players[player]->isLocalPlayer() ) + { + Sint32 goldSpent = (Sint32)SDLNet_Read32(&net_packet->data[5]); + stats[player]->GOLD -= goldSpent; + stats[player]->GOLD = std::max(0, stats[player]->GOLD); + + Sint32 magiccost = std::max(0, (Sint32)SDLNet_Read32(&net_packet->data[9])); + Sint32 prevMP = stats[player]->MP; + if ( players[player] && players[player]->entity ) + { + if ( magiccost > stats[player]->MP ) + { + // damage sound/effect due to overdraw. + strcpy((char*)net_packet->data, "SHAK"); + net_packet->data[4] = 10; // turns into .1 + net_packet->data[5] = 10; + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 6; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + playSoundPlayer(player, 28, 92); + } + players[player]->entity->drainMP(magiccost); + } + + Uint16 spellID = SDLNet_Read16(&net_packet->data[13]); + if ( spellID != SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + players[player]->mechanics.baseSpellIncrementMP(prevMP - stats[player]->MP, spell->skillID); + } + } + + strcpy((char*)net_packet->data, "GOLD"); + SDLNet_Write32(stats[player]->GOLD, &net_packet->data[4]); + net_packet->address.host = net_clients[player - 1].host; + net_packet->address.port = net_clients[player - 1].port; + net_packet->len = 8; + sendPacketSafe(net_sock, -1, net_packet, player - 1); + } + }}, + + { 'SANM',[]() { // player spellcast animation + int player = net_packet->data[4]; + int pose = net_packet->data[5]; + int charge = SDLNet_Read16(&net_packet->data[6]); + spellcastAnimationUpdateReceive(player, pose, charge); + } }, + + { 'OVRC', []() { + int player = net_packet->data[4]; + if ( player >= 1 && player < MAXPLAYERS && !players[player]->isLocalPlayer() ) + { + if ( players[player] && players[player]->entity && stats[player] ) + { + cast_animation[clientnum].overcharge_init = net_packet->data[5]; + } + } + } }, + + { 'SPLV',[]() { // player spell level proc + int player = net_packet->data[4]; + if ( player >= 0 && player < MAXPLAYERS ) + { + if ( !players[player]->isLocalPlayer() ) + { + if ( players[player]->entity ) + { + int spellID = SDLNet_Read16(&net_packet->data[5]); + Uint32 eventType = SDLNet_Read32(&net_packet->data[7]); + int eventValue = SDLNet_Read32(&net_packet->data[11]); + if ( spellID == SPELL_DETECT_FOOD ) + { + players[player]->mechanics.updateSustainedSpellEvent(SPELL_DETECT_FOOD, eventValue * 10, 1.0, nullptr); + } + else + { + magicOnSpellCastEvent(players[player]->entity, players[player]->entity, nullptr, spellID, eventType, eventValue); + } + } + } + } + } }, }; void serverHandlePacket() diff --git a/src/net.hpp b/src/net.hpp index c43c15e89..658d2e577 100644 --- a/src/net.hpp +++ b/src/net.hpp @@ -39,9 +39,10 @@ void serverUpdateEntitySprite(Entity* entity); void serverUpdateEntitySkill(Entity* entity, int skill); void serverUpdateEntityFSkill(Entity* entity, int fskill); void serverUpdateEntityStatFlag(Entity* entity, int flag); -void serverSpawnMiscParticles(Entity* entity, int particleType, int particleSprite, Uint32 optionalUid = 0); -void serverSpawnMiscParticlesAtLocation(Sint16 x, Sint16 y, Sint16 z, int particleType, int particleSprite); +void serverSpawnMiscParticles(Entity* entity, int particleType, int particleSprite, Uint32 optionalUid = 0, Uint32 duration = 0, Uint32 optionalData = 0); +void serverSpawnMiscParticlesAtLocation(Sint16 x, Sint16 y, Sint16 z, int particleType, int particleSprite, Uint32 duration = 0, Uint32 optionalData = 0, Uint32 optionalUid = 0); void serverUpdateEntityFlag(Entity* entity, int flag); +void serverUpdateMapTileFlag(Sint16 x, Sint16 y, int layer, Uint32 flagSet, Uint32 flagRemove); void serverUpdateBodypartIDs(Entity* entity); void serverUpdateEntityBodypart(Entity* entity, int bodypart); void serverUpdateEffects(int player); @@ -56,7 +57,7 @@ void serverSendItemToPickupAndEquip(int player, Item* item); void serverUpdateAllyStat(int player, Uint32 uidToUpdate, int LVL, int HP, int MAXHP, int type); void serverUpdatePlayerSummonStrength(int player); void serverUpdateAllyHP(int player, Uint32 uidToUpdate, int HP, int MAXHP, bool guarantee = false); -void sendMinimapPing(Uint8 player, Uint8 x, Uint8 y, Uint8 pingType = 0); +void sendMinimapPing(Uint8 player, Uint8 x, Uint8 y, Uint8 pingType = 0, bool radius = false); void sendAllyCommandClient(int player, Uint32 uid, int command, Uint8 x, Uint8 y, Uint32 targetUid = 0); enum NetworkingLobbyJoinRequestResult : int { diff --git a/src/objects.cpp b/src/objects.cpp index 0116a0fc9..c6308cb89 100644 --- a/src/objects.cpp +++ b/src/objects.cpp @@ -9,8 +9,8 @@ -------------------------------------------------------------------------------*/ -#include #include "main.hpp" +#include #include "entity.hpp" #include "messages.hpp" diff --git a/src/opengl.cpp b/src/opengl.cpp index 679d989b4..167875681 100644 --- a/src/opengl.cpp +++ b/src/opengl.cpp @@ -606,13 +606,20 @@ static void fillSmoothLightmap(int which, map_t& map) { auto& d = lightmapSmoothed[smoothindex]; const auto& s = lightmap[index]; - for (int c = 0; c < 4; ++c) { + for (int c = 0; c < 3; ++c) { // r,g,b of lightmap auto& dc = *(&d.x + c); const auto& sc = *(&s.x + c); const auto diff = sc - dc; if (fabsf(diff) < epsilon) { dc += diff; } else { dc += diff * rate; } } + { + // alpha/"shade" of lightmap + auto& dc = *(&d.x + 3); + const auto& sc = *(&s.x + 3); + const auto diff = sc - dc; + dc += diff * rate; + } } } @@ -622,7 +629,12 @@ static inline bool testTileOccludes(const map_t& map, int index) { } const Uint64& t0 = *(Uint64*)&map.tiles[index]; const Uint32& t1 = *(Uint32*)&map.tiles[index + 2]; - return (t0 & 0xffffffff00000000) && (t0 & 0x00000000ffffffff) && t1; + return (t0 & 0xffffffff00000000) // is floor != 0 + && (t0 & 0x00000000ffffffff) // is obstacle layer != 0 + && t1 // is ceiling != 0 + && (((t0 & 0xffffffff00000000) >> 32) != TRANSPARENT_TILE) // is floor != TRANSPARENT_TILE + && ((t0 & 0x00000000ffffffff) != TRANSPARENT_TILE) // is obstacle layer != TRANSPARENT_TILE + && (t1 != TRANSPARENT_TILE); // is ceiling != TRANSPARENT_TILE } static void loadLightmapTexture(int which, map_t& map) { @@ -638,6 +650,7 @@ static void loadLightmapTexture(int which, map_t& map) { #else const bool fullbright = (&map == &CompendiumEntries.compendiumMap) ? true :// compendium virtual map is always fullbright (conductGameChallenges[CONDUCT_CHEATS_ENABLED] ? *cvar_fullBright : false); + static ConsoleVariable cvar_shade_factor("/light_shade_factor", {0.8f, 0.8f, 0.63f, 0.f }); #endif // build lightmap texture data @@ -663,6 +676,15 @@ static void loadLightmapTexture(int which, map_t& map) { total.x = (total.x / count) * div; total.y = (total.y / count) * div; total.z = (total.z / count) * div; + if ( total.w > 0.01 ) + { +#ifndef EDITOR + float shade = std::min(1.f, (total.w / count) * div); + total.x -= total.x * shade * cvar_shade_factor->x; + total.y -= total.y * shade * cvar_shade_factor->y; + total.z -= total.z * shade * cvar_shade_factor->z; +#endif + } total.w = 1.f; pixels.insert(pixels.end(), {total.x, total.y, total.z, total.w}); } else { @@ -689,6 +711,8 @@ void beginGraphics() { #ifndef EDITOR ConsoleVariable cvar_fogDistance("/fog_distance", 0.f); ConsoleVariable cvar_fogColor("/fog_color", {0.f, 0.f, 0.f, 0.f}); +ConsoleVariable cvar_fogRate("/fog_rate", 0.0); +ConsoleVariable cvar_fogFade("/fog_fade", 0.0); #endif static void uploadUniforms(Shader& shader, float* proj, float* view, float* mapDims) { @@ -696,7 +720,7 @@ static void uploadUniforms(Shader& shader, float* proj, float* view, float* mapD if (proj) { GL_CHECK_ERR(glUniformMatrix4fv(shader.uniform("uProj"), 1, false, proj)); } if (view) { GL_CHECK_ERR(glUniformMatrix4fv(shader.uniform("uView"), 1, false, view)); } if (mapDims) { GL_CHECK_ERR(glUniform2fv(shader.uniform("uMapDims"), 1, mapDims)); } - + //GL_CHECK_ERR(glUniform1ui(shader.uniform("uTicks"), (GLuint)ticks)); #ifdef EDITOR float fogDistance = 0.f; float fogColor[4] = { 1.f, 1.f, 1.f, 1.f }; @@ -709,8 +733,13 @@ static void uploadUniforms(Shader& shader, float* proj, float* view, float* mapD GL_CHECK_ERR(glUniform4fv(shader.uniform("uFogColor"), 1, fogColor)); GL_CHECK_ERR(glUniform1f(shader.uniform("uFogDistance"), fogDistance)); } else { + auto fog_distance = *cvar_fogDistance; + if ( *cvar_fogFade > 0.01 ) + { + fog_distance *= (1.0 - (*cvar_fogFade / 2)) + ((*cvar_fogFade / 2) * sin(*cvar_fogRate * ticks * PI / 180.f)); + } GL_CHECK_ERR(glUniform4fv(shader.uniform("uFogColor"), 1, (float*)&*cvar_fogColor)); - GL_CHECK_ERR(glUniform1f(shader.uniform("uFogDistance"), *cvar_fogDistance)); + GL_CHECK_ERR(glUniform1f(shader.uniform("uFogDistance"), fog_distance)); } #endif } @@ -756,6 +785,13 @@ static vec4_t* HSVtoRGB(vec4_t* result, const vec4_t* hsv){ return result; } +#ifndef EDITOR +static ConsoleVariable cvar_color_mist_form("/color_mist_form", Vector4{ 0.6, 0.75, 0.0, 0.f }); +static ConsoleVariable cvar_color_hologram("/color_hologram", Vector4{ 0.9, 0.2, 0.5, 0.f }); +static ConsoleVariable cvar_color_force_shield("/color_force_shield", Vector4{ 0.8, 0.8, 0.0, 0.f }); +static ConsoleVariable cvar_color_reflector_shield("/color_reflector_shield", Vector4{ 0.8, 0.8, 0.0, 0.f }); +#endif + static void uploadLightUniforms(view_t* camera, Shader& shader, Entity* entity, int mode, bool remap) { const float cameraPos[4] = {(float)camera->x * 32.f, -(float)camera->z, (float)camera->y * 32.f, 1.f}; GL_CHECK_ERR(glUniform4fv(shader.uniform("uCameraPos"), 1, cameraPos)); @@ -794,7 +830,43 @@ static void uploadLightUniforms(view_t* camera, Shader& shader, Entity* entity, //remap.z.z = 0.8f; } } + +#ifndef EDITOR + if ( entity->mistformGLRender >= 0.45 ) + { + auto& whichColor = (entity->mistformGLRender > 1.9) ? cvar_color_hologram + : (entity->mistformGLRender > 0.9) ? cvar_color_mist_form + : ((entity->mistformGLRender >= 0.4 && entity->mistformGLRender <= 0.6) ? cvar_color_reflector_shield + : (entity->mistformGLRender >= 0.2 && entity->mistformGLRender <= 0.4) ? cvar_color_force_shield + : cvar_color_mist_form); + vec4_t hsv; + hsv.y = 100.f; // saturation + hsv.z = 100.f; // value + hsv.w = 0.f; // unused + + const auto amp = 360.0; + hsv.x = whichColor->x * amp; + HSVtoRGB(&remap.x, &hsv); // red + + hsv.x = whichColor->y * amp + 120; + HSVtoRGB(&remap.y, &hsv); // green + + hsv.x = whichColor->z * amp + 240; + HSVtoRGB(&remap.z, &hsv); // blue + } +#endif + #ifndef EDITOR + static ConsoleVariable cvar_colortest("/colortest", Vector4{1.f, 1.f, 1.f, 0.f}); + if ( cvar_colortest->w > 0.001f ) + { + const auto period = TICKS_PER_SECOND * 3; // 3 seconds + const auto time = (ticks % period) / (real_t)period; // [0-1] + + remap.x.x *= (1.0 - 0.5 + 0.5 * sin(2 * PI * time) * cvar_colortest->x); + remap.y.y *= (1.0 - 0.5 + 0.5 * sin(2 * PI * time) * cvar_colortest->y); + remap.z.z *= (1.0 - 0.5 + 0.5 * sin(2 * PI * time) * cvar_colortest->z); + } static ConsoleVariable cvar_rainbowTest("/rainbowtest", false); if (*cvar_rainbowTest) { remap = mat4x4_t(0.f); @@ -832,9 +904,18 @@ static void uploadLightUniforms(view_t* camera, Shader& shader, Entity* entity, #ifdef EDITOR false; #else - entity->monsterEntityRenderAsTelepath && player >= 0 && player < MAXPLAYERS + /*(entity->monsterEntityRenderAsTelepath == 2 && player >= 0 && player < MAXPLAYERS) + || */ + (entity->monsterEntityRenderAsTelepath && player >= 0 && player < MAXPLAYERS && players[player] && players[player]->entity - && stats[player]->mask&& stats[player]->mask->type == TOOL_BLINDFOLD_TELEPATHY; + && stats[player]->mask && stats[player]->mask->type == TOOL_BLINDFOLD_TELEPATHY); + + if ( !intro && player >= 0 && player < MAXPLAYERS && players[player] && players[player]->entity + && (entity->goldTelepathy > 0 && entity->behavior == &actGoldBag && entity->goldTelepathy & (1 << player) + || (entity->colliderTelepathy > 0 && entity->behavior == &actColliderDecoration && entity->colliderTelepathy & (1 << player))) ) + { + telepathy = true; + } #endif if ( telepathy ) { const GLfloat factor[4] = { 1.f, 1.f, 1.f, 1.f, }; @@ -918,12 +999,12 @@ static void uploadLightUniforms(view_t* camera, Shader& shader, Entity* entity, } } -constexpr Vector4 defaultBrightness = {1.f, 1.f, 1.f, 1.f}; -constexpr float defaultGamma = 0.75f; // default gamma level: 75% -constexpr float defaultExposure = 0.5f; // default exposure level: 50% -constexpr float defaultAdjustmentRate = 0.1f; // how fast your eyes adjust -constexpr float defaultLimitHigh = 4.f; // your aperture can increase to see something 4 times darker. -constexpr float defaultLimitLow = 0.1f; // your aperture can decrease to see something 10 times brighter. +const Vector4 defaultBrightness = {1.f, 1.f, 1.f, 1.f}; +const float defaultGamma = 0.75f; // default gamma level: 75% +const float defaultExposure = 0.5f; // default exposure level: 50% +const float defaultAdjustmentRate = 0.1f; // how fast your eyes adjust +const float defaultLimitHigh = 4.f; // your aperture can increase to see something 4 times darker. +const float defaultLimitLow = 0.1f; // your aperture can decrease to see something 10 times brighter. constexpr float defaultLumaRed = 0.2126f; // how much to weigh red light for luma (ITU 709) constexpr float defaultLumaGreen = 0.7152f; // how much to weigh green light for luma (ITU 709) constexpr float defaultLumaBlue = 0.0722f; // how much to weigh blue light for luma (ITU 709) @@ -939,18 +1020,18 @@ bool hdrEnabled = true; #else ConsoleVariable cvar_hdrBrightness("/hdr_brightness", defaultBrightness); static ConsoleVariable cvar_hdrMultithread("/hdr_multithread", defaultMultithread); -static ConsoleVariable cvar_hdrExposure("/hdr_exposure", defaultExposure); -static ConsoleVariable cvar_hdrGamma("/hdr_gamma", defaultGamma); -static ConsoleVariable cvar_hdrAdjustment("/hdr_adjust_rate", defaultAdjustmentRate); -static ConsoleVariable cvar_hdrLimitHigh("/hdr_limit_high", defaultLimitHigh); -static ConsoleVariable cvar_hdrLimitLow("/hdr_limit_low", defaultLimitLow); +ConsoleVariable cvar_hdrExposure("/hdr_exposure", defaultExposure); +ConsoleVariable cvar_hdrGamma("/hdr_gamma", defaultGamma); +ConsoleVariable cvar_hdrAdjustment("/hdr_adjust_rate", defaultAdjustmentRate); +ConsoleVariable cvar_hdrLimitHigh("/hdr_limit_high", defaultLimitHigh); +ConsoleVariable cvar_hdrLimitLow("/hdr_limit_low", defaultLimitLow); static ConsoleVariable cvar_hdrSamples("/hdr_samples", defaultSamples); static ConsoleVariable cvar_hdrLuma("/hdr_luma", Vector4{defaultLumaRed, defaultLumaGreen, defaultLumaBlue, 0.f}); bool hdrEnabled = true; #endif static int oldViewport[4]; - +static float fogFadeAmount[MAXPLAYERS] = { 0.5f }; void glBeginCamera(view_t* camera, bool useHDR, map_t& map) { if (!camera) { @@ -970,6 +1051,16 @@ void glBeginCamera(view_t* camera, bool useHDR, map_t& map) fog_color.w = 1.f; #endif + int lightmapIndex = 0; + int player = -1; + for ( int c = 0; c < MAXPLAYERS; ++c ) { + if ( camera == &cameras[c] ) { + lightmapIndex = c + 1; + player = c; + break; + } + } + if (hdr) { const int numFbs = sizeof(view_t::fb) / sizeof(view_t::fb[0]); const int fbIndex = camera->drawnFrames % numFbs; @@ -979,6 +1070,13 @@ void glBeginCamera(view_t* camera, bool useHDR, map_t& map) GL_CHECK_ERR(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); GL_CHECK_ERR(glScissor(0, 0, camera->winw, camera->winh)); } else { +#ifndef EDITOR + if ( *cvar_fogDistance > 0.f && player == 0 ) + { + GL_CHECK_ERR(glClearColor(fog_color.x, fog_color.y, fog_color.z, fog_color.w)); + GL_CHECK_ERR(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); + } +#endif GL_CHECK_ERR(glGetIntegerv(GL_VIEWPORT, oldViewport)); GL_CHECK_ERR(glViewport(camera->winx, yres - camera->winh - camera->winy, camera->winw, camera->winh)); GL_CHECK_ERR(glScissor(camera->winx, yres - camera->winh - camera->winy, camera->winw, camera->winh)); @@ -1019,16 +1117,38 @@ void glBeginCamera(view_t* camera, bool useHDR, map_t& map) mapDims.y = map.height; // upload lightmap - int lightmapIndex = 0; - for (int c = 0; c < MAXPLAYERS; ++c) { - if (camera == &cameras[c]) { - lightmapIndex = c + 1; - break; - } - } fillSmoothLightmap(lightmapIndex, map); loadLightmapTexture(lightmapIndex, map); +#ifndef EDITOR + float fogDistance = *cvar_fogDistance; + if ( *cvar_fogDistance > 0.f ) + { + if ( player >= 0 ) + { + if ( players[player]->entity ) + { + Sint32 PER = std::min(50, std::max(0, statGetPER(stats[player], players[player]->entity))); + if ( darkmap ) + { + PER = std::min(3, PER); + } + float mult = 0.5 + 0.5 * sin(pow((PER / 50.0), 0.5) * PI / 2); + const float fpsScale = getFPSScale(144.0); + if ( mult > fogFadeAmount[player] ) + { + fogFadeAmount[player] = std::min(mult, fogFadeAmount[player] + 0.001f * fpsScale); + } + else + { + fogFadeAmount[player] = std::max(mult, fogFadeAmount[player] - 0.001f * fpsScale); + } + *cvar_fogDistance *= fogFadeAmount[player]; + } + } + } +#endif + // upload uniforms uploadUniforms(voxelShader, (float*)&proj, (float*)&view, (float*)&mapDims); uploadUniforms(voxelBrightShader, (float*)&proj, (float*)&view, nullptr); @@ -1042,6 +1162,10 @@ void glBeginCamera(view_t* camera, bool useHDR, map_t& map) uploadUniforms(spriteDitheredShader, (float*)&proj, (float*)&view, (float*)&mapDims); uploadUniforms(spriteBrightShader, (float*)&proj, (float*)&view, nullptr); uploadUniforms(spriteUIShader, (float*)&proj, (float*)&view, nullptr); + +#ifndef EDITOR + *cvar_fogDistance = fogDistance; +#endif } #include @@ -1223,16 +1347,26 @@ void glDrawVoxel(view_t* camera, Entity* entity, int mode) { #ifdef EDITOR false; #else - (entity->monsterEntityRenderAsTelepath == 1 && !intro + /*(entity->monsterEntityRenderAsTelepath == 2 && !intro) + || */ + ((entity->monsterEntityRenderAsTelepath == 1 && !intro && player >= 0 && player < MAXPLAYERS && players[player] && players[player]->entity - && stats[player]->mask && stats[player]->mask->type == TOOL_BLINDFOLD_TELEPATHY); + && stats[player]->mask && stats[player]->mask->type == TOOL_BLINDFOLD_TELEPATHY)); + + if ( !intro && player >= 0 && player < MAXPLAYERS && players[player] && players[player]->entity + && (entity->goldTelepathy > 0 && entity->behavior == &actGoldBag && entity->goldTelepathy & (1 << player) + || (entity->colliderTelepathy > 0 && entity->behavior == &actColliderDecoration && entity->colliderTelepathy & (1 << player))) ) + { + telepath = true; + } #endif bool changedDepthRange = false; if (entity->flags[OVERDRAW] || telepath || modelindex == FOLLOWER_SELECTED_PARTICLE - || modelindex == FOLLOWER_TARGET_PARTICLE ) { + || modelindex == FOLLOWER_TARGET_PARTICLE + || (modelindex >= PINPOINT_PARTICLE_START && modelindex < PINPOINT_PARTICLE_END)) { changedDepthRange = true; GL_CHECK_ERR(glDepthRange(0, 0.1)); } @@ -1241,7 +1375,11 @@ void glDrawVoxel(view_t* camera, Entity* entity, int mode) { auto& dither = entity->dithering[camera]; auto& shader = !entity->flags[BRIGHT] && !telepath ? (dither.value < Entity::Dither::MAX ? voxelDitheredShader : voxelShader) : - ((entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER] && dither.value < Entity::Dither::MAX) ? voxelBrightDitheredShader : voxelBrightShader); + ((((entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER]) + || entity->mistformGLRender >= 0.45 + || entity->flags[INVISIBLE_DITHER]) + && dither.value < Entity::Dither::MAX) + ? voxelBrightDitheredShader : voxelBrightShader); shader.bind(); // upload dither amount, if necessary @@ -1266,14 +1404,28 @@ void glDrawVoxel(view_t* camera, Entity* entity, int mode) { (void)rotate_mat(&m, &t, rotx, &i.x); t = m; // roll GL_CHECK_ERR(glUniformMatrix4fv(shader.uniform("uProj"), 1, false, (float*)&camera->proj_hud)); } - rotx = entity->roll * 180.0 / PI; // roll - roty = 360.0 - entity->yaw * 180.0 / PI; // yaw - rotz = 360.0 - entity->pitch * 180.0 / PI; // pitch + v = vec4(entity->x * 2.f, -entity->z * 2.f - 1, entity->y * 2.f, 0.f); (void)translate_mat(&m, &t, &v); t = m; - (void)rotate_mat(&m, &t, roty, &i.y); t = m; // yaw - (void)rotate_mat(&m, &t, rotz, &i.z); t = m; // pitch - (void)rotate_mat(&m, &t, rotx, &i.x); t = m; // roll + +#ifndef EDITOR + if ( (modelindex >= PINPOINT_PARTICLE_START && modelindex < PINPOINT_PARTICLE_END) && entity->behavior == &actParticlePinpointTarget ) + { + // billboard + (void)rotate_mat(&m, &t, entity->flags[OVERDRAW] ? -90.f : + -90.f - camera->ang * (180.f / PI), &i.y); t = m; + } + else +#endif + { + rotx = entity->roll * 180.0 / PI; // roll + roty = 360.0 - entity->yaw * 180.0 / PI; // yaw + rotz = 360.0 - entity->pitch * 180.0 / PI; // pitch + (void)rotate_mat(&m, &t, roty, &i.y); t = m; // yaw + (void)rotate_mat(&m, &t, rotz, &i.z); t = m; // pitch + (void)rotate_mat(&m, &t, rotx, &i.x); t = m; // roll + } + v = vec4(entity->focalx * 2.f, -entity->focalz * 2.f, entity->focaly * 2.f, 0.f); (void)translate_mat(&m, &t, &v); t = m; v = vec4(entity->scalex, entity->scaley, entity->scalez, 0.f); @@ -1283,6 +1435,30 @@ void glDrawVoxel(view_t* camera, Entity* entity, int mode) { // upload light variables if (entity->flags[BRIGHT]) { mat4x4_t remap(1.f); + if ( entity->mistformGLRender >= 0.45 ) + { +#ifndef EDITOR + auto& whichColor = (entity->mistformGLRender > 1.9) ? cvar_color_hologram + : (entity->mistformGLRender > 0.9) ? cvar_color_mist_form + : ((entity->mistformGLRender >= 0.4 && entity->mistformGLRender <= 0.6) ? cvar_color_reflector_shield + : (entity->mistformGLRender >= 0.2 && entity->mistformGLRender <= 0.4) ? cvar_color_force_shield + : cvar_color_mist_form); + vec4_t hsv; + hsv.y = 100.f; // saturation + hsv.z = 100.f; // value + hsv.w = 0.f; // unused + + const auto amp = 360.0; + hsv.x = whichColor->x * amp; + HSVtoRGB(&remap.x, &hsv); // red + + hsv.x = whichColor->y * amp + 120; + HSVtoRGB(&remap.y, &hsv); // green + + hsv.x = whichColor->z * amp + 240; + HSVtoRGB(&remap.z, &hsv); // blue +#endif + } GL_CHECK_ERR(glUniformMatrix4fv(shader.uniform("uColorRemap"), 1, false, (float*)&remap)); const float b = std::max(0.5f, camera->luminance * 4.f); const GLfloat factor[4] = { 1.f, 1.f, 1.f, 1.f }; @@ -1323,7 +1499,16 @@ void glDrawVoxel(view_t* camera, Entity* entity, int mode) { GL_CHECK_ERR(glDisableVertexAttribArray(1)); GL_CHECK_ERR(glDisableVertexAttribArray(2)); #endif - + + /* + if ( SDL_Surface* sprite = tiles[83] ) + { + GL_CHECK_ERR(glActiveTexture(GL_TEXTURE2)); + GL_CHECK_ERR(glBindTexture(GL_TEXTURE_2D, texid[(long int)sprite->userdata])); + } + + GL_CHECK_ERR(glActiveTexture(GL_TEXTURE0));*/ + // reset GL state if (entity->flags[OVERDRAW]) { GL_CHECK_ERR(glUniformMatrix4fv(shader.uniform("uProj"), 1, false, (float*)&camera->proj)); @@ -1367,6 +1552,7 @@ Mesh spriteMesh = { #ifndef EDITOR static ConsoleVariable cvar_enemybarDepthRange("/enemybar_depth_range", 0.5); static ConsoleVariable cvar_ulight_factor_min("/sprite_ulight_factor_min", 0.5f); +static ConsoleVariable cvar_ulight_factor_max("/sprite_ulight_factor_max", 1.7f); static ConsoleVariable cvar_ulight_factor_mult("/sprite_ulight_factor_mult", 4.f); #endif @@ -1453,7 +1639,13 @@ void glDrawEnemyBarSprite(view_t* camera, int mode, int playerViewport, void* en GL_CHECK_ERR(glEnable(GL_BLEND)); // upload light variables - const float b = std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); + const float b = *cvar_hdrLimitLow > 1.f ? + // fortress fog, limit the high compensation + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, + std::min(camera->luminance * *cvar_ulight_factor_mult, *cvar_ulight_factor_max)) + : + // standard levels + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); const GLfloat factor[4] = { 1.f, 1.f, 1.f, (float)enemybar->animator.fadeOut / 100.f }; GL_CHECK_ERR(glUniform4fv(shader.uniform("uLightFactor"), 1, factor)); const GLfloat light[4] = { b, b, b, 1.f }; @@ -1463,9 +1655,10 @@ void glDrawEnemyBarSprite(view_t* camera, int mode, int playerViewport, void* en const float cameraPos[4] = {(float)camera->x * 32.f, -(float)camera->z, (float)camera->y * 32.f, 1.f}; GL_CHECK_ERR(glUniform4fv(shader.uniform("uCameraPos"), 1, cameraPos)); + // draw spriteMesh.draw(); - + // reset GL state GL_CHECK_ERR(glDepthRange(0, 1)); GL_CHECK_ERR(glDisable(GL_BLEND)); @@ -1555,7 +1748,13 @@ void glDrawWorldDialogueSprite(view_t* camera, void* worldDialogue, int mode) GL_CHECK_ERR(glUniformMatrix4fv(shader.uniform("uModel"), 1, false, (float*)&m)); // model matrix // upload light variables - const float b = std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); + const float b = *cvar_hdrLimitLow > 1.f ? + // fortress fog, limit the high compensation + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, + std::min(camera->luminance * *cvar_ulight_factor_mult, *cvar_ulight_factor_max)) + : + // standard levels + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); const GLfloat factor[4] = { 1.f, 1.f, 1.f, (float)dialogue->alpha }; GL_CHECK_ERR(glUniform4fv(shader.uniform("uLightFactor"), 1, factor)); const GLfloat light[4] = { b, b, b, 1.f }; @@ -1683,9 +1882,12 @@ void glDrawWorldUISprite(view_t* camera, Entity* entity, int mode) } else { scale += (0.05f * ((*MainMenu::cvar_worldtooltip_scale / 100.f) - 1.f)); } - + + const real_t zOffset = (entity->behavior == &actSpriteWorldTooltip + && player >= 0 && players[player]->entity) ? players[player]->worldUI.modifiedTooltipDrawHeight : 0.0; + // model matrix - v = vec4(entity->x * 2, -entity->z * 2 - 1, entity->y * 2, 0.f); + v = vec4(entity->x * 2, -(entity->z + zOffset) * 2 - 1, entity->y * 2, 0.f); (void)translate_mat(&m, &t, &v); t = m; (void)rotate_mat(&m, &t, -90.f - camera->ang * (180.f / PI), &i.y); t = m; (void)rotate_mat(&m, &t, -camera->vang * (180.f / PI), &i.x); t = m; @@ -1694,7 +1896,14 @@ void glDrawWorldUISprite(view_t* camera, Entity* entity, int mode) GL_CHECK_ERR(glUniformMatrix4fv(shader.uniform("uModel"), 1, false, (float*)&m)); // model matrix // upload light variables - const float b = std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); + const float b = *cvar_hdrLimitLow > 1.f ? + // fortress fog, limit the high compensation + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, + std::min(camera->luminance * *cvar_ulight_factor_mult, *cvar_ulight_factor_max)) + : + // standard levels + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); + const GLfloat factor[4] = { 1.f, 1.f, 1.f, (float)entity->worldTooltipAlpha }; GL_CHECK_ERR(glUniform4fv(shader.uniform("uLightFactor"), 1, factor)); const GLfloat light[4] = { b, b, b, 1.f }; @@ -1704,6 +1913,7 @@ void glDrawWorldUISprite(view_t* camera, Entity* entity, int mode) const float cameraPos[4] = {(float)camera->x * 32.f, -(float)camera->z, (float)camera->y * 32.f, 1.f}; GL_CHECK_ERR(glUniform4fv(shader.uniform("uCameraPos"), 1, cameraPos)); + // draw spriteMesh.draw(); @@ -1736,7 +1946,32 @@ void glDrawSprite(view_t* camera, Entity* entity, int mode) } else { sprite = sprites[0]; } - GL_CHECK_ERR(glBindTexture(GL_TEXTURE_2D, texid[(long int)sprite->userdata])); + + bool transparentDisableDepthBuffer = false; +#ifndef EDITOR + if ( entity->behavior == &actMagicRangefinder || + (entity->behavior == &actSprite && entity->actSpriteUseCustomSurface > 0 && (entity->entityHasString("aoe_indicator"))) ) + { + sprite = AOEIndicators_t::getSurface(entity->actSpriteUseCustomSurface); + if ( !sprite ) + { + return; + } + if ( auto tex = AOEIndicators_t::getTexture(entity->actSpriteUseCustomSurface) ) + { + GL_CHECK_ERR(glBindTexture(GL_TEXTURE_2D, tex->texid)); + transparentDisableDepthBuffer = true; + } + else + { + return; + } + } + else +#endif + { + GL_CHECK_ERR(glBindTexture(GL_TEXTURE_2D, texid[(long int)sprite->userdata])); + } // set GL state if (mode == REALCOLORS) { @@ -1784,8 +2019,24 @@ void glDrawSprite(view_t* camera, Entity* entity, int mode) } v = vec4(entity->x * 2.f, -entity->z * 2.f - 1, entity->y * 2.f, 0.f); (void)translate_mat(&m, &t, &v); t = m; - (void)rotate_mat(&m, &t, entity->flags[OVERDRAW] ? -90.f : - -90.f - camera->ang * (180.f / PI), &i.y); t = m; + + if ( (entity->actSpriteNoBillboard != 0 && entity->behavior == &actSprite) || entity->behavior == &actMagicRangefinder ) + { + // dont draw billboard + const float rotx = entity->roll * 180.0 / PI; // roll + const float roty = 360.0 - entity->yaw * 180.0 / PI; // yaw + const float rotz = 360.0 - entity->pitch * 180.0 / PI; // pitch + (void)rotate_mat(&m, &t, roty, &i.y); t = m; // yaw + (void)rotate_mat(&m, &t, rotz, &i.z); t = m; // pitch + (void)rotate_mat(&m, &t, rotx, &i.x); t = m; // roll + } + else + { + // billboard + (void)rotate_mat(&m, &t, entity->flags[OVERDRAW] ? -90.f : + -90.f - camera->ang * (180.f / PI), &i.y); t = m; + } + v = vec4(entity->focalx * 2.f, -entity->focalz * 2.f, entity->focaly * 2.f, 0.f); (void)translate_mat(&m, &t, &v); t = m; v = vec4(entity->scalex * sprite->w, entity->scaley * sprite->h, entity->scalez, 0.f); @@ -1795,14 +2046,35 @@ void glDrawSprite(view_t* camera, Entity* entity, int mode) // upload light variables if (entity->flags[BRIGHT]) { #ifndef EDITOR - const float b = std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); + const float b = *cvar_hdrLimitLow > 1.f ? + // fortress fog, limit the high compensation + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, + std::min(camera->luminance * *cvar_ulight_factor_mult, *cvar_ulight_factor_max)) + : + // standard levels + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); #else const float b = std::max(0.5f, camera->luminance * 4.f); #endif const GLfloat factor[4] = { 1.f, 1.f, 1.f, 1.f }; GL_CHECK_ERR(glUniform4fv(shader.uniform("uLightFactor"), 1, factor)); - const GLfloat light[4] = { b, b, b, 1.f }; - GL_CHECK_ERR(glUniform4fv(shader.uniform("uLightColor"), 1, light)); + if ( entity->actSpriteUseAlpha != 0 && entity->behavior == &actSprite ) + { + // use alpha + const GLfloat light[4] = { b, b, b, (float)entity->fskill[1]}; + GL_CHECK_ERR(glUniform4fv(shader.uniform("uLightColor"), 1, light)); + } + else if ( entity->behavior == &actMagicRangefinder ) + { + // use alpha + const GLfloat light[4] = { b * (float)entity->fskill[1], b * (float)entity->fskill[2], b * (float)entity->fskill[3], (float)entity->fskill[0]}; + GL_CHECK_ERR(glUniform4fv(shader.uniform("uLightColor"), 1, light)); + } + else + { + const GLfloat light[4] = { b, b, b, 1.f }; + GL_CHECK_ERR(glUniform4fv(shader.uniform("uLightColor"), 1, light)); + } const GLfloat empty[4] = { 0.f, 0.f, 0.f, 0.f }; GL_CHECK_ERR(glUniform4fv(shader.uniform("uColorAdd"), 1, empty)); const float cameraPos[4] = {(float)camera->x * 32.f, -(float)camera->z, (float)camera->y * 32.f, 1.f}; @@ -1811,6 +2083,11 @@ void glDrawSprite(view_t* camera, Entity* entity, int mode) uploadLightUniforms(camera, shader, entity, mode, false); } + if ( transparentDisableDepthBuffer ) + { + GL_CHECK_ERR(glDepthMask(GL_FALSE)); + } + // draw spriteMesh.draw(); @@ -1824,6 +2101,11 @@ void glDrawSprite(view_t* camera, Entity* entity, int mode) if (entity->flags[OVERDRAW] || entity->behavior == &actDamageGib) { GL_CHECK_ERR(glDepthRange(0.f, 1.f)); } + + if ( transparentDisableDepthBuffer ) + { + GL_CHECK_ERR(glDepthMask(GL_TRUE)); + } } void glDrawSpriteFromImage(view_t* camera, Entity* entity, std::string text, int mode, bool useTextAsImgPath, bool rotate) @@ -1946,7 +2228,13 @@ void glDrawSpriteFromImage(view_t* camera, Entity* entity, std::string text, int // upload light variables #ifndef EDITOR - const float b = std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); + const float b = *cvar_hdrLimitLow > 1.f ? + // fortress fog, limit the high compensation + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, + std::min(camera->luminance * *cvar_ulight_factor_mult, *cvar_ulight_factor_max)) + : + // standard levels + std::max(*MainMenu::cvar_hdrEnabled ? *cvar_ulight_factor_min : 1.f, camera->luminance * *cvar_ulight_factor_mult); #else const float b = std::max(0.5f, camera->luminance * 4.f); #endif @@ -2065,11 +2353,7 @@ void glDrawWorld(view_t* camera, int mode) return; } #endif - - // determine whether we should draw clouds, and their texture - int cloudtile; - const bool clouds = shouldDrawClouds(map, &cloudtile, false); - + // select texture atlas constexpr int numTileAtlases = sizeof(AnimatedTile::indices) / sizeof(AnimatedTile::indices[0]); const int atlasIndex = (ticks % (numTileAtlases * 10)) / 10; @@ -2171,11 +2455,19 @@ void glDrawWorld(view_t* camera, int mode) const bool allowChunkRebuild = *cvar_allowChunkRebuild; #endif + // determine whether we should draw clouds, and their texture + int cloudtile; + const bool clouds = shouldDrawClouds(map, &cloudtile, false); + // build chunks if (allowChunkRebuild) { - for (auto& pair : chunksToBuild) { - auto& chunk = *pair.second; - chunk.build(map, !clouds, chunk.x, chunk.y, chunk.w, chunk.h); + if ( chunksToBuild.size() > 0 ) + { + bool rebuildClouds = shouldDrawClouds(map); // force check for clouds regardless of fog so we don't rebuild the map wrong + for (auto& pair : chunksToBuild) { + auto& chunk = *pair.second; + chunk.build(map, !rebuildClouds, chunk.x, chunk.y, chunk.w, chunk.h); + } } } @@ -2250,6 +2542,10 @@ unsigned int GO_GetPixelU32(int x, int y, view_t& camera) main_framebuffer.unbindForWriting(); } +#ifndef EDITOR + float fogDistance = *cvar_fogDistance; + *cvar_fogDistance = 0.f; +#endif if (dirty) { GL_CHECK_ERR(glClearColor(0.f, 0.f, 0.f, 0.f)); GL_CHECK_ERR(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); @@ -2258,6 +2554,9 @@ unsigned int GO_GetPixelU32(int x, int y, view_t& camera) drawEntities3D(&camera, ENTITYUIDS); glEndCamera(&camera, false, map); } +#ifndef EDITOR + *cvar_fogDistance = fogDistance; +#endif GLubyte pixel[4]; GL_CHECK_ERR(glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, (void*)pixel)); diff --git a/src/paths.cpp b/src/paths.cpp index 4d8d6b2a9..bd33548b1 100644 --- a/src/paths.cpp +++ b/src/paths.cpp @@ -373,6 +373,12 @@ int pathCheckObstacle(int x, int y, Entity* my, Entity* target) || entity->sprite == 177 // shrine || entity->sprite == 178 // spell shrine || entity->sprite == 1481 // daedalus shrine + || entity->sprite == 217 // iron door + || entity->sprite == 218 // iron door + || entity->sprite == 300 // cauldron + || entity->sprite == 301 // workbench + || entity->sprite == 302 // mailbox + || entity->sprite == 303 // mailbox ) { if ( (int)floor(entity->x / 16) == u && (int)floor(entity->y / 16) == v ) @@ -576,7 +582,7 @@ list_t* generatePath(int x1, int y1, int x2, int y2, Entity* my, Entity* target, if ( entity->flags[PASSABLE] ) { if ( entity->behavior == &actSpearTrap - && (my->getRace() == HUMAN || my->monsterAllyGetPlayerLeader()) ) + && ((my && my->getRace() == HUMAN) || my->monsterAllyGetPlayerLeader()) ) { // humans/followers know better than that! @@ -621,7 +627,10 @@ list_t* generatePath(int x1, int y1, int x2, int y2, Entity* my, Entity* target, continue; } } - if ( entity->behavior == &actDoorFrame || entity->behavior == &actDoor || entity->behavior == &actMagicMissile ) + if ( entity->behavior == &actDoorFrame + || entity->behavior == &actDoor + || (entity->behavior == &actIronDoor && (entity->doorLocked == 0)) + || entity->behavior == &actMagicMissile ) { continue; } @@ -633,30 +642,40 @@ list_t* generatePath(int x1, int y1, int x2, int y2, Entity* my, Entity* target, { continue; } - if ( entity->behavior == &actMonster && (!my->checkEnemy(entity) && !entity->isInertMimic()) ) + if ( entity->behavior == &actMonster && + ((!my->checkEnemy(entity) && !entity->isInertMimic()) + || (my && my->getMonsterTypeFromSprite() == DUCK_SMALL) + || entity->sprite == 1822 /* fire sprite*/) ) { continue; } if ( entity->behavior == &actPlayer && my->monsterAllyIndex >= 0 - && (my->monsterTarget == 0 || my->monsterAllyState == ALLY_STATE_MOVETO) ) + /*&& (my->monsterTarget == 0 || my->monsterAllyState == ALLY_STATE_MOVETO)*/ ) { continue; } - if ( lavaIsPassable && - (entity->sprite == 41 - || lavatiles[map.tiles[static_cast(entity->y / 16) * MAPLAYERS + static_cast(entity->x / 16) * MAPLAYERS * map.height]] - || swimmingtiles[map.tiles[static_cast(entity->y / 16) * MAPLAYERS + static_cast(entity->x / 16) * MAPLAYERS * map.height]]) - ) + if ( lavaIsPassable ) { - //Fix to make ladders generate in hell. - continue; + int x = std::min(std::max(0, entity->x / 16), map.width - 1); + int y = std::min(std::max(0, entity->y / 16), map.height - 1); + if ( entity->sprite == 41 + || lavatiles[map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]] + || swimmingtiles[map.tiles[y * MAPLAYERS + x * MAPLAYERS * map.height]] ) + { + //Fix to make ladders generate in hell. + continue; + } } if (playerCheckAchievement && (entity->behavior == &actMonster || entity->behavior == &actPlayer)) { continue; } - if (stats && stats->type == MINOTAUR && (entity->behavior == &actBoulder || entity->behavior == &::actDaedalusShrine || (entity->isDamageableCollider() - && (entity->colliderHasCollision & EditorEntityData_t::COLLIDER_COLLISION_FLAG_MINO)))) + if (stats && stats->type == MINOTAUR + && (entity->behavior == &actBoulder + || entity->behavior == &::actDaedalusShrine + || entity->behavior == &actIronDoor + || (entity->isDamageableCollider() + && (entity->colliderHasCollision & EditorEntityData_t::COLLIDER_COLLISION_FLAG_MINO)))) { // minotaurs bust through boulders, not an obstacle continue; @@ -897,8 +916,8 @@ list_t* generatePath(int x1, int y1, int x2, int y2, Entity* my, Entity* target, auto now = std::chrono::high_resolution_clock::now(); ms = std::chrono::duration_cast(now - pathtime); DebugStats.gui2 = DebugStats.gui2 + ms; - messagePlayer(0, MESSAGE_DEBUG, "FAIL (%d) sprite: %d uid: %d : path tries: %d (%d, %d) to (%d, %d)", - (int)pathingType, my->sprite, my->getUID(), tries, x1, y1, x2, y2); + messagePlayer(0, MESSAGE_DEBUG, "FAIL (%d) sprite: %d uid: %d : path tries: %d (%d, %d) to (%d, %d) ms: %.2f", + (int)pathingType, my->sprite, my->getUID(), tries, x1, y1, x2, y2, ms); } lastGeneratePathTries = tries; if (my->behavior == &actMonster) { @@ -1279,6 +1298,18 @@ bool isPathObstacle(Entity* entity) return true; } } + else if ( entity->behavior == &actCauldron ) + { + return false; + } + else if ( entity->behavior == &actWorkbench ) + { + return false; + } + else if ( entity->behavior == &actMailbox ) + { + return false; + } return false; } diff --git a/src/player.cpp b/src/player.cpp index e28f10b76..75e045b43 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -54,7 +54,7 @@ bool gamepad_menux_invert = false; bool gamepad_menuy_invert = false; const int Player::Inventory_t::MAX_SPELLS_X = 4; -const int Player::Inventory_t::MAX_SPELLS_Y = 20; +const int Player::Inventory_t::MAX_SPELLS_Y = 70; const int Player::Inventory_t::MAX_CHEST_X = 4; const int Player::Inventory_t::MAX_CHEST_Y = 3; @@ -306,10 +306,13 @@ void GameController::handleAnalog(int player) virtualDpad.padVirtualDpad = DpadDirection::CENTERED; } + auto& leftStickDeadzone = playerSettings[multiplayer ? 0 : player].leftStickDeadzone; + auto& rightStickDeadzone = playerSettings[multiplayer ? 0 : player].rightStickDeadzone; + if (!players[player]->shootmode || gamePaused) { - const auto rawx = getRawRightXMove() * (playerSettings[multiplayer ? 0 : player].gamepad_rightx_invert * -2 + 1); - const auto rawy = getRawRightYMove() * (playerSettings[multiplayer ? 0 : player].gamepad_righty_invert * -2 + 1); + const auto rawx = getRawRightXMove(player) * (playerSettings[multiplayer ? 0 : player].gamepad_rightx_invert * -2 + 1); + const auto rawy = getRawRightYMove(player) * (playerSettings[multiplayer ? 0 : player].gamepad_righty_invert * -2 + 1); int rightx = rawx * getGamepadMenuXSensitivity(player); int righty = rawy * getGamepadMenuYSensitivity(player); @@ -334,8 +337,8 @@ void GameController::handleAnalog(int player) if ( radialMenuOpen ) { - const auto rawx = getRawRightXMove(); - const auto rawy = getRawRightYMove(); + const auto rawx = getRawRightXMove(player); + const auto rawy = getRawRightYMove(player); const real_t floatx = rawx; const real_t floaty = rawy; @@ -386,8 +389,8 @@ void GameController::handleAnalog(int player) } else if ( rightStickDeadzoneType != DEADZONE_PER_AXIS ) { - const auto rawx = getRawRightXMove() * (playerSettings[multiplayer ? 0 : player].gamepad_rightx_invert * -2 + 1); - const auto rawy = getRawRightYMove() * (playerSettings[multiplayer ? 0 : player].gamepad_righty_invert * -2 + 1); + const auto rawx = getRawRightXMove(player) * (playerSettings[multiplayer ? 0 : player].gamepad_rightx_invert * -2 + 1); + const auto rawy = getRawRightYMove(player) * (playerSettings[multiplayer ? 0 : player].gamepad_righty_invert * -2 + 1); rightx = rawx; righty = rawy; @@ -458,8 +461,8 @@ void GameController::handleAnalog(int player) } else if ( rightStickDeadzoneType == DEADZONE_MAGNITUDE_LINEAR || rightStickDeadzoneType == DEADZONE_MAGNITUDE_HALFPIPE ) { - const auto rawx = getRawRightXMove() * (playerSettings[multiplayer ? 0 : player].gamepad_rightx_invert * -2 + 1); - const auto rawy = getRawRightYMove() * (playerSettings[multiplayer ? 0 : player].gamepad_righty_invert * -2 + 1); + const auto rawx = getRawRightXMove(player) * (playerSettings[multiplayer ? 0 : player].gamepad_rightx_invert * -2 + 1); + const auto rawy = getRawRightYMove(player) * (playerSettings[multiplayer ? 0 : player].gamepad_righty_invert * -2 + 1); floatx = rawx; floaty = rawy; @@ -560,7 +563,7 @@ int GameController::getRightXMove(int player) // with sensitivity { return 0; } - int x = getRawRightXMove(); + int x = getRawRightXMove(player); x *= playerSettings[multiplayer ? 0 : player].gamepad_rightx_invert * -2 + 1; x *= getGamepadRightXSensitivity(player); return x; @@ -572,7 +575,7 @@ int GameController::getRightYMove(int player) // with sensitivity { return 0; } - int y = getRawRightYMove(); + int y = getRawRightYMove(player); y *= playerSettings[multiplayer ? 0 : player].gamepad_righty_invert * -2 + 1; y *= getGamepadRightYSensitivity(player); return y; @@ -581,7 +584,7 @@ int GameController::getRightYMove(int player) // with sensitivity int GameController::getLeftTrigger() { return getRawLeftTrigger(); } //No sensitivity taken into account (yet) int GameController::getRightTrigger() { return getRawRightTrigger(); } //No sensitivity taken into account (yet) -int GameController::getRawLeftXMove() // no sensitivity +int GameController::getRawLeftXMove(int player) // no sensitivity { if (!isActive()) { @@ -592,6 +595,7 @@ int GameController::getRawLeftXMove() // no sensitivity #else int x = SDL_GameControllerGetAxis(sdl_device, SDL_CONTROLLER_AXIS_LEFTX); #endif + auto& leftStickDeadzone = playerSettings[multiplayer ? 0 : player].leftStickDeadzone; if ( leftStickDeadzoneType == DEADZONE_PER_AXIS ) { if (x < leftStickDeadzone && x > -leftStickDeadzone ) @@ -610,7 +614,7 @@ int GameController::getRawLeftXMove() // no sensitivity return (!gamepad_leftx_invert) ? x : -x; } -int GameController::getRawLeftYMove() // no sensitivity +int GameController::getRawLeftYMove(int player) // no sensitivity { if (!isActive()) { @@ -621,6 +625,7 @@ int GameController::getRawLeftYMove() // no sensitivity #else int y = SDL_GameControllerGetAxis(sdl_device, SDL_CONTROLLER_AXIS_LEFTY); #endif + auto& leftStickDeadzone = playerSettings[multiplayer ? 0 : player].leftStickDeadzone; if ( leftStickDeadzoneType == DEADZONE_PER_AXIS ) { if (y < leftStickDeadzone && y > -leftStickDeadzone ) @@ -639,7 +644,7 @@ int GameController::getRawLeftYMove() // no sensitivity return (!gamepad_lefty_invert) ? -y : y; } -int GameController::getRawRightXMove() // no sensitivity +int GameController::getRawRightXMove(int player) // no sensitivity { if (!isActive()) { @@ -650,6 +655,7 @@ int GameController::getRawRightXMove() // no sensitivity #else int x = SDL_GameControllerGetAxis(sdl_device, SDL_CONTROLLER_AXIS_RIGHTX); #endif + auto& rightStickDeadzone = playerSettings[multiplayer ? 0 : player].rightStickDeadzone; if ( rightStickDeadzoneType == DEADZONE_PER_AXIS ) { if (x < rightStickDeadzone && x > -rightStickDeadzone ) @@ -668,7 +674,7 @@ int GameController::getRawRightXMove() // no sensitivity return (!gamepad_rightx_invert) ? x : -x; } -int GameController::getRawRightYMove() // no sensitivity +int GameController::getRawRightYMove(int player) // no sensitivity { if (!isActive()) { @@ -679,6 +685,7 @@ int GameController::getRawRightYMove() // no sensitivity #else int y = SDL_GameControllerGetAxis(sdl_device, SDL_CONTROLLER_AXIS_RIGHTY); #endif + auto& rightStickDeadzone = playerSettings[multiplayer ? 0 : player].rightStickDeadzone; if ( rightStickDeadzoneType == DEADZONE_PER_AXIS ) { if (y < rightStickDeadzone && y > -rightStickDeadzone ) @@ -735,9 +742,9 @@ int GameController::getRawRightTrigger() return n; } -float GameController::getLeftXPercentForPlayerMovement() +float GameController::getLeftXPercentForPlayerMovement(int player) { - float x_force = getLeftXPercent(); + float x_force = getLeftXPercent(player); if ( x_force > 0 ) { x_force = std::min(x_force / x_forceMaxForwardThreshold, 1.f); @@ -748,9 +755,9 @@ float GameController::getLeftXPercentForPlayerMovement() } return x_force; } -float GameController::getLeftYPercentForPlayerMovement() +float GameController::getLeftYPercentForPlayerMovement(int player) { - float y_force = getLeftYPercent(); + float y_force = getLeftYPercent(player); if ( y_force > 0 ) { y_force = std::min(y_force / y_forceMaxStrafeThreshold, 1.f); @@ -762,19 +769,19 @@ float GameController::getLeftYPercentForPlayerMovement() return y_force; } -float GameController::getLeftXPercent() { return (float)getRawLeftXMove() / (float)maxLeftXMove(); } -float GameController::getLeftYPercent() { return (float)getRawLeftYMove() / (float)maxLeftYMove(); } -float GameController::getRightXPercent() { return (float)getRawRightXMove() / (float)maxRightXMove(); } -float GameController::getRightYPercent() { return (float)getRawRightYMove() / (float)maxRightYMove(); } +float GameController::getLeftXPercent(int player) { return (float)getRawLeftXMove(player) / (float)maxLeftXMove(player); } +float GameController::getLeftYPercent(int player) { return (float)getRawLeftYMove(player) / (float)maxLeftYMove(player); } +float GameController::getRightXPercent(int player) { return (float)getRawRightXMove(player) / (float)maxRightXMove(player); } +float GameController::getRightYPercent(int player) { return (float)getRawRightYMove(player) / (float)maxRightYMove(player); } float GameController::getLeftTriggerPercent() { return (float)getRawLeftTrigger() / (float)maxLeftTrigger(); } float GameController::getRightTriggerPercent() { return (float)getRawRightTrigger() / (float)maxRightTrigger(); } //Ya, it's pretty constant in SDL2. -int GameController::maxLeftXMove() { return 32767 - (leftStickDeadzoneType == DEADZONE_PER_AXIS ? leftStickDeadzone : 0); } -int GameController::maxLeftYMove() { return 32767 - (leftStickDeadzoneType == DEADZONE_PER_AXIS ? leftStickDeadzone : 0); } -int GameController::maxRightXMove() { return 32767 - (rightStickDeadzoneType == DEADZONE_PER_AXIS ? rightStickDeadzone : 0); } -int GameController::maxRightYMove() { return 32767 - (rightStickDeadzoneType == DEADZONE_PER_AXIS ? rightStickDeadzone : 0); } +int GameController::maxLeftXMove(int player) { return 32767 - (leftStickDeadzoneType == DEADZONE_PER_AXIS ? playerSettings[multiplayer ? 0 : player].leftStickDeadzone : 0); } +int GameController::maxLeftYMove(int player) { return 32767 - (leftStickDeadzoneType == DEADZONE_PER_AXIS ? playerSettings[multiplayer ? 0 : player].leftStickDeadzone : 0); } +int GameController::maxRightXMove(int player) { return 32767 - (rightStickDeadzoneType == DEADZONE_PER_AXIS ? playerSettings[multiplayer ? 0 : player].rightStickDeadzone : 0); } +int GameController::maxRightYMove(int player) { return 32767 - (rightStickDeadzoneType == DEADZONE_PER_AXIS ? playerSettings[multiplayer ? 0 : player].rightStickDeadzone : 0); } int GameController::maxLeftTrigger() { return 32767 - gamepad_deadzone; } int GameController::maxRightTrigger() { return 32767 - gamepad_deadzone; } @@ -1192,6 +1199,7 @@ bool Player::GUI_t::bModuleAccessibleWithMouse(GUIModules moduleToAccess) || moduleToAccess == MODULE_SHOP || moduleToAccess == MODULE_TINKERING || moduleToAccess == MODULE_FEATHER || moduleToAccess == MODULE_ALCHEMY + || moduleToAccess == MODULE_MAILBOX || moduleToAccess == MODULE_ASSISTSHRINE || moduleToAccess == MODULE_ITEMEFFECTGUI ) { @@ -1333,6 +1341,10 @@ Player::GUI_t::GUIModules Player::GUI_t::handleModuleNavigation(bool checkDestin { return MODULE_NONE; } + else if ( GenericGUI[player.playernum].itemfxGUI.bOpen ) + { + return MODULE_NONE; + } else if ( GenericGUI[player.playernum].featherGUI.bOpen ) { return MODULE_NONE; @@ -1342,6 +1354,14 @@ Player::GUI_t::GUIModules Player::GUI_t::handleModuleNavigation(bool checkDestin return MODULE_NONE; } else if ( GenericGUI[player.playernum].alchemyGUI.bOpen ) + { + return MODULE_NONE; + } + else if ( GenericGUI[player.playernum].mailboxGUI.bOpen ) + { + return MODULE_NONE; + } + else if ( GenericGUI[player.playernum].alchemyGUI.bOpen ) { if ( inputs.getUIInteraction(player.playernum)->selectedItem ) { @@ -1459,6 +1479,14 @@ Player::GUI_t::GUIModules Player::GUI_t::handleModuleNavigation(bool checkDestin } return MODULE_INVENTORY; } + else if ( activeModule == MODULE_ALCHEMY ) + { + return MODULE_NONE; + } + else if ( activeModule == MODULE_MAILBOX ) + { + return MODULE_NONE; + } else if ( activeModule == MODULE_ALCHEMY && (GenericGUI[player.playernum].alchemyGUI.bFirstTimeSnapCursor || checkDestinationOnly) ) { @@ -1698,6 +1726,10 @@ Player::GUI_t::GUIModules Player::GUI_t::handleModuleNavigation(bool checkDestin { return MODULE_NONE; } + else if ( GenericGUI[player.playernum].itemfxGUI.bOpen ) + { + return MODULE_NONE; + } else if ( GenericGUI[player.playernum].featherGUI.bOpen ) { return MODULE_NONE; @@ -1706,6 +1738,14 @@ Player::GUI_t::GUIModules Player::GUI_t::handleModuleNavigation(bool checkDestin { return MODULE_NONE; } + else if ( GenericGUI[player.playernum].alchemyGUI.bOpen ) + { + return MODULE_NONE; + } + else if ( GenericGUI[player.playernum].mailboxGUI.bOpen ) + { + return MODULE_NONE; + } else if ( player.ghost.isActive() ) { if ( bCompactView ) @@ -1824,6 +1864,14 @@ Player::GUI_t::GUIModules Player::GUI_t::handleModuleNavigation(bool checkDestin } return MODULE_INVENTORY; } + else if ( activeModule == MODULE_ALCHEMY ) + { + return MODULE_NONE; + } + else if ( activeModule == MODULE_MAILBOX ) + { + return MODULE_NONE; + } else if ( activeModule == MODULE_ALCHEMY && (GenericGUI[player.playernum].alchemyGUI.bFirstTimeSnapCursor || checkDestinationOnly) ) { @@ -2153,6 +2201,13 @@ bool Player::GUI_t::handleInventoryMovement() GenericGUI[player].alchemyGUI.getSelectedAlchemySlotY(), -1, 0); } + else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX ) + { + select_mail_slot(player, + GenericGUI[player].mailboxGUI.getSelectedMailSlotX(), + GenericGUI[player].mailboxGUI.getSelectedMailSlotY(), + -1, 0); + } else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_ASSISTSHRINE ) { select_assistshrine_slot(player, @@ -2260,6 +2315,13 @@ bool Player::GUI_t::handleInventoryMovement() GenericGUI[player].alchemyGUI.getSelectedAlchemySlotY(), 1, 0); } + else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX ) + { + select_mail_slot(player, + GenericGUI[player].mailboxGUI.getSelectedMailSlotX(), + GenericGUI[player].mailboxGUI.getSelectedMailSlotY(), + 1, 0); + } else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_ASSISTSHRINE ) { select_assistshrine_slot(player, @@ -2419,6 +2481,13 @@ bool Player::GUI_t::handleInventoryMovement() GenericGUI[player].alchemyGUI.getSelectedAlchemySlotY(), 0, -1); } + else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX ) + { + select_mail_slot(player, + GenericGUI[player].mailboxGUI.getSelectedMailSlotX(), + GenericGUI[player].mailboxGUI.getSelectedMailSlotY(), + 0, -1); + } else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_ASSISTSHRINE ) { select_assistshrine_slot(player, @@ -2578,6 +2647,13 @@ bool Player::GUI_t::handleInventoryMovement() GenericGUI[player].alchemyGUI.getSelectedAlchemySlotY(), 0, 1); } + else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_MAILBOX ) + { + select_mail_slot(player, + GenericGUI[player].mailboxGUI.getSelectedMailSlotX(), + GenericGUI[player].mailboxGUI.getSelectedMailSlotY(), + 0, 1); + } else if ( players[player]->GUI.activeModule == Player::GUI_t::MODULE_ASSISTSHRINE ) { select_assistshrine_slot(player, @@ -2812,7 +2888,8 @@ GameController::Haptic_t::HapticEffect* GameController::handleRumble() || rumbleToPlay->second.pattern == Haptic_t::RUMBLE_BOULDER || rumbleToPlay->second.pattern == Haptic_t::RUMBLE_BOULDER_BOUNCE || rumbleToPlay->second.pattern == Haptic_t::RUMBLE_DEATH - || rumbleToPlay->second.pattern == Haptic_t::RUMBLE_TMP)) + || rumbleToPlay->second.pattern == Haptic_t::RUMBLE_TMP + || rumbleToPlay->second.pattern == Haptic_t::RUMBLE_SPELL)) #endif { Uint32 newStartTime = (haptics.hapticTick - rumbleToPlay->second.startTick); @@ -2854,6 +2931,10 @@ void GameController::addRumble(Haptic_t::RumblePattern pattern, Uint16 smallMagn { priority = 5; } + else if ( pattern == Haptic_t::RumblePattern::RUMBLE_SPELL ) + { + priority = 5; + } else { priority = 1; @@ -3045,6 +3126,13 @@ GameController::Haptic_t::HapticEffect* GameController::doRumble(Haptic_t::Rumbl haptics.hapticEffect.large_magnitude = r->largeMagnitude * r->customEffect; haptics.hapticEffect.small_magnitude = r->smallMagnitude * r->customEffect; } + else if ( r->pattern == Haptic_t::RUMBLE_SPELL ) + { + real_t currentPlayheadPercent = r->startTime / static_cast(r->length); + r->customEffect = sin(currentPlayheadPercent * PI); + haptics.hapticEffect.large_magnitude = r->largeMagnitude * r->customEffect; + haptics.hapticEffect.small_magnitude = r->smallMagnitude * r->customEffect; + } else if ( r->pattern == Haptic_t::RUMBLE_DEATH ) { real_t currentPlayheadPercent = r->startTime / static_cast(r->length); @@ -3152,7 +3240,37 @@ void Player::init() // for use on new/restart game, UI related levelUpAnimation[playernum].lvlUps.clear(); skillUpAnimation[playernum].skillUps.clear(); mechanics.itemDegradeRng.clear(); - mechanics.sustainedSpellMPUsed = 0; + mechanics.sustainedSpellIDCounter.clear(); + hamletShopkeeperSkillLimit[playernum].clear(); + mechanics.baseSpellLevelUpProcs.clear(); + mechanics.learnedSpells.clear(); + mechanics.ducksInARow.clear(); + mechanics.pendingDucks.clear(); + mechanics.numFishingCaught = 0; + mechanics.sustainedSpellMPUsedSorcery = 0; + mechanics.sustainedSpellMPUsedMysticism = 0; + mechanics.sustainedSpellMPUsedThaumaturgy = 0; + mechanics.baseSpellMPUsedSorcery = 0; + mechanics.baseSpellMPUsedMysticism = 0; + mechanics.baseSpellMPUsedThaumaturgy = 0; + mechanics.ensemblePlaying = -1; + mechanics.ensembleRequireRecast = false; + mechanics.ensembleTakenInitialMP = false; + mechanics.ensembleDataUpdate = 0; + mechanics.gremlinBreakableCounter = 0; + mechanics.escalatingRngRolls.clear(); + mechanics.escalatingSpellRngRolls.clear(); + mechanics.favoriteBooksAchievement.clear(); + + mechanics.fociDarkChargeTime = 0; + mechanics.fociHolyChargeTime = 0; + mechanics.lastFociHeldType = 0; + + mechanics.donationRevealedOnFloor = 0; + mechanics.donationClaimed = false; + + inventoryUI.appraisal.appraisalProgressionItems.clear(); + inventoryUI.appraisal.manual_appraised_item = 0; } void Player::cleanUpOnEntityRemoval() @@ -3164,8 +3282,33 @@ void Player::cleanUpOnEntityRemoval() worldUI.reset(); } mechanics.enemyRaisedBlockingAgainst.clear(); + mechanics.enemyRaisedStealthAgainst.clear(); + mechanics.targetsCompelled.clear(); + mechanics.targetsRefuseCompel.clear(); + mechanics.ensemblePlaying = -1; + mechanics.ensembleRequireRecast = false; + mechanics.ensembleTakenInitialMP = false; + mechanics.ensembleDataUpdate = 0; selectedEntity[playernum] = nullptr; client_selected[playernum] = nullptr; + magic.telekinesisTarget = 0; + + cast_animation[playernum].overcharge = 0; + cast_animation[playernum].overcharge_init = 0; + + mechanics.fociDarkChargeTime = 0; + mechanics.fociHolyChargeTime = 0; + mechanics.lastFociHeldType = 0; + + mechanics.pendingDucks.clear(); + mechanics.numFishingCaught = 0; + + mechanics.gremlinBreakableCounter = 0; + + mechanics.previouslyLevitating = false; + + mechanics.donationRevealedOnFloor = 0; + mechanics.donationClaimed = false; } const bool Player::isLocalPlayer() const @@ -3236,15 +3379,35 @@ bool monsterIsFriendlyForTooltip(const int player, Entity& entity) { return true; // this is my follower } + if ( entity.getStats() ) + { + if ( entity.getStats()->getEffectActive(EFF_PENANCE) >= 1 && entity.getStats()->getEffectActive(EFF_PENANCE) < 1 + MAXPLAYERS ) + { + return true; + } + } Monster playerRace = stats[player]->type; Monster targetEntityType = entity.getMonsterTypeFromSprite(); if ( targetEntityType == SHOPKEEPER ) { - if ( shopIsMysteriousShopkeeper(&entity) || !ShopkeeperPlayerHostility.isPlayerEnemy(player) ) + if ( shopIsMysteriousShopkeeper(&entity) ) { return true; } + bool hostile = ShopkeeperPlayerHostility.isPlayerEnemy(player); + if ( !hostile && !(entity.getStats() && entity.getStats()->getEffectActive(EFF_CONFUSED)) ) + { + return true; + } + else if ( hostile && (entity.getStats() && entity.getStats()->getEffectActive(EFF_CONFUSED)) ) + { + return true; + } + } + else if ( entity.monsterCanTradeWith(player) ) + { + return true; } else if ( targetEntityType == SUCCUBUS || targetEntityType == INCUBUS ) { @@ -3255,6 +3418,22 @@ bool monsterIsFriendlyForTooltip(const int player, Entity& entity) return true; } } + else if ( targetEntityType == HUMAN && (playerRace == MYCONID || playerRace == DRYAD || playerRace == SALAMANDER) ) + { + return true; + } + else if ( targetEntityType == GOBLIN && (playerRace == GREMLIN) ) + { + return true; + } + else if ( targetEntityType == HUMAN && (playerRace == GNOME) ) + { + return true; + } + else if ( targetEntityType == TROLL && (playerRace == GNOME) ) + { + return true; + } if ( targetEntityType != NOTHING ) { std::map>* allyTable = &Player::SkillSheet_t::skillSheetData.leadershipAllyTableBase; @@ -3330,6 +3509,7 @@ real_t Player::WorldUI_t::tooltipInRange(Entity& tooltip) bool selectInteract = false; bool callout = false; + bool spellInteract = false; if ( FollowerMenu[player.playernum].followerMenuIsOpen() && FollowerMenu[player.playernum].selectMoveTo ) { selectInteract = (FollowerMenu[player.playernum].optionSelected == ALLY_CMD_ATTACK_SELECT); @@ -3341,8 +3521,15 @@ real_t Player::WorldUI_t::tooltipInRange(Entity& tooltip) callout = true; maxDist = 256; } + else if ( cast_animation[player.playernum].active && cast_animation[player.playernum].rangefinder == RANGEFINDER_TOUCH_INTERACT_TEST ) + { + selectInteract = true; + spellInteract = true; + maxDist = 64.0; + } else if ( parent && (parent->getMonsterTypeFromSprite() == SHOPKEEPER + || parent->monsterCanTradeWith(player.playernum) || (parent->behavior == &actFloorDecoration && parent->sprite == 991 /* sign */) || (parent->behavior == &actMonster && (monsterIsFriendlyForTooltip(player.playernum, *parent))) || (parent->monsterAllyGetPlayerLeader() @@ -3350,7 +3537,7 @@ real_t Player::WorldUI_t::tooltipInRange(Entity& tooltip) { maxDist = TOUCHRANGE; } - if ( parent && parent->behavior == &actDoor && parent->doorStatus == 0 ) // min dist 0.0 when door closed, just in case we're stuck inside. + if ( parent && (parent->behavior == &actDoor || parent->behavior == &actIronDoor) && parent->doorStatus == 0 ) // min dist 0.0 when door closed, just in case we're stuck inside. { minDist = 0.0; } @@ -3443,6 +3630,14 @@ real_t Player::WorldUI_t::tooltipInRange(Entity& tooltip) { return 0.0; } + else if ( parent->behavior == &actWallLock ) + { + if ( parent->wallLockState == Entity::WallLockStates::LOCK_KEY_START + || parent->wallLockState == Entity::WallLockStates::LOCK_KEY_ENTER ) + { + return 0.0; + } + } else if ( player.ghost.isActive() && !player.ghost.allowedInteractEntity(*parent) ) { return 0.0; @@ -3474,13 +3669,32 @@ real_t Player::WorldUI_t::tooltipInRange(Entity& tooltip) { if ( !shopIsMysteriousShopkeeper(parent) && ShopkeeperPlayerHostility.isPlayerEnemy(player.playernum) ) { - return 0.0; + bool hostile = ShopkeeperPlayerHostility.isPlayerEnemy(player.playernum); + if ( !hostile && (parent->getStats() && parent->getStats()->getEffectActive(EFF_CONFUSED)) ) + { + return 0.0; + } + else if ( hostile && !(parent->getStats() && parent->getStats()->getEffectActive(EFF_CONFUSED)) ) + { + return 0.0; + } } } + if ( parent->monsterCanTradeWith(player.playernum) && !selectInteract ) + { + /*if ( !shopIsMysteriousShopkeeper(parent) && ShopkeeperPlayerHostility.isPlayerEnemy(player.playernum) ) + { + return 0.0; + }*/ + } if ( parent->getMonsterTypeFromSprite() == BAT_SMALL && !selectInteract ) { return 0.0; } + if ( parent->getMonsterTypeFromSprite() == HOLOGRAM && !selectInteract ) + { + return 0.0; + } if ( parent->behavior == &actPlayer || (parent->behavior == &actMonster && !(parent->isInertMimic())) ) { @@ -3526,7 +3740,7 @@ real_t Player::WorldUI_t::tooltipInRange(Entity& tooltip) } else if ( (parent->behavior == &actTorch || parent->behavior == &actCrystalShard) ) { - if ( callout ) + if ( callout || spellInteract ) { dist += 8.0; // distance penalty when calling out } @@ -3575,7 +3789,7 @@ real_t Player::WorldUI_t::tooltipInRange(Entity& tooltip) { playerEntity->flags[PASSABLE] = false; // hack to make ghosts linetraceable } - lineTraceTarget(&tooltip, tooltip.x, tooltip.y, tangent2, maxDist, 0, false, playerEntity); + lineTraceTarget(&tooltip, tooltip.x, tooltip.y, tangent2, maxDist, LINETRACE_TOOLTIP_INTERACT, false, playerEntity); playerEntity->flags[PASSABLE] = oldPassable; if ( hit.entity != playerEntity ) { @@ -3741,7 +3955,7 @@ real_t Player::WorldUI_t::tooltipInRange(Entity& tooltip) } real_t lookDist = sqrt(pow(previousx - playerEntity->x, 2) + pow(previousy - playerEntity->y, 2)); - if ( callout ) + if ( callout || spellInteract ) { if ( parent ) { @@ -3825,7 +4039,7 @@ void Player::WorldUI_t::setTooltipActive(Entity& tooltip) return; } } - else if ( player.ghost.isActive() && !CalloutMenu[player.playernum].calloutMenuIsOpen() ) + else if ( player.ghost.isActive() && !CalloutMenu[player.playernum].calloutMenuIsOpen() && !FollowerMenu[player.playernum].followerMenuIsOpen() ) { if ( !player.ghost.allowedInteractEntity(*parent) ) { @@ -3967,6 +4181,10 @@ void Player::WorldUI_t::setTooltipActive(Entity& tooltip) { interactText = Language::get(4013) + name; // "Trade with " } + else if ( parent->monsterCanTradeWith(player.playernum) ) + { + interactText = Language::get(4013) + name; // "Trade with " + } else { interactText = Language::get(4014) + name; // "Interact with " @@ -3984,6 +4202,17 @@ void Player::WorldUI_t::setTooltipActive(Entity& tooltip) interactText = Language::get(4016); // "Open door" } } + else if ( parent->behavior == &actIronDoor ) + { + if ( parent->doorStatus != 0 ) + { + interactText = Language::get(6420); // "Close door" + } + else + { + interactText = Language::get(6421); // "Open door" + } + } else if ( parent->behavior == &actGate ) { interactText = Language::get(4017); // "Inspect gate" @@ -4022,6 +4251,18 @@ void Player::WorldUI_t::setTooltipActive(Entity& tooltip) { interactText = Language::get(4024); // "Pull torch" } + else if ( parent->behavior == &actCauldron ) + { + interactText = Language::get(6977); // "Use cauldron" + } + else if ( parent->behavior == &actWorkbench ) + { + interactText = Language::get(6979); // "Use workbench" + } + else if ( parent->behavior == &actMailbox ) + { + interactText = Language::get(6984); // "Use mailbox" + } else if ( parent->behavior == &actFurniture || parent->behavior == &actMCaxe ) { interactText = Language::get(4023); // "Inspect" @@ -4130,7 +4371,7 @@ void Player::WorldUI_t::setTooltipActive(Entity& tooltip) } else if ( parent->behavior == &actTeleporter ) { - if ( parent->teleporterType == 2 ) // portal + if ( parent->teleporterType == 2 || parent->teleporterType == 3 ) // portal { interactText += Language::get(4035); // "Enter portal"; } @@ -4155,6 +4396,34 @@ void Player::WorldUI_t::setTooltipActive(Entity& tooltip) { interactText += Language::get(6354); // "use shrine"; } + else if ( parent->behavior == &::actWallLock ) + { + static char buf[256] = ""; + int wallLockState = parent->wallLockState; + if ( wallLockState == Entity::WallLockStates::LOCK_NO_KEY ) + { + snprintf(buf, sizeof(buf), Language::get(6397), Language::get(6383 + parent->wallLockMaterial)); + interactText = buf; + snprintf(buf, sizeof(buf), Language::get(6402), player.inventoryUI.getKeyAmountForWallLock(*parent)); + interactText += buf; + } + else if ( wallLockState == Entity::WallLockStates::LOCK_KEY_ACTIVE_START + || wallLockState == Entity::WallLockStates::LOCK_KEY_ACTIVE ) + { + snprintf(buf, sizeof(buf), Language::get(6399), Language::get(6383 + parent->wallLockMaterial)); + interactText = buf; // deactivate lock + } + else if ( wallLockState == Entity::WallLockStates::LOCK_KEY_INACTIVE_START + || wallLockState == Entity::WallLockStates::LOCK_KEY_INACTIVE ) + { + snprintf(buf, sizeof(buf), Language::get(6400), Language::get(6383 + parent->wallLockMaterial)); + interactText = buf; // deactivate lock + } + } + else if ( parent->behavior == &::actWallButton ) + { + interactText += Language::get(6401); // press button + } else if ( parent->behavior == &actBomb && parent->skill[21] != 0 ) //skill[21] item type { interactText = Language::get(4039); // "Disarm "; @@ -4313,7 +4582,21 @@ bool entityBlocksTooltipInteraction(const int player, Entity& entity) { return false; } - else if ( entity.behavior == &actDoor || entity.behavior == &actFountain || entity.behavior == &actSink + else if ( entity.behavior == &actCauldron ) + { + return false; + } + else if ( entity.behavior == &actWorkbench ) + { + return false; + } + else if ( entity.behavior == &actMailbox ) + { + return false; + } + else if ( entity.behavior == &actDoor + || entity.behavior == &actIronDoor + || entity.behavior == &actFountain || entity.behavior == &actSink || entity.behavior == &actHeadstone || entity.behavior == &actChest || entity.behavior == &actChestLid || entity.behavior == &actBoulder || entity.behavior == &actPlayer || entity.behavior == &actPedestalOrb || entity.behavior == &actPowerCrystalBase || entity.behavior == &actPowerCrystal @@ -4394,6 +4677,7 @@ void Player::WorldUI_t::handleTooltips() } bool foundTinkeringKit = false; + bool foundInstrument = false; bool radialMenuOpen = FollowerMenu[player].followerMenuIsOpen(); bool selectInteract = false; if ( radialMenuOpen ) @@ -4419,6 +4703,13 @@ void Player::WorldUI_t::handleTooltips() radialMenuOpen = false; selectInteract = (CalloutMenu[player].optionSelected == CalloutRadialMenu::CALLOUT_CMD_SELECT); } + else + { + if ( cast_animation[player].active && cast_animation[player].rangefinder == RANGEFINDER_TOUCH_INTERACT_TEST ) + { + selectInteract = true; + } + } } bool bDoingActionHideTooltips = false; @@ -4444,7 +4735,8 @@ void Player::WorldUI_t::handleTooltips() { bDoingActionHideTooltips = true; } - else if ( (cast_animation[player].active || cast_animation[player].active_spellbook) && !selectInteract && players[player]->entity ) + else if ( ((cast_animation[player].active && !cast_animation[player].spellWaitingAttackInput()) + || cast_animation[player].active_spellbook) && !selectInteract && players[player]->entity ) { // spells bDoingActionHideTooltips = true; @@ -4456,6 +4748,12 @@ void Player::WorldUI_t::handleTooltips() // don't ignore foundTinkeringKit = true; } + else if ( stats[player]->shield && + ((itemTypeIsInstrument(stats[player]->shield->type) + || itemTypeIsFoci(stats[player]->shield->type))) ) + { + foundInstrument = true; + } else { bDoingActionHideTooltips = true; @@ -4470,7 +4768,7 @@ void Player::WorldUI_t::handleTooltips() { Entity* ohitentity = hit.entity; lineTrace(playerEntity, playerEntity->x, playerEntity->y, - playerEntity->yaw, STRIKERANGE, 0, true); + playerEntity->yaw, STRIKERANGE, 0, false); if ( hit.entity ) { if ( hit.entity->behavior == &actMonster && selectInteract @@ -4524,7 +4822,15 @@ void Player::WorldUI_t::handleTooltips() if ( bDoingActionHideTooltips ) { - players[player]->worldUI.gimpDisplayTimer = 10; + if ( stats[player]->weapon && (stats[player]->weapon->type == TOOL_LOCKPICK || stats[player]->weapon->type == TOOL_SKELETONKEY) ) + { + // shorter sequence to allow lockpicking bombs/wall locks faster + players[player]->worldUI.gimpDisplayTimer = 1; + } + else + { + players[player]->worldUI.gimpDisplayTimer = 10; + } continue; } @@ -4551,7 +4857,15 @@ void Player::WorldUI_t::handleTooltips() parent = uidToEntity(tooltip->parent); if ( parent && parent->flags[INVISIBLE] && !(parent->behavior == &actMonster && - (parent->getMonsterTypeFromSprite() == DUMMYBOT || parent->getMonsterTypeFromSprite() == MIMIC || parent->getMonsterTypeFromSprite() == BAT_SMALL)) ) + (parent->getMonsterTypeFromSprite() == DUMMYBOT + || parent->getMonsterTypeFromSprite() == MIMIC + || parent->getMonsterTypeFromSprite() == REVENANT_SKULL + || parent->getMonsterTypeFromSprite() == MINIMIMIC + || parent->getMonsterTypeFromSprite() == FLAME_ELEMENTAL + || parent->getMonsterTypeFromSprite() == EARTH_ELEMENTAL + || parent->getMonsterTypeFromSprite() == MONSTER_ADORCISED_WEAPON + || parent->getMonsterTypeFromSprite() == MOTH_SMALL + || parent->getMonsterTypeFromSprite() == BAT_SMALL)) ) { continue; } @@ -4559,6 +4873,11 @@ void Player::WorldUI_t::handleTooltips() { continue; } + if ( parent && parent->flags[PASSABLE] && parent->behavior == &actMonster + && parent->getMonsterTypeFromSprite() == MONSTER_ADORCISED_WEAPON ) + { + continue; + } real_t newDist = players[player]->worldUI.tooltipInRange(*tooltip); if ( newDist > 0.01 ) { @@ -4647,8 +4966,8 @@ void Player::WorldUI_t::handleTooltips() real_t pitchDiff = players[player]->worldUI.playerLastPitch - currentPitch; if ( inputs.hasController(player) ) { - real_t floatx = inputs.getController(player)->getLeftXPercent(); - real_t floaty = inputs.getController(player)->getLeftYPercent(); + real_t floatx = inputs.getController(player)->getLeftXPercent(player); + real_t floaty = inputs.getController(player)->getLeftYPercent(player); real_t magnitude = sqrt(pow(floaty, 2) + pow(floatx, 2)); if ( magnitude > 0.0 ) { @@ -4701,37 +5020,87 @@ void Player::WorldUI_t::handleTooltips() continue; } - std::array switchStrings = { Language::get(4018), Language::get(4019) }; - bool foundSwitchString = false; - for ( auto s : switchStrings ) - { - if ( players[player]->worldUI.interactText.find(s) != std::string::npos ) - { - foundSwitchString = true; - break; - } - } - for ( auto& tooltip : players[player]->worldUI.tooltipsInRange ) { - if ( players[player]->worldUI.bTooltipActiveForPlayer(*tooltip.first) && foundSwitchString ) + if ( players[player]->worldUI.bTooltipActiveForPlayer(*tooltip.first) ) { if ( Entity* parent = uidToEntity(tooltip.first->parent) ) { if ( parent->behavior == &actSwitch || parent->behavior == &actSwitchWithTimer ) { - if ( parent->skill[0] == 1 ) + std::array switchStrings = { Language::get(4018), Language::get(4019) }; + bool foundSwitchString = false; + for ( auto s : switchStrings ) + { + if ( players[player]->worldUI.interactText.find(s) != std::string::npos ) + { + foundSwitchString = true; + break; + } + } + if ( foundSwitchString ) + { + if ( parent->skill[0] == 1 ) + { + if ( players[player]->worldUI.interactText.find(Language::get(4019)) != std::string::npos ) + { + // rescan, out of date string. + players[player]->worldUI.tooltipView = TOOLTIP_VIEW_RESCAN; + break; + } + } + else + { + if ( players[player]->worldUI.interactText.find(Language::get(4018)) != std::string::npos ) + { + // rescan, out of date string. + players[player]->worldUI.tooltipView = TOOLTIP_VIEW_RESCAN; + break; + } + } + } + } + else if ( parent->behavior == &actWallLock ) + { + std::string wallLockStringNoKey; + std::string wallLockStringActivate; + std::string wallLockStringDeactivate; + static char buf[256]; + snprintf(buf, sizeof(buf), Language::get(6397), Language::get(6383 + parent->wallLockMaterial)); + wallLockStringNoKey = buf; + snprintf(buf, sizeof(buf), Language::get(6402), players[player]->inventoryUI.getKeyAmountForWallLock(*parent)); + wallLockStringNoKey += buf; + + snprintf(buf, sizeof(buf), Language::get(6400), Language::get(6383 + parent->wallLockMaterial)); + wallLockStringActivate = buf; + + snprintf(buf, sizeof(buf), Language::get(6399), Language::get(6383 + parent->wallLockMaterial)); + wallLockStringDeactivate = buf; + + int wallLockState = parent->wallLockState; + if ( wallLockState == Entity::WallLockStates::LOCK_NO_KEY ) { - if ( players[player]->worldUI.interactText.find(Language::get(4019)) != std::string::npos ) + if ( players[player]->worldUI.interactText != wallLockStringNoKey ) { // rescan, out of date string. players[player]->worldUI.tooltipView = TOOLTIP_VIEW_RESCAN; break; } } - else + else if ( wallLockState == Entity::WallLockStates::LOCK_KEY_ACTIVE_START + || wallLockState == Entity::WallLockStates::LOCK_KEY_ACTIVE ) + { + if ( players[player]->worldUI.interactText != wallLockStringDeactivate ) + { + // rescan, out of date string. + players[player]->worldUI.tooltipView = TOOLTIP_VIEW_RESCAN; + break; + } + } + else if ( wallLockState == Entity::WallLockStates::LOCK_KEY_INACTIVE_START + || wallLockState == Entity::WallLockStates::LOCK_KEY_INACTIVE ) { - if ( players[player]->worldUI.interactText.find(Language::get(4018)) != std::string::npos ) + if ( players[player]->worldUI.interactText != wallLockStringActivate ) { // rescan, out of date string. players[player]->worldUI.tooltipView = TOOLTIP_VIEW_RESCAN; @@ -4976,16 +5345,37 @@ const int Player::HUD_t::getActionIconForPlayer(ActionPrompts prompt, std::strin switch ( prompt ) { case ACTION_PROMPT_MAGIC: - promptString = Language::get(6047); // Haunt + if ( player.ghost.isSpiritGhost() ) + { + promptString = Language::get(6879); // Return + } + else + { + promptString = Language::get(6047); // Haunt + } break; case ACTION_PROMPT_MAINHAND: - promptString = Language::get(6046); // Chill + if ( player.ghost.isSpiritGhost() ) + { + promptString = Language::get(6878); // Distract + } + else + { + promptString = Language::get(6046); // Chill + } break; case ACTION_PROMPT_OFFHAND: promptString = Language::get(6048); // Push break; case ACTION_PROMPT_SNEAK: - promptString = Language::get(4077); // Sneak + if ( player.ghost.isSpiritGhost() ) + { + promptString = Language::get(6877); // Profile + } + else + { + promptString = Language::get(4077); // Sneak + } break; default: break; @@ -4996,7 +5386,7 @@ const int Player::HUD_t::getActionIconForPlayer(ActionPrompts prompt, std::strin if ( prompt == ACTION_PROMPT_MAGIC ) { promptString = Language::get(4078); - return PRO_SPELLCASTING; + return PRO_LEGACY_SPELLCASTING; } bool shapeshifted = false; @@ -5028,11 +5418,25 @@ const int Player::HUD_t::getActionIconForPlayer(ActionPrompts prompt, std::strin bool hasSpellBook = itemCategory(stats[player.playernum]->shield) == SPELLBOOK; bool allowCasting = true; bool allowDefending = true; - if ( shapeshifted && playerRace == CREATURE_IMP ) + if ( shapeshifted && playerRace != CREATURE_IMP ) { // imp allowed to cast via spellbook. allowCasting = false; } + if ( !shapeshifted && itemTypeIsInstrument(stats[player.playernum]->shield->type) ) + { + promptString = Language::get(6844); + return PRO_APPRAISAL; + } + if ( allowCasting && itemTypeIsFoci(stats[player.playernum]->shield->type) ) + { + if ( auto spell = getSpellFromID(getSpellIDFromFoci(stats[player.playernum]->shield->type)) ) + { + return spell->skillID; + } + promptString = Language::get(6843); + return PRO_SORCERY; + } if ( hasSpellBook && allowCasting ) { if ( player.bUseCompactGUIHeight() ) @@ -5043,7 +5447,11 @@ const int Player::HUD_t::getActionIconForPlayer(ActionPrompts prompt, std::strin { promptString = Language::get(4079); } - return PRO_MAGIC; + if ( auto spell = getSpellFromID(getSpellIDFromSpellbook(stats[player.playernum]->shield->type)) ) + { + return spell->skillID; + } + return PRO_SORCERY; } if ( shapeshifted || itemTypeIsQuiver(stats[player.playernum]->shield->type) ) @@ -5087,7 +5495,7 @@ const int Player::HUD_t::getActionIconForPlayer(ActionPrompts prompt, std::strin { if ( !shapeshifted || (shapeshifted && playerRace == CREATURE_IMP) ) { - skill = PRO_SPELLCASTING; + skill = PRO_LEGACY_MAGIC; promptString = Language::get(4083); } } @@ -5356,7 +5764,7 @@ Frame* Player::Inventory_t::getSpellSlotFrame(int x, int y) const { if ( x >= 0 && y >= 0 && x < MAX_SPELLS_X && y < MAX_SPELLS_Y ) { - return spellSlotFrames.at(x + y * 100); + return spellSlotFrames.at(x + y * 1000); } } return nullptr; @@ -5466,6 +5874,32 @@ void Player::PaperDoll_t::drawSlots() return; } +bool Player::Magic_t::doQuickCastTome() { + if ( quick_cast_tome != 0 ) + { + if ( Item* item = uidToItem(quick_cast_tome) ) + { + return true; + } + } + return false; +} + +void Player::Magic_t::setQuickCastTomeFromInventory(Item* item) +{ + if ( item && itemCategory(item) == TOME_SPELL ) + { + int spellID = item->getTomeSpellID(); + if ( auto spell = getSpellFromID(spellID) ) + { + if ( spell->ID > SPELL_NONE ) + { + quick_cast_tome = item->uid; + } + } + } +} + void Player::Magic_t::setQuickCastSpellFromInventory(Item* item) { if ( quick_cast_spell ) // spell already queued, ignore. @@ -6783,6 +7217,9 @@ void Player::clearGUIPointers() genericGUI.alchemyGUI.alchFrame = nullptr; genericGUI.alchemyGUI.alchemySlotFrames.clear(); genericGUI.alchemyGUI.itemRequiresTitleReflow = true; + genericGUI.mailboxGUI.mailFrame = nullptr; + genericGUI.mailboxGUI.mailSlotFrames.clear(); + genericGUI.mailboxGUI.itemRequiresTitleReflow = true; genericGUI.assistShrineGUI.assistShrineFrame = nullptr; genericGUI.assistShrineGUI.assistShrineSlotFrames.clear(); genericGUI.featherGUI.featherFrame = nullptr; @@ -6907,9 +7344,13 @@ void Player::PlayerMechanics_t::onItemDegrade(Item* item) { itemDegradeRng[item->type] = 0; } + else if ( items[item->type].item_slot != ItemEquippableSlot::EQUIPPABLE_IN_SLOT_SHIELD ) + { + itemDegradeRng[item->type] = 0; + } } -bool Player::PlayerMechanics_t::itemDegradeRoll(Item* item, int* checkInterval) +bool Player::PlayerMechanics_t::itemDegradeRoll(Item* item, int skillID, int* checkInterval) { if ( !item ) { @@ -6926,64 +7367,104 @@ bool Player::PlayerMechanics_t::itemDegradeRoll(Item* item, int* checkInterval) if ( itemCategory(item) == SPELLBOOK ) { // 10 max base interval - interval = (1 + item->status) + stats[player.playernum]->getModifiedProficiency(PRO_SPELLCASTING) / 20; - if ( item->beatitude < 0 - && !intro && !shouldInvertEquipmentBeatitude(stats[player.playernum]) ) - { - interval = 0; - } - else if ( item->beatitude > 0 - || (item->beatitude < 0 - && !intro && shouldInvertEquipmentBeatitude(stats[player.playernum])) ) + auto spellID = getSpellIDFromSpellbook(item->type); + if ( auto spell = getSpellFromID(spellID) ) { - interval += std::min(abs(item->beatitude), 2); + interval = (1 + item->status) + stats[player.playernum]->getModifiedProficiency(spell->skillID) / 20; + if ( item->beatitude < 0 + && !intro && !shouldInvertEquipmentBeatitude(stats[player.playernum]) ) + { + interval = 0; + } + else if ( item->beatitude > 0 + || (item->beatitude < 0 + && !intro && shouldInvertEquipmentBeatitude(stats[player.playernum])) ) + { + interval += std::min(abs(item->beatitude), 2); + } } } else { - switch ( item->type ) - { - case WOODEN_SHIELD: - interval = 10; - break; - case BRONZE_SHIELD: - interval = 20; - break; - case IRON_SHIELD: - interval = 20; - break; - case STEEL_SHIELD: - interval = 30; - break; - case STEEL_SHIELD_RESISTANCE: - interval = 30; - break; - case CRYSTAL_SHIELD: - interval = 20; - break; - default: - break; - } - if ( item->beatitude < 0 - && !intro && !shouldInvertEquipmentBeatitude(stats[player.playernum]) ) + if ( items[item->type].item_slot == ItemEquippableSlot::EQUIPPABLE_IN_SLOT_SHIELD ) { - interval = 0; - } - else if ( item->beatitude > 0 - || (item->beatitude < 0 - && !intro && shouldInvertEquipmentBeatitude(stats[player.playernum])) ) - { - interval += std::min(abs(item->beatitude), 5); - } - } - - if ( checkInterval ) - { - *checkInterval = interval; - return false; - } - - //messagePlayer(0, MESSAGE_DEBUG, "counter: %d | interval: %d", counter, interval); + switch ( item->type ) + { + case WOODEN_SHIELD: + interval = 10; + break; + case BRONZE_SHIELD: + interval = 20; + break; + case IRON_SHIELD: + case BONE_SHIELD: + interval = 20; + break; + case STEEL_SHIELD: + interval = 30; + break; + case STEEL_SHIELD_RESISTANCE: + interval = 30; + break; + case SILVER_SHIELD: + case BLACKIRON_SHIELD: + case SCUTUM: + interval = 30; + break; + case CRYSTAL_SHIELD: + interval = 20; + break; + default: + break; + } + if ( item->beatitude < 0 + && !intro && !shouldInvertEquipmentBeatitude(stats[player.playernum]) ) + { + interval = 0; + } + else if ( item->beatitude > 0 + || (item->beatitude < 0 + && !intro && shouldInvertEquipmentBeatitude(stats[player.playernum])) ) + { + interval += std::min(abs(item->beatitude), 5); + } + } + else + { + interval = 4 + item->status; + if ( item->beatitude < 0 + && !intro && !shouldInvertEquipmentBeatitude(stats[player.playernum]) ) + { + interval = 0; + } + else + { + if ( item->beatitude > 0 + || (item->beatitude < 0 + && !intro && shouldInvertEquipmentBeatitude(stats[player.playernum])) ) + { + interval += std::min(abs(item->beatitude), 1); + } + if ( skillID >= 0 ) + { + if ( skillID == PRO_SWORD || skillID == PRO_RANGED || skillID == PRO_AXE + || skillID == PRO_MACE || skillID == PRO_POLEARM || skillID == PRO_UNARMED ) + { + int bonus = (stats[player.playernum]->getModifiedProficiency(skillID) / 20); + interval += bonus; + } + } + } + } + } + + if ( checkInterval ) + { + *checkInterval = interval; + return false; + } + + //messagePlayer(0, MESSAGE_DEBUG, "counter: %d | interval: %d", counter, interval); if ( counter >= interval ) { @@ -6991,6 +7472,10 @@ bool Player::PlayerMechanics_t::itemDegradeRoll(Item* item, int* checkInterval) { // dont decrement until degraded } + else if ( items[item->type].item_slot != ItemEquippableSlot::EQUIPPABLE_IN_SLOT_SHIELD ) + { + // dont decrement until degraded + } else { counter = 0; @@ -7001,25 +7486,316 @@ bool Player::PlayerMechanics_t::itemDegradeRoll(Item* item, int* checkInterval) return false; } -void Player::PlayerMechanics_t::sustainedSpellIncrementMP(int mpChange) +void Player::PlayerMechanics_t::sustainedSpellIncrementMP(int mpChange, int skillID) +{ + if ( skillID == PRO_SORCERY ) + { + sustainedSpellMPUsedSorcery += std::max(0, mpChange); + } + else if ( skillID == PRO_MYSTICISM ) + { + sustainedSpellMPUsedMysticism += std::max(0, mpChange); + } + else if ( skillID == PRO_THAUMATURGY ) + { + sustainedSpellMPUsedThaumaturgy += std::max(0, mpChange); + } +} + +void Player::PlayerMechanics_t::baseSpellIncrementMP(int mpChange, int skillID) { - sustainedSpellMPUsed += std::max(0, mpChange); + if ( skillID == PRO_SORCERY ) + { + baseSpellMPUsedSorcery += std::max(0, mpChange); + } + else if ( skillID == PRO_MYSTICISM ) + { + baseSpellMPUsedMysticism += std::max(0, mpChange); + } + else if ( skillID == PRO_THAUMATURGY ) + { + baseSpellMPUsedThaumaturgy += std::max(0, mpChange); + } } -bool Player::PlayerMechanics_t::sustainedSpellLevelChance() +bool Player::PlayerMechanics_t::sustainedSpellLevelChance(int skillID) { int threshold = 10; - if ( stats[player.playernum]->getProficiency(PRO_SPELLCASTING) < SKILL_LEVEL_BASIC ) + /*if ( stats[player.playernum]->getProficiency(skillID) < SKILL_LEVEL_BASIC ) { threshold = 5; } - if ( sustainedSpellMPUsed >= threshold ) + else { - return true; + }*/ + threshold = 5 + 5 * (stats[player.playernum]->getProficiency(skillID) / 20); // 5-25 + + if ( skillID == PRO_SORCERY ) + { + return sustainedSpellMPUsedSorcery >= threshold; + } + else if ( skillID == PRO_MYSTICISM ) + { + return sustainedSpellMPUsedMysticism >= threshold; + } + else if ( skillID == PRO_THAUMATURGY ) + { + return sustainedSpellMPUsedThaumaturgy >= threshold; + } + + return false; +} + +int Player::PlayerMechanics_t::baseSpellMPSpent(int skillID) +{ + int counter = 0; + if ( skillID == PRO_SORCERY ) + { + counter = baseSpellMPUsedSorcery; + } + else if ( skillID == PRO_MYSTICISM ) + { + counter = baseSpellMPUsedMysticism; + } + else if ( skillID == PRO_THAUMATURGY ) + { + counter = baseSpellMPUsedThaumaturgy; } else { - return false; + return 0; + } + + return counter; +} + +int Player::PlayerMechanics_t::baseSpellLevelChance(int skillID) +{ + int counter = 0; + if ( skillID == PRO_SORCERY ) + { + counter = baseSpellMPUsedSorcery; + } + else if ( skillID == PRO_MYSTICISM ) + { + counter = baseSpellMPUsedMysticism; + } + else if ( skillID == PRO_THAUMATURGY ) + { + counter = baseSpellMPUsedThaumaturgy; + } + else + { + return 0; + } + int threshold = 20 + stats[player.playernum]->getProficiency(skillID) / 5; //20-40 + + return counter / threshold; +} + +void Player::PlayerMechanics_t::sustainedSpellClearMP(int skillID) +{ + if ( skillID == PRO_SORCERY ) + { + sustainedSpellMPUsedSorcery = 0; + } + else if ( skillID == PRO_MYSTICISM ) + { + sustainedSpellMPUsedMysticism = 0; + } + else if ( skillID == PRO_THAUMATURGY ) + { + sustainedSpellMPUsedThaumaturgy = 0; + } +} + +bool Player::PlayerMechanics_t::updateSustainedSpellEvent(int spellID, real_t value, real_t scaleValue, Entity* hitentity) +{ + if ( value < 0.05 ) { return false; } + + if ( hitentity && multiplayer != CLIENT ) + { + if ( Stat* hitstats = hitentity->getStats() ) + { + if ( hitstats->getEffectActive(EFF_STASIS) ) + { + return false; + } + else if ( (hitentity->behavior == &actMonster + && (hitentity->monsterAllyGetPlayerLeader() || (hitstats && achievementObserver.checkUidIsFromPlayer(hitstats->leader_uid) >= 0))) + || hitentity->behavior == &actPlayer ) + { + return false; + } + } + } + + if ( spellID == SPELL_MAGICMAPPING && multiplayer == CLIENT ) + { + sustainedSpellIDCounter[spellID] += value * scaleValue; + if ( players[player.playernum]->entity && sustainedSpellIDCounter[spellID] > 8 * 16.0 ) + { + sustainedSpellIDCounter[spellID] = 0.0; + Uint32 flags = spell_t::SPELL_LEVEL_EVENT_DEFAULT; + magicOnSpellCastEvent(players[player.playernum]->entity, players[player.playernum]->entity, + nullptr, spellID, flags, 1); + return true; + } + } + + if ( multiplayer == CLIENT ) { return false; } + if ( intro ) { return false; } + //if ( auto spell = getSpellFromID(spellID) ) + { + if ( spellID == SPELL_LIGHT || spellID == SPELL_DEEP_SHADE ) + { + sustainedSpellIDCounter[spellID] += value * scaleValue; + if ( players[player.playernum]->entity && sustainedSpellIDCounter[spellID] > 8 * 16.0 ) + { + Uint32 flags = spell_t::SPELL_LEVEL_EVENT_SUSTAIN; + sustainedSpellIDCounter[spellID] = 0.0; + if ( magicOnSpellCastEvent(players[player.playernum]->entity, players[player.playernum]->entity, + nullptr, spellID, flags, 1) ) + { + return true; + } + } + return false; + } + else if ( spellID == SPELL_PINPOINT || spellID == SPELL_PENANCE || spellID == SPELL_TURN_UNDEAD + || spellID == SPELL_SIGIL + || spellID == SPELL_SANCTUARY + || spellID == SPELL_HEALING + || spellID == SPELL_EXTRAHEALING + || spellID == SPELL_HOLY_BEAM + || spellID == SPELL_MAGICMAPPING + || spellID == SPELL_WINDGATE + || spellID == SPELL_DEMON_ILLUSION + || spellID == SPELL_DASH + || spellID == SPELL_HEAL_MINOR + || spellID == SPELL_HEAL_OTHER + || spellID == SPELL_HEAL_PULSE + || spellID == SPELL_SPEED + || spellID == SPELL_DETECT_FOOD + || spellID == SPELL_COMMAND + || spellID == SPELL_FLUTTER + || spellID == SPELL_DIG ) + { + sustainedSpellIDCounter[spellID] += value * scaleValue; + if ( players[player.playernum]->entity && sustainedSpellIDCounter[spellID] > 8 * 16.0 ) + { + sustainedSpellIDCounter[spellID] = 0.0; + Uint32 flags = spell_t::SPELL_LEVEL_EVENT_DEFAULT; + if ( magicOnSpellCastEvent(players[player.playernum]->entity, players[player.playernum]->entity, + nullptr, spellID, flags, 1) ) + { + return true; + } + } + } + else if ( spellID == SPELL_PROF_NIMBLENESS || spellID == SPELL_PROF_STURDINESS + || spellID == SPELL_PROF_GREATER_MIGHT || spellID == SPELL_PROF_COUNSEL ) + { + sustainedSpellIDCounter[spellID] += value * scaleValue; + if ( players[player.playernum]->entity && sustainedSpellIDCounter[spellID] > 8 * 16.0 ) + { + Uint32 flags = spell_t::SPELL_LEVEL_EVENT_SUMMON; + if ( magicOnSpellCastEvent(players[player.playernum]->entity, players[player.playernum]->entity, + nullptr, spellID, flags, 1) ) + { + sustainedSpellIDCounter[spellID] = 0.0; + return true; + } + } + } + else if ( spellID == SPELL_BLESS_FOOD + || spellID == SPELL_SCRY_TRAPS + || spellID == SPELL_SCRY_SHRINES + || spellID == SPELL_SCRY_TREASURES + || spellID == SPELL_DONATION ) + { + sustainedSpellIDCounter[spellID] += value * scaleValue; + if ( players[player.playernum]->entity && sustainedSpellIDCounter[spellID] > 8 * 16.0 ) + { + Uint32 flags = spell_t::SPELL_LEVEL_EVENT_DEFAULT; + if ( spellID == SPELL_DONATION ) + { + flags |= spell_t::SPELL_LEVEL_EVENT_ALWAYS; + } + if ( magicOnSpellCastEvent(players[player.playernum]->entity, players[player.playernum]->entity, + nullptr, spellID, flags, 1) ) + { + sustainedSpellIDCounter[spellID] = 0.0; + return true; + } + } + } + + for ( node_t* node = stats[player.playernum]->magic_effects.first; node; node = node->next ) + { + if ( spell_t* sustainedSpell = (spell_t*)node->element ) + { + if ( sustainedSpell->ID == spellID ) + { + sustainedSpellIDCounter[spellID] += value * scaleValue; + if ( players[player.playernum]->entity && sustainedSpellIDCounter[spellID] > 8 * 16.0 ) + { + Uint32 flags = spell_t::SPELL_LEVEL_EVENT_SUSTAIN; + if ( sustainedSpell->spellbook ) + { + flags |= spell_t::SPELL_LEVEL_EVENT_SPELLBOOK; + } + else if ( sustainedSpell->magicstaff ) + { + flags |= spell_t::SPELL_LEVEL_EVENT_MAGICSTAFF; + } + + bool resetOnProc = false; + if ( spellID == SPELL_BLOOD_WARD ) + { + resetOnProc = true; + } + if ( !resetOnProc ) + { + sustainedSpellIDCounter[spellID] = 0.0; + } + if ( magicOnSpellCastEvent(players[player.playernum]->entity, players[player.playernum]->entity, + nullptr, spellID, flags, 1) ) + { + if ( resetOnProc ) + { + sustainedSpellIDCounter[spellID] = 0.0; + } + return true; + } + } + break; + } + } + } + } + + return false; +} + +void Player::PlayerMechanics_t::baseSpellClearMP(int skillID) +{ + int threshold = 20 + stats[player.playernum]->getProficiency(skillID) / 5; + int leftoverCap = threshold * 4; + if ( skillID == PRO_SORCERY ) + { + baseSpellMPUsedSorcery = std::max(0, baseSpellMPUsedSorcery - 4 * threshold); + baseSpellMPUsedSorcery = std::min(baseSpellMPUsedSorcery, leftoverCap); + } + else if ( skillID == PRO_MYSTICISM ) + { + baseSpellMPUsedMysticism = std::max(0, baseSpellMPUsedMysticism - 4 * threshold); + baseSpellMPUsedMysticism = std::min(baseSpellMPUsedMysticism, leftoverCap); + } + else if ( skillID == PRO_THAUMATURGY ) + { + baseSpellMPUsedThaumaturgy = std::max(0, baseSpellMPUsedThaumaturgy - 4 * threshold); + baseSpellMPUsedThaumaturgy = std::min(baseSpellMPUsedThaumaturgy, leftoverCap); } } @@ -7029,5 +7805,914 @@ bool Player::PlayerMechanics_t::allowedRaiseBlockingAgainstEntity(Entity& attack { return false; } + else if ( attacker.behavior == &actMonster + && (attacker.monsterAllyGetPlayerLeader() + || (attacker.getStats() && achievementObserver.checkUidIsFromPlayer(attacker.getStats()->leader_uid) >= 0)) ) + { + return false; + } return enemyRaisedBlockingAgainst[attacker.getUID()] < 1; +} + +bool Player::PlayerMechanics_t::allowedRaiseStealthAgainstEntity(Entity& attacker) +{ + if ( attacker.behavior != &actMonster ) + { + return false; + } + return enemyRaisedStealthAgainst[attacker.getUID()] < 1; +} + +void Player::PlayerMechanics_t::ensembleMusicUpdateServer() +{ + if ( multiplayer != CLIENT ) + { + bool forceUpdate = false; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + Uint32 data = (i + 1) & 0x7F; + if ( players[i]->entity ) + { + if ( players[i]->mechanics.ensemblePlaying >= 0 ) + { + Uint8 playingData = 0; + switch ( players[i]->mechanics.ensemblePlaying ) + { + case EFF_ENSEMBLE_DRUM: + playingData = 1; + break; + case EFF_ENSEMBLE_FLUTE: + playingData = 2; + break; + case EFF_ENSEMBLE_LUTE: + playingData = 4; + break; + case EFF_ENSEMBLE_LYRE: + playingData = 5; + break; + case EFF_ENSEMBLE_HORN: + playingData = 3; + break; + default: + break; + } + data |= (0xFF & playingData) << 8; + } + + Uint16 effectData = 0; + if ( Uint8 effectStrength = stats[i]->getEffectActive(EFF_ENSEMBLE_DRUM) ) + { + effectData |= (1 << 0); + if ( effectStrength >= Stat::kEnsembleBreakPointTier4 ) + { + effectData |= (1 << 7); // beb 2 + } + if ( effectStrength >= Stat::kEnsembleBreakPointTier3 ) + { + effectData |= (1 << 6); // beb 1 + } + } + if ( Uint8 effectStrength = stats[i]->getEffectActive(EFF_ENSEMBLE_FLUTE) ) + { + effectData |= (1 << 1); + if ( effectStrength >= Stat::kEnsembleBreakPointTier4 ) + { + effectData |= (1 << 7); // beb 2 + } + if ( effectStrength >= Stat::kEnsembleBreakPointTier3 ) + { + effectData |= (1 << 6); // beb 1 + } + } + if ( Uint8 effectStrength = stats[i]->getEffectActive(EFF_ENSEMBLE_LUTE) ) + { + effectData |= (1 << 3); + if ( effectStrength >= Stat::kEnsembleBreakPointTier4 ) + { + effectData |= (1 << 7); // beb 2 + } + if ( effectStrength >= Stat::kEnsembleBreakPointTier3 ) + { + effectData |= (1 << 6); // beb 1 + } + } + if ( Uint8 effectStrength = stats[i]->getEffectActive(EFF_ENSEMBLE_LYRE) ) + { + effectData |= (1 << 4); + if ( effectStrength >= Stat::kEnsembleBreakPointTier4 ) + { + effectData |= (1 << 7); // beb 2 + } + if ( effectStrength >= Stat::kEnsembleBreakPointTier3 ) + { + effectData |= (1 << 6); // beb 1 + } + } + if ( Uint8 effectStrength = stats[i]->getEffectActive(EFF_ENSEMBLE_HORN) ) + { + effectData |= (1 << 2); + if ( effectStrength >= Stat::kEnsembleBreakPointTier4 ) + { + effectData |= (1 << 7); // beb 2 + } + if ( effectStrength >= Stat::kEnsembleBreakPointTier3 ) + { + effectData |= (1 << 6); // beb 1 + } + } + if ( effectData ) + { + effectData |= (1 << 5); // tambo + } + data |= (0xFF & effectData) << 16; + } + if ( players[i]->mechanics.ensembleDataUpdate != data ) + { + forceUpdate = true; + } + players[i]->mechanics.ensembleDataUpdate = data; + } + + if ( multiplayer == SERVER ) + { + if ( forceUpdate || ((ticks % static_cast(TICKS_PER_SECOND * 5.2) == 0)) ) + { + strcpy((char*)net_packet->data, "ENSM"); + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + SDLNet_Write32(players[i]->mechanics.ensembleDataUpdate, &net_packet->data[4 + i * 4]); + } + net_packet->len = 4 + MAXPLAYERS * 4; + for ( int i = 1; i < MAXPLAYERS; ++i ) + { + if ( !client_disconnected[i] ) + { + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } + } + } + } + } +} + +#ifdef USE_FMOD +static ConsoleCommand ccmd_ensemble_pitch("/ensemble_pitch", "", + [](int argc, const char** argv) { + if ( argc < 2 ) + { + return; + } + music_ensemble_global_send_group->setPitch(atoi(argv[1]) / 100.f); + }); + +static ConsoleCommand ccmd_ensemble_reverb("/ensemble_reverb", "", + [](int argc, const char** argv) { + + if ( argc < 3 ) + { + return; + } + FMOD::DSP* dspreverb = nullptr; + music_ensemble_global_recv_group->getDSP(0, &dspreverb); + dspreverb->setBypass(false); + int reverbType = atoi(argv[1]); + float dryLevel = atoi(argv[2]); + if ( reverbType >= 24 ) + { + reverbType = 0; + } + FMOD_REVERB_PROPERTIES props = FMOD_PRESET_OFF; + switch ( reverbType ) + { + case 0: + props = FMOD_PRESET_OFF; + break; + case 1: + props = FMOD_PRESET_GENERIC; + break; + case 2: + props = FMOD_PRESET_PADDEDCELL; + break; + case 3: + props = FMOD_PRESET_ROOM; + break; + case 4: + props = FMOD_PRESET_BATHROOM; + break; + case 5: + props = FMOD_PRESET_LIVINGROOM; + break; + case 6: + props = FMOD_PRESET_STONEROOM; + break; + case 7: + props = FMOD_PRESET_AUDITORIUM; + break; + case 8: + props = FMOD_PRESET_CONCERTHALL; + break; + case 9: + props = FMOD_PRESET_CAVE; + break; + case 10: + props = FMOD_PRESET_ARENA; + break; + case 11: + props = FMOD_PRESET_HANGAR; + break; + case 12: + props = FMOD_PRESET_CARPETTEDHALLWAY; + break; + case 13: + props = FMOD_PRESET_HALLWAY; + break; + case 14: + props = FMOD_PRESET_STONECORRIDOR; + break; + case 15: + props = FMOD_PRESET_ALLEY; + break; + case 16: + props = FMOD_PRESET_FOREST; + break; + case 17: + props = FMOD_PRESET_CITY; + break; + case 18: + props = FMOD_PRESET_MOUNTAINS; + break; + case 19: + props = FMOD_PRESET_QUARRY; + break; + case 20: + props = FMOD_PRESET_PLAIN; + break; + case 21: + props = FMOD_PRESET_PARKINGLOT; + break; + case 22: + props = FMOD_PRESET_SEWERPIPE; + break; + case 23: + props = FMOD_PRESET_UNDERWATER; + break; + } + + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DECAYTIME, props.DecayTime); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_EARLYDELAY, props.EarlyDelay); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LATEDELAY, props.LateDelay); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HFREFERENCE, props.HFReference); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HFDECAYRATIO, props.HFDecayRatio); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DIFFUSION, props.Diffusion); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DENSITY, props.Density); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LOWSHELFFREQUENCY, props.LowShelfFrequency); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_LOWSHELFGAIN, props.LowShelfGain); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_HIGHCUT, props.HighCut); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_EARLYLATEMIX, props.EarlyLateMix); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_WETLEVEL, props.WetLevel); + dspreverb->setParameterFloat(FMOD_DSP_SFXREVERB_DRYLEVEL, dryLevel); +}); + + +ConsoleVariable cvar_ensemble_vol_bg("/ensemble_vol_bg", -6.f); +#endif + +void Player::PlayerMechanics_t::ensembleMusicUpdate() +{ + if ( multiplayer != CLIENT ) + { + ensembleMusicUpdateServer(); + } + +#ifdef USE_FMOD + static ConsoleVariable cvar_ensemble_pitch_base("/ensemble_pitch_base", 0); + static ConsoleVariable cvar_ensemble_pitch_combat("/ensemble_pitch_combat", 0); + static ConsoleVariable cvar_ensemble_vol_fg("/ensemble_vol_fg", 0.25f); + if ( *cvar_ensemble_pitch_base > 0 && *cvar_ensemble_pitch_combat > 0 ) + { + if ( combat ) + { + music_ensemble_global_send_group->setPitch(*cvar_ensemble_pitch_combat / 100.f); + } + else + { + music_ensemble_global_send_group->setPitch(*cvar_ensemble_pitch_base / 100.f); + } + } + + bool notificationPlaying = false; + if ( music_notification_group ) + { + music_notification_group->isPlaying(¬ificationPlaying); + } + + if ( music_ensemble_global_recv_group ) + { + float volume = 0.f; + music_ensemble_global_recv_group->getVolume(&volume); + + if ( notificationPlaying && volume > 0.0f ) + { + volume -= fadeout_increment * 5; + if ( volume < ensembleSounds.ensemble_recv_global_volume * 0.5f ) + { + volume = ensembleSounds.ensemble_recv_global_volume * 0.5f; + } + music_ensemble_global_recv_group->setVolume(volume); + } + else + { + volume += fadein_increment * 2; + if ( volume > ensembleSounds.ensemble_recv_global_volume ) + { + volume = ensembleSounds.ensemble_recv_global_volume; + } + music_ensemble_global_recv_group->setVolume(volume); + } + } + if ( music_ensemble_local_recv_group ) + { + float volume = 0.f; + music_ensemble_local_recv_group->getVolume(&volume); + if ( notificationPlaying && volume > 0.0f ) + { + volume -= fadeout_increment * 5; + if ( volume < ensembleSounds.ensemble_recv_player_volume * 0.5f ) + { + volume = ensembleSounds.ensemble_recv_player_volume * 0.5f; + } + music_ensemble_local_recv_group->setVolume(volume); + } + else + { + volume += fadein_increment * 2; + if ( volume > ensembleSounds.ensemble_recv_player_volume ) + { + volume = ensembleSounds.ensemble_recv_player_volume; + } + music_ensemble_local_recv_group->setVolume(volume); + } + } + + // global tracks + if ( music_ensemble_global_send_group ) + { + bool isPlaying = false; + bool isPaused = false; + music_ensemble_global_send_group->isPlaying(&isPlaying); + if ( isPlaying ) + { + music_ensemble_global_send_group->getPaused(&isPaused); + } + + bool active = !isPaused && isPlaying; + /*std::string debugPlaying = ""; + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + bool channelPlaying = false; + music_ensemble_global_channel[i]->isPlaying(&channelPlaying); + float volume = 0.f; + music_ensemble_global_channel[i]->getVolume(&volume); + debugPlaying += channelPlaying ? "(1 " : "(0 "; + debugPlaying += "Vol: " + std::to_string(volume) + ") "; + } + messagePlayer(0, MESSAGE_DEBUG, "%s", debugPlaying.c_str());*/ + //float defaultVolume = 0.5f * *cvar_ensemble_vol_bg; + + Uint32 trackstatus = 0; + Uint32 trackstatusLocal = 0; + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( players[i]->isLocalPlayer() ) + { + trackstatusLocal |= (players[i]->mechanics.ensembleDataUpdate >> 16) & 0xFFFF; + } + trackstatus |= (players[i]->mechanics.ensembleDataUpdate >> 16) & 0xFFFF; + } + + if ( trackstatus == 0 ) // nothing playing + { + if ( active ) + { + bool allStopped = true; + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + if ( ensembleSounds.transceiver_group[i] ) + { + float volume = 0.f; + ensembleSounds.transceiver_group[i]->getVolume(&volume); + volume = std::max(0.f, volume - fadeout_increment * 5); + ensembleSounds.transceiver_group[i]->setVolume(volume); + if ( volume < 0.0001f ) + { + // wait until all tracks 0 volume + } + else + { + allStopped = false; + } + } + } + if ( allStopped ) + { + ensembleSounds.stopPlaying(false); + if ( music_ensemble_global_send_group ) + { + music_ensemble_global_send_group->setPaused(true); + } + + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + FMOD::DSP* channelFader = nullptr; + fmod_result = music_ensemble_local_recv_player[c]->getDSP(FMOD_CHANNELCONTROL_DSP_FADER, &channelFader); + channelFader->setActive(false); + } + } + } + else + { + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + FMOD::DSP* channelFader = nullptr; + fmod_result = music_ensemble_local_recv_player[c]->getDSP(FMOD_CHANNELCONTROL_DSP_FADER, &channelFader); + channelFader->setActive(false); + } + } + } + else + { + if ( !active ) + { + // restart channels + ensembleSounds.stopPlaying(false); + ensembleSounds.playSong(); + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + /*if ( music_ensemble_global_channel[i] ) + { + fmod_result = music_ensemble_global_channel[i]->stop(); + } + fmod_result = fmod_system->playSound(music_ensemble_global_sound[i], music_ensemble_global_send_group, + false, &music_ensemble_global_channel[i]);*/ + + + //if ( music_ensemble_global_channel[i] ) + //{ + // music_ensemble_global_channel[i]->setVolume(0.f); + // music_ensemble_global_channel[i]->setVolumeRamp(true); + + // static ConsoleVariable cvar_ensemble_global_x("/ensemble_global_x", 0.f); + // static ConsoleVariable cvar_ensemble_global_y("/ensemble_global_y", 0.f); + // static ConsoleVariable cvar_ensemble_global_z("/ensemble_global_z", 0.f); + // FMOD_VECTOR position; + // position.x = *cvar_ensemble_global_x; + // position.y = *cvar_ensemble_global_y; + // position.z = *cvar_ensemble_global_z; + // fmod_result = music_ensemble_global_channel[i]->setMode(FMOD_3D_HEADRELATIVE); + // fmod_result = music_ensemble_global_channel[i]->set3DAttributes(&position, nullptr); + + // { + // FMOD::DSP* tranceiver = nullptr; + // fmod_result = music_ensemble_global_channel[i]->getDSP(0, &tranceiver); + // FMOD_DSP_TYPE dspType = FMOD_DSP_TYPE::FMOD_DSP_TYPE_UNKNOWN; + // if ( tranceiver ) + // { + // tranceiver->getType(&dspType); + // } + // if ( dspType != FMOD_DSP_TYPE_TRANSCEIVER ) + // { + // fmod_result = fmod_system->createDSPByType(FMOD_DSP_TYPE_TRANSCEIVER, &tranceiver); + // fmod_result = music_ensemble_global_channel[i]->addDSP(0, tranceiver); + // } + // fmod_result = tranceiver->setParameterBool(FMOD_DSP_TRANSCEIVER_TRANSMIT, true); // sending signal + // fmod_result = tranceiver->setParameterInt(FMOD_DSP_TRANSCEIVER_CHANNEL, 1 + i); // sending on channel x + // } + //} + } + if ( music_ensemble_global_send_group ) + { + music_ensemble_global_send_group->setPaused(false); + } + } + + static float trackZeroVolumes[NUMENSEMBLEMUSIC] = { 0.f }; + static ConsoleCommand ccmd_ensemble_zero_vol("/ensemble_zero_vol", "", + [](int argc, const char** argv) { + if ( argc < 3 ) + { + return; + } + if ( atoi(argv[1]) < NUMENSEMBLEMUSIC ) + { + trackZeroVolumes[atoi(argv[1])] = atoi(argv[2]) / 100.f; + } + }); + + /*if ( ensembleSounds.exploreChannel[0] ) + { + bool playing = false; + ensembleSounds.exploreChannel[0]->isPlaying(&playing); + if ( playing ) + { + unsigned int songPos = 0; + fmod_result = ensembleSounds.exploreChannel[0]->getPosition(&songPos, FMOD_TIMEUNIT_PCM); + + } + }*/ + + if ( combat && ensembleSounds.songTransitionState == EnsembleSounds_t::TRANSITION_COMBAT_ENDING ) + { + // haven't triggered combat end yet, so continue combat + ensembleSounds.songTransitionState = EnsembleSounds_t::TRANSITION_COMBAT; + } + else if ( combat && ensembleSounds.combatDelay == 0 ) + { + if ( ensembleSounds.songTransitionState == EnsembleSounds_t::TRANSITION_EXPLORE ) + { + ensembleSounds.songTransitionState = EnsembleSounds_t::TRANSITION_COMBAT_START; + } + } + else + { + if ( ensembleSounds.songTransitionState == EnsembleSounds_t::TRANSITION_COMBAT ) + { + ensembleSounds.songTransitionState = EnsembleSounds_t::TRANSITION_COMBAT_ENDING; + } + } + + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + if ( ensembleSounds.transceiver_group[i] ) + { + float targetVolume = 1.f; + if ( trackstatus & (1 << i) ) + { + if ( (!active || true) && !(i == 6 || i == 7) ) + { + // no ramp up + fmod_result = ensembleSounds.transceiver_group[i]->setVolume(targetVolume); + } + else + { + float volume = 0.f; + fmod_result = ensembleSounds.transceiver_group[i]->getVolume(&volume); + volume = std::min(targetVolume, volume + fadein_increment * 5); + fmod_result = ensembleSounds.transceiver_group[i]->setVolume(volume); + } + } + else + { + // fade out + float volume = 0.f; + fmod_result = ensembleSounds.transceiver_group[i]->getVolume(&volume); + volume = std::max(0.f + trackZeroVolumes[i], volume - fadeout_increment * 5); + fmod_result = ensembleSounds.transceiver_group[i]->setVolume(volume); + } + } + + { + // global recv transceivers + FMOD::DSP* transceiver = nullptr; + fmod_result = music_ensemble_global_recv_group->getDSP(2 + ((NUMENSEMBLEMUSIC - 1) - i), &transceiver); + FMOD_DSP_TYPE dspType = FMOD_DSP_TYPE::FMOD_DSP_TYPE_UNKNOWN; + int channel = -1; + if ( transceiver ) + { + transceiver->getType(&dspType); + transceiver->getParameterInt(FMOD_DSP_TRANSCEIVER_CHANNEL, &channel, nullptr, 0); + } + if ( dspType != FMOD_DSP_TYPE_TRANSCEIVER ) + { + transceiver = nullptr; + } + + float bg_volume = instrument_bg_enabled ? *cvar_ensemble_vol_bg : -80.f; + if ( transceiver ) + { + if ( trackstatusLocal & (1 << i) ) + { + if ( (!active || true) && !(i == 6 || i == 7) ) + { + // no ramp up + transceiver->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, bg_volume); + } + else + { + float volume = 0.f; + transceiver->getParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, &volume, nullptr, 0); + volume = std::min(bg_volume, volume + fadein_increment * 5 * 0.8f * 100.f); + transceiver->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, volume); + } + } + else + { + // fade out + float volume = 0.f; + transceiver->getParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, &volume, nullptr, 0); + volume = std::max(-80.f + trackZeroVolumes[i] * 80.f, volume - fadeout_increment * 50); + transceiver->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, volume); + } + } + } + } + + for ( int c = 0; c < MAXPLAYERS; ++c ) + { + if ( !music_ensemble_local_recv_player[c] ) + { + continue; + } + Uint32 trackStatusLocal = (players[c]->mechanics.ensembleDataUpdate >> 8) & 0xFF; + + FMOD::DSP* channelFader = nullptr; + fmod_result = music_ensemble_local_recv_player[c]->getDSP(FMOD_CHANNELCONTROL_DSP_FADER, &channelFader); + + if ( players[c]->entity ) + { + FMOD_VECTOR position; + position.x = (float)(players[c]->entity->x / (real_t)16.0); + position.y = (float)(0.0); + position.z = (float)(players[c]->entity->y / (real_t)16.0); + fmod_result = music_ensemble_local_recv_player[c]->set3DAttributes(&position, nullptr); // update to player position + channelFader->setActive(true); + } + else + { + channelFader->setActive(false); + } + music_ensemble_local_recv_player[c]->setVolume((instrument_bg_enabled ? 0.5f : 4.f) * *cvar_ensemble_vol_fg); + + FMOD::DSP* transceivers[NUMENSEMBLEMUSIC] = { nullptr }; + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + fmod_result = music_ensemble_local_recv_player[c]->getDSP(1 + ((NUMENSEMBLEMUSIC - 1) - i), &transceivers[i]); + FMOD_DSP_TYPE dspType = FMOD_DSP_TYPE::FMOD_DSP_TYPE_UNKNOWN; + int channel = -1; + if ( transceivers[i] ) + { + transceivers[i]->getType(&dspType); + transceivers[i]->getParameterInt(FMOD_DSP_TRANSCEIVER_CHANNEL, &channel, nullptr, 0); + } + if ( dspType != FMOD_DSP_TYPE_TRANSCEIVER ) + { + transceivers[i] = nullptr; + } + } + + for ( int i = 0; i < NUMENSEMBLEMUSIC; ++i ) + { + bool localTrackPlaying = false; + if ( trackStatusLocal > 0 && (((trackStatusLocal - 1) == i) || i == 5) ) + { + localTrackPlaying = true; + } + + if ( localTrackPlaying ) + { + if ( !active || true ) + { + // no ramp up + if ( i == 5 ) + { + transceivers[i]->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, -6.f); + } + else + { + transceivers[i]->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, 0.f); + } + } + else + { + float volume = 0.f; + transceivers[i]->getParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, &volume, nullptr, 0); + volume = std::min(0.f, volume + fadein_increment * 50); + transceivers[i]->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, volume); + } + } + else + { + // fade out + float volume = 0.f; + transceivers[i]->getParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, &volume, nullptr, 0); + volume = std::max(-80.f, volume - fadeout_increment * 50); + transceivers[i]->setParameterFloat(FMOD_DSP_TRANSCEIVER_GAIN, volume); + } + } + } + } + + ensembleSounds.updatePlayingChannelVolumes(); + } +#endif +} + +int Player::PlayerMechanics_t::getBreakableCounterTier() +{ + if ( stats[player.playernum]->type == GREMLIN ) + { + if ( gremlinBreakableCounter >= 50 ) + { + return 4; + } + else if ( gremlinBreakableCounter >= 30 ) + { + return 3; + } + else if ( gremlinBreakableCounter >= 20 ) + { + return 2; + } + else if ( gremlinBreakableCounter >= 10 ) + { + return 1; + } + else if ( gremlinBreakableCounter > 0 ) + { + return 0; + } + } + + return 0; +} + +void Player::PlayerMechanics_t::incrementBreakableCounter(Player::PlayerMechanics_t::BreakableEvent eventType, Entity* entity) +{ + if ( stats[player.playernum]->type == GREMLIN ) + { + int amount = 0; + if ( eventType == BreakableEvent::GBREAK_COMMON ) + { + amount = 1; + if ( entity && entity->behavior == &actChest ) + { + amount = 3; + } + } + else if ( eventType == BreakableEvent::GBREAK_DEGRADE ) + { + amount += 3; + } + else if ( eventType == BreakableEvent::GBREAK_KILL ) + { + if ( entity && entity->behavior == &actMonster && players[player.playernum]->entity ) + { + if ( Stat* targetStats = entity->getStats() ) + { + if ( targetStats->type == CRYSTALGOLEM + || targetStats->type == AUTOMATON + || targetStats->type == MINIMIMIC + || targetStats->type == MIMIC + || monsterIsImmobileTurret(entity, targetStats) ) + { + amount = 3; + } + } + } + } + else if ( eventType == BreakableEvent::GBREAK_DEFACE ) + { + amount += 5; + } + int prevTier = getBreakableCounterTier(); + gremlinBreakableCounter += amount; + gremlinBreakableCounter = std::min(50, gremlinBreakableCounter); + if ( getBreakableCounterTier() > prevTier ) + { + if ( player.entity ) + { + player.entity->modMP(getBreakableCounterTier() + local_rng.rand() % 2); + int duration = (5 + (getBreakableCounterTier() * 5)) * TICKS_PER_SECOND; + player.entity->setEffect(EFF_MP_REGEN, true, stats[player.playernum]->EFFECTS_TIMERS[EFF_MP_REGEN] + duration, false); + messagePlayerColor(player.playernum, MESSAGE_HINT, makeColorRGB(0, 255, 0), Language::get(6886)); + playSoundEntity(player.entity, 168, 128); + } + updateBreakableCounterServer(); + } + } +} + +void Player::PlayerMechanics_t::updateBreakableCounterServer() +{ + if ( multiplayer == SERVER && player.playernum > 0 && !player.isLocalPlayer() && !client_disconnected[player.playernum] ) + { + int i = player.playernum; + strcpy((char*)net_packet->data, "GBRK"); + net_packet->data[4] = std::min(255, gremlinBreakableCounter); + net_packet->len = 5; + net_packet->address.host = net_clients[i - 1].host; + net_packet->address.port = net_clients[i - 1].port; + sendPacketSafe(net_sock, -1, net_packet, i - 1); + } +} + +std::map prng_tables +{ + {0, 0.0}, + {5, 0.0038}, + {10, 0.0147}, + {15, 0.0322}, + {20, 0.0557}, + {25, 0.0847}, + {30, 0.1189}, + {35, 0.1579}, + {40, 0.2015}, + {45, 0.2493}, + {50, 0.3021}, + {55, 0.3604}, + {60, 0.4226}, + {65, 0.4811}, + {70, 0.5714}, + {75, 0.6667}, + {80, 0.7500}, + {85, 0.8235}, + {90, 0.8889}, + {95, 0.9474}, +}; + +// escalating rng +bool Player::PlayerMechanics_t::rollRngProc(Player::PlayerMechanics_t::RngRollTypes rngType, int chance, int spellID) +{ + chance = std::min(95, chance); + if ( chance <= 0 ) + { + return false; + } + int breakpoint = (chance / 5) * 5; + auto find = prng_tables.find(breakpoint); + if ( find != prng_tables.end() ) + { + real_t c = find->second; + if ( chance < 5 ) + { + c = (chance / 5.0) * prng_tables[5]; + } + else if ( chance % 5 > 0 ) + { + // find next breakpoint, lerp + auto find2 = prng_tables.find(breakpoint + 5); + if ( find2 != prng_tables.end() ) + { + real_t c2 = find2->second; + c += (c2 - c) * ((chance % 5) / 5.0); + } + } + + int pityCap = 20; + if ( chance < 5 ) + { + int oneInRoll = 1 / (chance / 100.0); + pityCap = std::max(pityCap, oneInRoll); + } + + auto& rng_counter = rngType == RngRollTypes::RNG_ROLL_SPELL_LEVELS ? escalatingSpellRngRolls[spellID] : escalatingRngRolls[(int)rngType]; + + if ( c * rng_counter >= 1.0 + || (rng_counter >= pityCap) ) + { + // success + rng_counter = 0; + return true; + } + + real_t roll = (local_rng.rand() % 10000) / 10000.0; + if ( roll <= c * (rng_counter + 1) ) + { + // success + rng_counter = 0; + return true; + } + + ++rng_counter; + } + else + { + return local_rng.rand() % 100 < chance; // default to basic 1-100 + } + return false; +} + +int Player::PlayerMechanics_t::getWealthTier() +{ + if ( stats[player.playernum]->type == GNOME ) + { + if ( stats[player.playernum]->GOLD >= 100 && stats[player.playernum]->GOLD < 1000 ) + { + return 1; + } + else if ( stats[player.playernum]->GOLD >= 1000 && stats[player.playernum]->GOLD < 5000 ) + { + return 2; + } + else if ( stats[player.playernum]->GOLD >= 5000 && stats[player.playernum]->GOLD < 20000 ) + { + return 3; + } + else if ( stats[player.playernum]->GOLD >= 20000 ) + { + return 4; + } + } + return 0; } \ No newline at end of file diff --git a/src/player.hpp b/src/player.hpp index 164aa4994..fd39a557c 100644 --- a/src/player.hpp +++ b/src/player.hpp @@ -59,6 +59,7 @@ struct PlayerSettings_t bool mkb_world_tooltips_enabled = true; bool gamepad_facehotbar = true; bool hotbar_numkey_quick_add = true; + bool hotbar_numkey_change_slot = true; bool reversemouse = 0; bool smoothmouse = false; real_t gamepad_rightx_sensitivity = 1.0; @@ -67,6 +68,8 @@ struct PlayerSettings_t bool gamepad_righty_invert = false; float quick_turn_speed = 1.f; float quick_turn_speed_mkb = 1.f; + Sint32 leftStickDeadzone = 8000; + Sint32 rightStickDeadzone = 8000; void init(const int _player) { player = _player; @@ -109,7 +112,8 @@ class GameController RUMBLE_BOULDER_BOUNCE, RUMBLE_BOULDER_ROLLING, RUMBLE_DEATH, - RUMBLE_TMP + RUMBLE_TMP, + RUMBLE_SPELL }; int hapticEffectId = -1; struct HapticEffect @@ -272,33 +276,33 @@ class GameController int getRightTrigger(); //The amount of movement of the given analog stick along its respective axis, with no gamepad sensitivity application. Deadzone is taken into account. - int getRawLeftXMove(); - int getRawLeftYMove(); - int getRawRightXMove(); - int getRawRightYMove(); + int getRawLeftXMove(int player); + int getRawLeftYMove(int player); + int getRawRightXMove(int player); + int getRawRightYMove(int player); int getRawLeftTrigger(); int getRawRightTrigger(); //Gets the percentage the given stick is pressed along its current axis. From 0% after the deadzone to 100% all the way to the edge of the analog stick. - float getLeftXPercent(); - float getLeftYPercent(); - float getRightXPercent(); - float getRightYPercent(); + float getLeftXPercent(int player); + float getLeftYPercent(int player); + float getRightXPercent(int player); + float getRightYPercent(int player); //Gets the percentage of the left stick for player movement, 100% input is multiplied by : // x_forceMaxForwardThreshold, x_forceMaxBackwardThreshold, y_forceMaxStrafeThreshold - float getLeftXPercentForPlayerMovement(); - float getLeftYPercentForPlayerMovement(); + float getLeftXPercentForPlayerMovement(int player); + float getLeftYPercentForPlayerMovement(int player); float getLeftTriggerPercent(); float getRightTriggerPercent(); //The maximum amount the given analog stick can move on its respective axis. After the gamepad deadzone is taken into account. - int maxLeftXMove(); - int maxLeftYMove(); - int maxRightXMove(); - int maxRightYMove(); + int maxLeftXMove(int player); + int maxLeftYMove(int player); + int maxRightXMove(int player); + int maxRightYMove(int player); int maxLeftTrigger(); int maxRightTrigger(); @@ -312,9 +316,6 @@ class GameController DeadZoneType leftStickDeadzoneType = DEADZONE_PER_AXIS; DeadZoneType rightStickDeadzoneType = DEADZONE_MAGNITUDE_HALFPIPE; - Sint32 leftStickDeadzone = 8000; - Sint32 rightStickDeadzone = 8000; - real_t oldFloatRightX = 0.0; // current delta per frame right-stick analogue value real_t oldFloatRightY = 0.0; // current delta per frame right-stick analogue value int oldAxisRightX = 0; // current raw right-stick analogue value (0-32768) @@ -658,7 +659,8 @@ class Player public: Entity* entity; - + real_t player_last_x = 0.0; + real_t player_last_y = 0.0; enum SplitScreenTypes : int { SPLITSCREEN_DEFAULT, @@ -668,6 +670,7 @@ class Player bool bSplitscreen = false; SplitScreenTypes splitScreenType = SPLITSCREEN_DEFAULT; bool bControlEnabled = true; // disabled if dead waiting for gameover prompt etc + bool was_connected_to_game = false; Player(int playernum = 0, bool local_host = true); ~Player(); @@ -786,7 +789,8 @@ class Player MODULE_SIGN_VIEW, MODULE_ITEMEFFECTGUI, MODULE_PORTRAIT, - MODULE_ASSISTSHRINE + MODULE_ASSISTSHRINE, + MODULE_MAILBOX }; GUIModules activeModule = MODULE_NONE; GUIModules previousModule = MODULE_NONE; @@ -922,6 +926,7 @@ class Player bool bOpen = false; bool bFirstTimeSnapCursor = false; int currentScrollRow = 0; + int spellFilterBySkill = 0; const int kNumSpellsToDisplayVertical = 5; int getNumSpellsToDisplayVertical() const; void openSpellPanel(); @@ -949,9 +954,10 @@ class Player bool bOpen = false; bool bFirstTimeSnapCursor = false; int currentScrollRow = 0; + bool voidChest = false; const int kNumItemsToDisplayVertical = 3; int getNumItemsToDisplayVertical() const; - void openChest(); + void openChest(bool _voidChest); void closeChest(); void updateChest(); void scrollToSlot(int x, int y, bool instantly); @@ -1078,6 +1084,8 @@ class Player bool warpMouseToSelectedChestSlot(Item* snapToItem, Uint32 flags); bool guiAllowDropItems(Item* itemToDrop) const; bool guiAllowDefaultRightClick() const; + Item* hasKeyForWallLock(Entity& entity) const; + int getKeyAmountForWallLock(Entity& entity) const; void processInventory(); void updateInventory(); void updateCursor(); @@ -1149,7 +1157,9 @@ class Player int timer = 0; //There is a delay after the appraisal skill is activated before the item is identified. int timermax = 0; Uint32 current_item = 0; //The item being appraised (or rather its uid) + std::map appraisalProgressionItems; Uint32 old_item = 0; + Uint32 manual_appraised_item = 0; int getAppraisalTime(Item* item); // Return time in ticks needed to appraise an item void appraiseItem(Item* item); // start appraise process bool appraisalPossible(Item* item); // if possible with current skill and stats @@ -1157,6 +1167,7 @@ class Player Uint32 animStartTick = 0; Uint32 itemNotifyUpdatedThisTick = 0; int itemNotifyAnimState = 0; + real_t spellLearnAnim = 0.0; enum ItemNotifyHoverStates : int { NOTIFY_ITEM_WAITING_TO_HOVER, @@ -1165,6 +1176,17 @@ class Player }; std::unordered_map itemsToNotify; void updateAppraisalAnim(); + static void readFromFile(); + static std::vector> appraisal_time_points; + struct AppraisalBreakpoint_t + { + int skillLVL = 0; + int goldValueLimit = 0; + int fastTimeGold = 0; + }; + static int fastTimeAppraisal; + static int perStatMult; + static std::vector appraisal_tables; } appraisal; bool bNewInventoryLayout = true; } inventoryUI; @@ -1369,6 +1391,7 @@ class Player static real_t windowCompactHeightScaleX; static real_t windowCompactHeightScaleY; static bool generateFollowerTableForSkillsheet; + static std::string getSkillNameFromID(int skillID, bool shortName = false); static struct SkillSheetData_t { Uint32 defaultTextColor = 0xFFFFFFFF; @@ -1377,9 +1400,31 @@ class Player Uint32 legendTextColor = 0xFFFFFFFF; struct SkillEntry_t { + private: + std::string skillName = ""; + std::string skillShortName = ""; + public: + void setSkillName(std::string name) + { + skillName = name; + } + void setSkillShortName(std::string name) + { + skillShortName = name; + } + std::string getSkillName(bool shortName = false) + { + if ( shortName ) + { + if ( skillShortName != "" ) + { + return skillShortName; + } + } + return skillName; + } SkillEntry_t() {}; ~SkillEntry_t() {}; - std::string name; int skillId = -1; std::string skillIconPath; std::string skillIconPathLegend; @@ -1398,6 +1443,7 @@ class Player ~SkillEffect_t() {}; std::string tag; std::string title; + std::string titleShort; std::string rawValue; std::string value; int valueCustomWidthOffset = 0; @@ -1481,6 +1527,7 @@ class Player Entity* arm = nullptr; Entity* magicLeftHand = nullptr; Entity* magicRightHand = nullptr; + Entity* magicRangefinder = nullptr; bool weaponSwitch = false; bool shieldSwitch = false; @@ -1715,6 +1762,7 @@ class Player Player& player; spell_t* selected_spell = nullptr; //The spell the player has currently selected. spell_t* quick_cast_spell = nullptr; //Spell ready for quick-casting + Uint32 quick_cast_tome = 0; // Tome read for quick-casting public: spell_t* selected_spell_alternate[NUM_HOTBAR_ALTERNATES] = { nullptr, nullptr, nullptr, nullptr, nullptr }; int selected_spell_last_appearance = -1; @@ -1723,6 +1771,7 @@ class Player Uint32 noManaFeedbackTicks = 0; Uint32 noManaProcessedOnTick = 0; Uint32 spellbookUidFromHotbarSlot = 0; + Uint32 telekinesisTarget = 0; void flashNoMana() { noManaFeedbackTicks = 0; @@ -1749,8 +1798,12 @@ class Player selected_spell = spell; } void setQuickCastSpellFromInventory(Item* item); + void setQuickCastTomeFromInventory(Item* item); bool doQuickCastSpell() { return quick_cast_spell != nullptr; } void resetQuickCastSpell() { quick_cast_spell = nullptr; } + void resetQuickCastTome() { quick_cast_tome = 0; } + Uint32 quickCastTome() { return quick_cast_tome; } + bool doQuickCastTome(); spell_t* selectedSpell() const { return selected_spell; } spell_t* quickCastSpell() const { return quick_cast_spell; } @@ -1778,6 +1831,7 @@ class Player int monsterEmoteGimpTimer = 0; int selectedEntityGimpTimer = 0; bool insectoidLevitating = false; + static real_t minimiseMaximiseCameraZ; bool handleQuickTurn(bool useRefreshRateDelta); void startQuickTurn(); @@ -1806,6 +1860,7 @@ class Player GHOST_SPELL_NONE, GHOST_SPELL_TELEPORT, GHOST_SPELL_BOLT, + GHOST_SPELL_QUACK, GHOST_SPELL_POST_CASTING }; GhostSpells_t castingSpellAnimation = GHOST_SPELL_NONE; @@ -1843,6 +1898,7 @@ class Player void handleGhostMovement(bool useRefreshRateDelta); void handleActions(); void handleAttack(); + bool isSpiritGhost(); bool isActive(); void setActive(bool active); void initTeleportLocations(int x, int y); @@ -1954,7 +2010,8 @@ class Player int beatitude = -99; int count = 0; Uint32 appearance = 0; - bool identified = false; + bool identifiedItem = false; + bool hasAppraiseCapstone = false; bool isItemSameAsCurrent(Item* item); SDL_Surface* blitItemWorldTooltip(Item* item); SDL_Surface* itemWorldTooltipSurface = nullptr; @@ -2005,6 +2062,7 @@ class Player DialogueType_t dialogueType = DIALOGUE_NONE; SDL_Surface* blitDialogueTooltip(); SDL_Surface* dialogueTooltipSurface = nullptr; + void updateWorldCoordinates(); Dialogue_t() {}; Dialogue_t(int player) { @@ -2061,6 +2119,7 @@ class Player TooltipView tooltipView = TOOLTIP_VIEW_FREE; std::vector> tooltipsInRange; static real_t tooltipHeightOffsetZ; + real_t modifiedTooltipDrawHeight = 0.0; real_t playerLastYaw = 0.0; real_t playerLastPitch = 0.0; int gimpDisplayTimer = 0; @@ -2081,7 +2140,6 @@ class Player real_t tooltipInRange(Entity& tooltip); // returns distance of added tooltip, otherwise 0. void cycleToNextTooltip(); void cycleToPreviousTooltip(); - } worldUI; class PaperDoll_t @@ -2172,6 +2230,7 @@ class Player int swapHotbarOnShapeshift = 0; bool hotbarHasFocus = false; int magicBoomerangHotbarSlot = -1; + int magicDuckHotbarSlot = -1; Uint32 hotbarTooltipLastGameTick = 0; SDL_Rect hotbarBox; Frame* hotbarFrame = nullptr; @@ -2244,6 +2303,7 @@ class Player current_hotbar = 0; //hotbarHasFocus = false; magicBoomerangHotbarSlot = -1; + magicDuckHotbarSlot = -1; hotbarTooltipLastGameTick = 0; for ( int j = 0; j < NUM_HOTBAR_ALTERNATES; ++j ) { @@ -2331,14 +2391,72 @@ class Player Player& player; public: std::map itemDegradeRng; - bool itemDegradeRoll(Item* item, int* checkInterval = nullptr); + std::set learnedSpells; + std::vector> ducksInARow; + std::vector> pendingDucks; + std::map favoriteBooksAchievement; + int numFishingCaught = 0; + bool itemDegradeRoll(Item* item, int skillID = -1, int* checkInterval = nullptr); void onItemDegrade(Item* item); - int sustainedSpellMPUsed = 0; + int sustainedSpellMPUsedSorcery = 0; + int sustainedSpellMPUsedMysticism = 0; + int sustainedSpellMPUsedThaumaturgy = 0; + int baseSpellMPUsedSorcery = 0; + int baseSpellMPUsedMysticism = 0; + int baseSpellMPUsedThaumaturgy = 0; Uint32 defendTicks = 0; - bool sustainedSpellLevelChance(); - void sustainedSpellIncrementMP(int mpChange); + int fociHolyChargeTime = 0; + int fociDarkChargeTime = 0; + int lastFociHeldType = 0; + enum class RngRollTypes + { + RNG_ROLL_DEFAULT, + RNG_ROLL_EVASION, + RNG_ROLL_GROWTH, + RNG_ROLL_SILKEN_BOW, + RNG_ROLL_SPELL_LEVELS, + RNG_ROLL_ENUM_END + }; + std::map escalatingRngRolls; + std::map escalatingSpellRngRolls; + bool sustainedSpellLevelChance(int skillID); + int baseSpellLevelChance(int skillID); + int baseSpellMPSpent(int skillID); + void sustainedSpellIncrementMP(int mpChange, int skillID); + void baseSpellIncrementMP(int mpChange, int skillID); + void sustainedSpellClearMP(int skillID); + void baseSpellClearMP(int skillID); + std::map baseSpellLevelUpProcs; + std::map sustainedSpellIDCounter; + bool updateSustainedSpellEvent(int spellID, real_t value, real_t scaleValue, Entity* hitEntity); + bool rollRngProc(RngRollTypes rngType, int chance, int spellID = -1); std::map enemyRaisedBlockingAgainst; + std::map enemyRaisedStealthAgainst; bool allowedRaiseBlockingAgainstEntity(Entity& attacker); + bool allowedRaiseStealthAgainstEntity(Entity& attacker); + int getWealthTier(); + int ensemblePlaying = -1; + bool ensembleRequireRecast = false; + bool ensembleTakenInitialMP = false; + bool previouslyLevitating = false; + Uint32 donationRevealedOnFloor = 0; + bool donationClaimed = false; + std::map> targetsCompelled; + std::set targetsRefuseCompel; + static void ensembleMusicUpdateServer(); + static void ensembleMusicUpdate(); + enum class BreakableEvent + { + GBREAK_COMMON, + GBREAK_KILL, + GBREAK_DEFACE, + GBREAK_DEGRADE + }; + int gremlinBreakableCounter = 0; + void incrementBreakableCounter(BreakableEvent eventType, Entity* entity); + int getBreakableCounterTier(); + void updateBreakableCounterServer(); + Uint32 ensembleDataUpdate = 0; PlayerMechanics_t(Player& p) : player(p) {}; ~PlayerMechanics_t() {}; diff --git a/src/playfab.cpp b/src/playfab.cpp index 643b391f4..2c2e21c61 100644 --- a/src/playfab.cpp +++ b/src/playfab.cpp @@ -480,6 +480,15 @@ int parseOnlineHiscore(SaveGameInfo& info, Json::Value score) { player.selected_spell_alternate[i] = UINT32_MAX; } + player.stats.EFFECTS.resize(NUMEFFECTS); + player.stats.EFFECTS_TIMERS.resize(NUMEFFECTS); + player.stats.EFFECTS_ACCRETION_TIME.resize(NUMEFFECTS); + for ( int i = 0; i < NUMEFFECTS; ++i ) + { + player.stats.EFFECTS[i] = 0; + player.stats.EFFECTS_TIMERS[i] = 0; + player.stats.EFFECTS_ACCRETION_TIME[i] = 0; + } for ( auto& m : score.getMemberNames() ) { @@ -647,6 +656,29 @@ int parseOnlineHiscore(SaveGameInfo& info, Json::Value score) jsonArrayToInt(score[m][s], i, player.stats.PROFICIENCIES[i]); } } + else if ( s == "effects" ) + { + if ( score[m][s].isObject() ) + { + for ( auto& eff : score[m][s].getMemberNames() ) + { + if ( score[m][s][eff].isInt() ) + { + try + { + int effIndex = std::stoi(eff); + if ( effIndex >= 0 && effIndex < NUMEFFECTS ) + { + player.stats.EFFECTS[effIndex] = score[m][s][eff].asInt(); + } + } + catch (...) + { + } + } + } + } + } else if ( s == "conducts" ) { for ( Json::ArrayIndex i = 0; i < score[m][s].size() && i < (NUM_CONDUCT_CHALLENGES + 4); ++i ) @@ -1228,7 +1260,7 @@ void PlayfabUser_t::getLeaderboardTop100(std::string lid) if ( leaderboardData.leaderboards[lid].loading ) { - for ( auto pair : leaderboardData.leaderboards[lid].awaitingResponse ) + for ( auto& pair : leaderboardData.leaderboards[lid].awaitingResponse ) { if ( (processTick - pair.second.first) < 5 * TICKS_PER_SECOND ) { @@ -1523,7 +1555,7 @@ bool PlayfabUser_t::PostScoreHandler_t::ScoreUpdate_t::saveToFile() d.AddMember("version", rapidjson::Value(1), d.GetAllocator()); d.AddMember("hash", rapidjson::Value(hash.c_str(), d.GetAllocator()), d.GetAllocator()); - d.AddMember("name", rapidjson::Value(hash.c_str(), d.GetAllocator()), d.GetAllocator()); + d.AddMember("name", rapidjson::Value(name.c_str(), d.GetAllocator()), d.GetAllocator()); d.AddMember("score", rapidjson::Value(score.c_str(), d.GetAllocator()), d.GetAllocator()); File* fp = FileIO::open(outputPath.c_str(), "wb"); @@ -1560,9 +1592,10 @@ void PlayfabUser_t::PostScoreHandler_t::readFromFiles() return; } - for ( auto f : directoryContents("scores/processing/", false, true, outputdir) ) + for ( auto& f : directoryContents("scores/processing/", false, true, outputdir) ) { std::string inputPath = PHYSFS_getRealDir(baseDir.c_str()); + inputPath.append(PHYSFS_getDirSeparator()); inputPath += "scores/processing/"; inputPath += f; @@ -2015,7 +2048,7 @@ void PlayfabUser_t::LeaderboardSearch_t::applySavedChallengeSearchIfExists() return; } - auto lid = savedSearchesFromNotification[challengeBoard]; + auto& lid = savedSearchesFromNotification[challengeBoard]; scoresNearMe = true; win = lid.find("_victory_") != std::string::npos; //victory = win ? 3 : 0; diff --git a/src/savepng.cpp b/src/savepng.cpp index 6a7d0f7be..796a0f9c6 100644 --- a/src/savepng.cpp +++ b/src/savepng.cpp @@ -4,6 +4,8 @@ * This code is free software, available under zlib/libpng license. * http://www.libpng.org/pub/png/src/libpng-LICENSE.txt */ + +#include "main.hpp" #include "Config.hpp" #ifndef NINTENDO #ifdef APPLE diff --git a/src/scores.cpp b/src/scores.cpp index dc2c67476..520ee14b6 100644 --- a/src/scores.cpp +++ b/src/scores.cpp @@ -26,13 +26,16 @@ #include "collision.hpp" #include "mod_tools.hpp" #include "lobbies.hpp" +#include "shops.hpp" #ifdef USE_PLAYFAB #include "playfab.hpp" #endif // definitions -list_t topscores; -list_t topscoresMultiplayer; +list_t topscores_json; +list_t topscoresMultiplayer_json; +list_t topscores_legacy; +list_t topscoresMultiplayer_legacy; int victory = 0; Uint32 completionTime = 0; bool conductPenniless = true; @@ -132,7 +135,7 @@ score_t* scoreConstructor(int player) } for ( int c = 0; c < NUMEFFECTS; c++ ) { - score->stats->EFFECTS[c] = stats[player]->EFFECTS[c]; + score->stats->setEffectValueUnsafe(c, stats[player]->getEffectActive(c)); score->stats->EFFECTS_TIMERS[c] = stats[player]->EFFECTS_TIMERS[c]; } score->stats->leader_uid = 0; @@ -140,6 +143,8 @@ score_t* scoreConstructor(int player) score->stats->FOLLOWERS.last = NULL; score->stats->inventory.first = NULL; score->stats->inventory.last = NULL; + score->stats->void_chest_inventory.first = NULL; + score->stats->void_chest_inventory.last = NULL; score->stats->helmet = NULL; score->stats->breastplate = NULL; score->stats->gloves = NULL; @@ -156,6 +161,12 @@ score_t* scoreConstructor(int player) Item* item = (Item*)node->element; item->node = node; } + list_Copy(&score->stats->void_chest_inventory, &stats[player]->void_chest_inventory); + for ( node = score->stats->void_chest_inventory.first; node != NULL; node = node->next ) + { + Item* item = (Item*)node->element; + item->node = node; + } int c; for ( c = 0, node = stats[player]->inventory.first; node != NULL; node = node->next, c++ ) { @@ -297,10 +308,10 @@ int saveScore(int player) return -1; } score_t* currentscore = scoreConstructor(player); - list_t* scoresPtr = &topscores; + list_t* scoresPtr = &topscores_json; if ( conductGameChallenges[CONDUCT_MULTIPLAYER] ) { - scoresPtr = &topscoresMultiplayer; + scoresPtr = &topscoresMultiplayer_json; } int c; @@ -350,7 +361,7 @@ int totalScore(score_t* score) for ( node_t* node = score->stats->inventory.first; node != NULL; node = node->next ) { Item* item = (Item*)node->element; - amount += items[item->type].value; + amount += items[item->type].gold_value; } amount += score->stats->GOLD; amount += score->stats->EXP; @@ -364,7 +375,18 @@ int totalScore(score_t* score) { if ( c != HUMAN ) { - amount += score->kills[c] * 100; + if ( c == DEVIL || c == LICH_ICE || c == LICH_FIRE ) + { + amount += std::min(1, score->kills[c]) * 100; + } + else if ( c == LICH ) + { + amount += std::min(3, score->kills[c]) * 100; + } + else + { + amount += score->kills[c] * 100; + } } else { @@ -491,17 +513,24 @@ void loadScore(score_t* score) } for ( int c = 0; c < NUMEFFECTS; c++ ) { - stats[0]->EFFECTS[c] = score->stats->EFFECTS[c]; + stats[0]->setEffectValueUnsafe(c, score->stats->getEffectActive(c)); stats[0]->EFFECTS_TIMERS[c] = score->stats->EFFECTS_TIMERS[c]; + stats[0]->EFFECTS_ACCRETION_TIME[c] = score->stats->EFFECTS_ACCRETION_TIME[c]; } list_FreeAll(&stats[0]->inventory); list_Copy(&stats[0]->inventory, &score->stats->inventory); - for ( node_t* node = stats[0]->inventory.first; node != NULL; node = node->next ) { Item* item = (Item*)node->element; item->node = node; } + list_FreeAll(&stats[0]->void_chest_inventory); + list_Copy(&stats[0]->void_chest_inventory, &score->stats->void_chest_inventory); + for ( node_t* node = stats[0]->void_chest_inventory.first; node != NULL; node = node->next ) + { + Item* item = (Item*)node->element; + item->node = node; + } int c; node_t* node; @@ -586,11 +615,11 @@ void loadScore(int scorenum) node_t* node = nullptr; if ( scoreDisplayMultiplayer ) { - node = list_Node(&topscoresMultiplayer, scorenum); + node = list_Node(&topscoresMultiplayer_json, scorenum); } else { - node = list_Node(&topscores, scorenum); + node = list_Node(&topscores_json, scorenum); } if ( !node ) { @@ -607,10 +636,13 @@ void loadScore(int scorenum) saves all highscores to the scores data file -------------------------------------------------------------------------------*/ - +static ConsoleVariable cvar_scores_json("/scores_json", true); void saveAllScoresJSON(const std::string& scoresfilename) { - return; + if ( !*cvar_scores_json ) + { + return; + } char path[PATH_MAX] = ""; completePath(path, scoresfilename.c_str(), outputdir); @@ -638,43 +670,16 @@ void saveAllScoresJSON(const std::string& scoresfilename) versionNumber = atoi(versionStr); // convert from string to int. // header info - { - rapidjson::Value books_read(rapidjson::kArrayType); - for ( node_t* node = booksRead.first; node != NULL; node = node->next ) - { - char* book = (char*)node->element; - books_read.PushBack(rapidjson::Value(book, d.GetAllocator()), d.GetAllocator()); - } - d.AddMember("books_read", books_read, d.GetAllocator()); - } - - { - rapidjson::Value used_class(rapidjson::kArrayType); - for ( int c = 0; c < NUMCLASSES; c++ ) - { - used_class.PushBack(rapidjson::Value(usedClass[c]), d.GetAllocator()); - } - d.AddMember("used_class", used_class, d.GetAllocator()); - } - - { - rapidjson::Value used_race(rapidjson::kArrayType); - for ( int c = 0; c < NUMRACES; c++ ) - { - used_race.PushBack(rapidjson::Value(usedRace[c]), d.GetAllocator()); - } - d.AddMember("used_race", used_race, d.GetAllocator()); - } // score list node_t* node = nullptr; - if ( scoresfilename == "scores.json" ) + if ( scoresfilename == SCORESFILE ) { - node = topscores.first; + node = topscores_json.first; } - else if ( scoresfilename == "scores_multiplayer.json" ) + else if ( scoresfilename == SCORESFILE_MULTIPLAYER ) { - node = topscoresMultiplayer.first; + node = topscoresMultiplayer_json.first; } else { @@ -687,35 +692,27 @@ void saveAllScoresJSON(const std::string& scoresfilename) score_t* score = (score_t*)node->element; rapidjson::Value entry(rapidjson::kObjectType); - { - rapidjson::Value kills_num(rapidjson::kArrayType); - for ( int c = 0; c < NUMMONSTERS; c++ ) - { - kills_num.PushBack(kills[c], d.GetAllocator()); - } - entry.AddMember("kills", kills_num, d.GetAllocator()); - } + entry.AddMember("name", rapidjson::Value(score->stats->name, d.GetAllocator()), d.GetAllocator()); + entry.AddMember("type", score->stats->type, d.GetAllocator()); + entry.AddMember("sex", score->stats->sex, d.GetAllocator()); + entry.AddMember("race", score->stats->playerRace, d.GetAllocator()); + entry.AddMember("appearance", score->stats->stat_appearance, d.GetAllocator()); + + entry.AddMember("classnum", score->classnum, d.GetAllocator()); + entry.AddMember("dungeonlevel", score->dungeonlevel, d.GetAllocator()); + entry.AddMember("victory", score->victory, d.GetAllocator()); entry.AddMember("completionTime", score->completionTime, d.GetAllocator()); entry.AddMember("conductPenniless", score->conductPenniless, d.GetAllocator()); entry.AddMember("conductFoodless", score->conductFoodless, d.GetAllocator()); entry.AddMember("conductVegetarian", score->conductVegetarian, d.GetAllocator()); entry.AddMember("conductIlliterate", score->conductIlliterate, d.GetAllocator()); - entry.AddMember("type", score->stats->type, d.GetAllocator()); - entry.AddMember("sex", score->stats->sex, d.GetAllocator()); - entry.AddMember("race", score->stats->playerRace, d.GetAllocator()); - entry.AddMember("appearance", score->stats->stat_appearance, d.GetAllocator()); - entry.AddMember("name", rapidjson::Value(score->stats->name, d.GetAllocator()), d.GetAllocator()); entry.AddMember("killer_monster", score->stats->killer_monster, d.GetAllocator()); entry.AddMember("killer_item", score->stats->killer_item, d.GetAllocator()); entry.AddMember("killer", score->stats->killer, d.GetAllocator()); entry.AddMember("killer_name", rapidjson::Value(score->stats->killer_name.c_str(), d.GetAllocator()), d.GetAllocator()); - entry.AddMember("classnum", score->classnum, d.GetAllocator()); - entry.AddMember("dungeonlevel", score->dungeonlevel, d.GetAllocator()); - entry.AddMember("victory", score->victory, d.GetAllocator()); - entry.AddMember("HP", score->stats->HP, d.GetAllocator()); entry.AddMember("MAXHP", score->stats->MAXHP, d.GetAllocator()); entry.AddMember("MP", score->stats->MP, d.GetAllocator()); @@ -731,6 +728,15 @@ void saveAllScoresJSON(const std::string& scoresfilename) entry.AddMember("GOLD", score->stats->GOLD, d.GetAllocator()); entry.AddMember("HUNGER", score->stats->HUNGER, d.GetAllocator()); + { + rapidjson::Value kills_num(rapidjson::kArrayType); + for ( int c = 0; c < NUMMONSTERS; c++ ) + { + kills_num.PushBack(score->kills[c], d.GetAllocator()); + } + entry.AddMember("kills", kills_num, d.GetAllocator()); + } + { rapidjson::Value proficiencies(rapidjson::kArrayType); for ( int c = 0; c < NUMPROFICIENCIES; c++ ) @@ -744,7 +750,7 @@ void saveAllScoresJSON(const std::string& scoresfilename) rapidjson::Value effects(rapidjson::kArrayType); for ( int c = 0; c < NUMEFFECTS; c++ ) { - effects.PushBack(score->stats->EFFECTS[c], d.GetAllocator()); + effects.PushBack(score->stats->getEffectActive(c), d.GetAllocator()); } entry.AddMember("effects", effects, d.GetAllocator()); } @@ -756,6 +762,14 @@ void saveAllScoresJSON(const std::string& scoresfilename) } entry.AddMember("effects_timers", effects_timers, d.GetAllocator()); } + { + rapidjson::Value effects_accretion_time(rapidjson::kArrayType); + for ( int c = 0; c < NUMEFFECTS; c++ ) + { + effects_accretion_time.PushBack(score->stats->EFFECTS_ACCRETION_TIME[c], d.GetAllocator()); + } + entry.AddMember("effects_accretion_time", effects_accretion_time, d.GetAllocator()); + } { @@ -787,7 +801,7 @@ void saveAllScoresJSON(const std::string& scoresfilename) inv_item.AddMember("status", item->status, d.GetAllocator()); inv_item.AddMember("beatitude", item->beatitude, d.GetAllocator()); inv_item.AddMember("count", item->count, d.GetAllocator()); - inv_item.AddMember("appearance", item->appearance, d.GetAllocator()); + inv_item.AddMember("appearance", rapidjson::Value(item->appearance), d.GetAllocator()); inv_item.AddMember("identified", item->identified, d.GetAllocator()); inventory.PushBack(inv_item, d.GetAllocator()); @@ -896,6 +910,34 @@ void saveAllScoresJSON(const std::string& scoresfilename) d.AddMember("scores_list", scores_list, d.GetAllocator()); + { + rapidjson::Value books_read(rapidjson::kArrayType); + for ( node_t* node = booksRead.first; node != NULL; node = node->next ) + { + char* book = (char*)node->element; + books_read.PushBack(rapidjson::Value(book, d.GetAllocator()), d.GetAllocator()); + } + d.AddMember("books_read", books_read, d.GetAllocator()); + } + + { + rapidjson::Value used_class(rapidjson::kArrayType); + for ( int c = 0; c < NUMCLASSES; c++ ) + { + used_class.PushBack(rapidjson::Value(usedClass[c]), d.GetAllocator()); + } + d.AddMember("used_class", used_class, d.GetAllocator()); + } + + { + rapidjson::Value used_race(rapidjson::kArrayType); + for ( int c = 0; c < NUMRACES; c++ ) + { + used_race.PushBack(rapidjson::Value(usedRace[c]), d.GetAllocator()); + } + d.AddMember("used_race", used_race, d.GetAllocator()); + } + // open file File* fp = FileIO::open(path, "wb"); if ( !fp ) @@ -906,6 +948,8 @@ void saveAllScoresJSON(const std::string& scoresfilename) rapidjson::StringBuffer os; rapidjson::PrettyWriter writer(os); + writer.SetIndent(' ', 2); + writer.SetFormatOptions(rapidjson::PrettyFormatOptions::kFormatSingleLineArray); d.Accept(writer); fp->write(os.GetString(), sizeof(char), os.GetSize()); FileIO::close(fp); @@ -916,15 +960,27 @@ void saveAllScoresJSON(const std::string& scoresfilename) void saveAllScores(const std::string& scoresfilename) { + if ( *cvar_scores_json ) + { + saveAllScoresJSON(scoresfilename); + return; + } + + std::string filename = "scores.dat"; + if ( scoresfilename == SCORESFILE_MULTIPLAYER ) + { + filename = "scores_multiplayer.dat"; + } + File* fp; char path[PATH_MAX] = ""; - completePath(path, scoresfilename.c_str(), outputdir); + completePath(path, filename.c_str(), outputdir); // open file if ( (fp = FileIO::open(path, "wb")) == NULL ) { - printlog("error: failed to save '%s!'\n", scoresfilename.c_str()); + printlog("error: failed to save '%s!'\n", filename.c_str()); return; } @@ -972,15 +1028,15 @@ void saveAllScores(const std::string& scoresfilename) // score list node_t* node; int numScoresInFile; - if ( scoresfilename.compare(SCORESFILE) == 0 ) + if ( filename == "scores.dat" ) { - numScoresInFile = list_Size(&topscores); - node = topscores.first; + numScoresInFile = list_Size(&topscores_legacy); + node = topscores_legacy.first; } else { - numScoresInFile = list_Size(&topscoresMultiplayer); - node = topscoresMultiplayer.first; + numScoresInFile = list_Size(&topscoresMultiplayer_legacy); + node = topscoresMultiplayer_legacy.first; } fp->write(&numScoresInFile, sizeof(Uint32), 1); @@ -1044,8 +1100,10 @@ void saveAllScores(const std::string& scoresfilename) } for ( int c = 0; c < NUMEFFECTS; c++ ) { - fp->write(&score->stats->EFFECTS[c], sizeof(bool), 1); + Uint8 effectVal = score->stats->getEffectActive(c); + fp->write(&effectVal, sizeof(Uint8), 1); fp->write(&score->stats->EFFECTS_TIMERS[c], sizeof(Sint32), 1); + fp->write(&score->stats->EFFECTS_ACCRETION_TIME[c], sizeof(Sint32), 1); } for ( int c = 0; c < NUM_CONDUCT_CHALLENGES; ++c ) { @@ -1173,23 +1231,12 @@ void saveAllScores(const std::string& scoresfilename) } FileIO::close(fp); - - /*if ( scoresfilename == "scores.dat" ) - { - std::string scoresjson = "scores.json"; - saveAllScoresJSON(scoresjson); - } - else if ( scoresfilename == "scores_multiplayer.dat" ) - { - std::string scoresjson = "scores_multiplayer.json"; - saveAllScoresJSON(scoresjson); - }*/ } bool deleteScore(bool multiplayer, int index) { auto node = list_Node(multiplayer ? - &topscoresMultiplayer : &topscores, index); + &topscoresMultiplayer_json : &topscores_json, index); if (node) { list_RemoveNode(node); return true; @@ -1232,17 +1279,234 @@ const char* jsonGetStr(rapidjson::Value& d, const char* key) return ""; } +bool verifyScoreStruct(score_t* score, score_t* score2) +{ + assert(totalScore(score) == totalScore(score2)); + + for ( int c = 0; c < NUMMONSTERS; c++ ) + { + assert(score->kills[c] == score2->kills[c]); + } + assert(score->stats->type == score2->stats->type); + assert(score->stats->sex == score2->stats->sex); + assert(score->stats->stat_appearance == score2->stats->stat_appearance); + assert(score->stats->playerRace == score2->stats->playerRace); + std::string name1 = score->stats->name; + std::string name2 = score2->stats->name; + assert(name1 == name2); + assert(score->classnum == score2->classnum); + assert(score->victory == score2->victory); + assert(score->dungeonlevel == score2->dungeonlevel); + + assert(score->completionTime == score2->completionTime); + assert(score->conductPenniless == score2->conductPenniless); + assert(score->conductFoodless == score2->conductFoodless); + assert(score->conductVegetarian == score2->conductVegetarian); + assert(score->conductIlliterate == score2->conductIlliterate); + + assert(score->stats->HP == score2->stats->HP); + assert(score->stats->MAXHP == score2->stats->MAXHP); + assert(score->stats->MP == score2->stats->MP); + assert(score->stats->MAXMP == score2->stats->MAXMP); + assert(score->stats->STR == score2->stats->STR); + assert(score->stats->DEX == score2->stats->DEX); + assert(score->stats->CON == score2->stats->CON); + assert(score->stats->INT == score2->stats->INT); + assert(score->stats->PER == score2->stats->PER); + assert(score->stats->CHR == score2->stats->CHR); + assert(score->stats->EXP == score2->stats->EXP); + assert(score->stats->LVL == score2->stats->LVL); + assert(score->stats->GOLD == score2->stats->GOLD); + assert(score->stats->HUNGER == score2->stats->HUNGER); + + assert(score->stats->killer == score2->stats->killer); + assert(score->stats->killer_monster == score2->stats->killer_monster ); + assert(score->stats->killer_item == score2->stats->killer_item); + assert(score->stats->killer_name == score2->stats->killer_name); + + for ( int c = 0; c < NUMPROFICIENCIES; c++ ) + { + assert(score->stats->getProficiency(c) == score2->stats->getProficiency(c)); + } + for ( int c = 0; c < NUMEFFECTS; c++ ) + { + assert(score->stats->getEffectActive(c) == score2->stats->getEffectActive(c)); + assert(score->stats->EFFECTS_TIMERS[c] == score2->stats->EFFECTS_TIMERS[c]); + assert(score->stats->EFFECTS_ACCRETION_TIME[c] == score2->stats->EFFECTS_ACCRETION_TIME[c]); + } + + for ( int c = 0; c < NUM_CONDUCT_CHALLENGES; ++c ) + { + assert(score->conductGameChallenges[c] == score2->conductGameChallenges[c]); + } + + for ( int c = 0; c < NUM_GAMEPLAY_STATISTICS; ++c ) + { + assert(score->gameStatistics[c] == score2->gameStatistics[c]); + } + + assert(list_Size(&score->stats->inventory) == list_Size(&score2->stats->inventory)); + int inventory_items = list_Size(&score->stats->inventory); + for ( int i = 0; i < inventory_items; ++i ) + { + node_t* node1 = list_Node(&score->stats->inventory, i); + node_t* node2 = list_Node(&score2->stats->inventory, i); + + assert(node1 && node2); + + if ( node1 && node2 ) + { + Item* item1 = (Item*)node1->element; + Item* item2 = (Item*)node2->element; + + assert(item1->type == item2->type); + assert(item1->status == item2->status); + assert(item1->beatitude == item2->beatitude); + assert(item1->count == item2->count); + assert(item1->appearance == item2->appearance); + assert(item1->identified == item2->identified); + + assert((item1 == score->stats->helmet) == (item2 == score2->stats->helmet)); + assert((item1 == score->stats->breastplate) == (item2 == score2->stats->breastplate)); + assert((item1 == score->stats->gloves) == (item2 == score2->stats->gloves)); + assert((item1 == score->stats->shoes) == (item2 == score2->stats->shoes)); + assert((item1 == score->stats->shield) == (item2 == score2->stats->shield)); + assert((item1 == score->stats->weapon) == (item2 == score2->stats->weapon)); + assert((item1 == score->stats->cloak) == (item2 == score2->stats->cloak)); + assert((item1 == score->stats->amulet) == (item2 == score2->stats->amulet)); + assert((item1 == score->stats->ring) == (item2 == score2->stats->ring)); + assert((item1 == score->stats->mask) == (item2 == score2->stats->mask)); + } + } + + assert(list_Size(&score->stats->void_chest_inventory) == list_Size(&score2->stats->void_chest_inventory)); + inventory_items = list_Size(&score->stats->void_chest_inventory); + for ( int i = 0; i < inventory_items; ++i ) + { + node_t* node1 = list_Node(&score->stats->void_chest_inventory, i); + node_t* node2 = list_Node(&score2->stats->void_chest_inventory, i); + + assert(node1 && node2); + + if ( node1 && node2 ) + { + Item* item1 = (Item*)node1->element; + Item* item2 = (Item*)node2->element; + + assert(item1->type == item2->type); + assert(item1->status == item2->status); + assert(item1->beatitude == item2->beatitude); + assert(item1->count == item2->count); + assert(item1->appearance == item2->appearance); + assert(item1->identified == item2->identified); + } + } + + return true; +} + +bool verifyScoreLoader() +{ + int numScores = list_Size(&topscores_legacy); + int numScores2 = list_Size(&topscores_json); + assert(numScores == numScores2); + for ( int i = 0; i < numScores; ++i ) + { + node_t* score1 = list_Node(&topscores_legacy, i); + node_t* score2 = list_Node(&topscores_json, i); + assert(score1 && score2); + if ( score1 && score2 ) + { + verifyScoreStruct((score_t*)score1->element, (score_t*)score2->element); + } + } + + numScores = list_Size(&topscoresMultiplayer_legacy); + numScores2 = list_Size(&topscoresMultiplayer_json); + assert(numScores == numScores2); + for ( int i = 0; i < numScores; ++i ) + { + node_t* score1 = list_Node(&topscoresMultiplayer_legacy, i); + node_t* score2 = list_Node(&topscoresMultiplayer_json, i); + assert(score1 && score2); + if ( score1 && score2 ) + { + verifyScoreStruct((score_t*)score1->element, (score_t*)score2->element); + } + } + + return true; +} + +class FileReadStreamCustomWrapper { +public: + typedef char Ch; //!< Character type (byte). + + //! Constructor. + /*! + \param fp File pointer opened for read. + \param buffer user-supplied buffer. + \param bufferSize size of buffer in bytes. Must >=4 bytes. + */ + FileReadStreamCustomWrapper(File* fp, char* buffer, size_t bufferSize) : fp_(fp), buffer_(buffer), bufferSize_(bufferSize), bufferLast_(0), current_(buffer_), readCount_(0), count_(0), eof_(false) { + RAPIDJSON_ASSERT(fp_ != 0); + RAPIDJSON_ASSERT(bufferSize >= 4); + Read(); + } + + Ch Peek() const { return *current_; } + Ch Take() { Ch c = *current_; Read(); return c; } + size_t Tell() const { return count_ + static_cast(current_ - buffer_); } + + // Not implemented + void Put(Ch) { RAPIDJSON_ASSERT(false); } + void Flush() { RAPIDJSON_ASSERT(false); } + Ch* PutBegin() { RAPIDJSON_ASSERT(false); return 0; } + size_t PutEnd(Ch*) { RAPIDJSON_ASSERT(false); return 0; } + + // For encoding detection only. + const Ch* Peek4() const { + return (current_ + 4 <= bufferLast_) ? current_ : 0; + } + +private: + void Read() { + if ( current_ < bufferLast_ ) + ++current_; + else if ( !eof_ ) { + count_ += readCount_; + readCount_ = fp_->read(buffer_, 1, bufferSize_); + bufferLast_ = buffer_ + readCount_ - 1; + current_ = buffer_; + + if ( readCount_ < bufferSize_ ) { + buffer_[readCount_] = '\0'; + ++bufferLast_; + eof_ = true; + } + } + } + + File* fp_; + Ch* buffer_; + size_t bufferSize_; + Ch* bufferLast_; + Ch* current_; + size_t readCount_; + size_t count_; //!< Number of characters read + bool eof_; +}; + void loadAllScoresJSON(const std::string& scoresfilename) { - return; // clear top scores - if ( scoresfilename == "scores.json" ) + if ( scoresfilename == SCORESFILE ) { - list_FreeAll(&topscores); + list_FreeAll(&topscores_json); } - else if ( scoresfilename == "scores_multiplayer.json" ) + else if ( scoresfilename == SCORESFILE_MULTIPLAYER ) { - list_FreeAll(&topscoresMultiplayer); + list_FreeAll(&topscoresMultiplayer_json); } else { @@ -1261,14 +1525,16 @@ void loadAllScoresJSON(const std::string& scoresfilename) return; } - static char buf[2000000]; - int count = fp->read(buf, sizeof(buf[0]), sizeof(buf) - 1); - buf[count] = '\0'; - rapidjson::StringStream is(buf); - FileIO::close(fp); + static char buf[65536]; + memset(buf, 0, sizeof(buf)); + //int count = fp->read(buf, sizeof(buf[0]), sizeof(buf) - 1); + //buf[count] = '\0'; + FileReadStreamCustomWrapper is(fp, buf, sizeof(buf)); rapidjson::Document d; d.ParseStream(is); + FileIO::close(fp); + if ( !d.HasMember("version") ) { printlog("[JSON]: Error: No 'version' value in json file, or JSON syntax incorrect! %s", path); @@ -1320,6 +1586,10 @@ void loadAllScoresJSON(const std::string& scoresfilename) } } + for ( int c = 0; c < NUMCLASSES; ++c ) + { + usedClass[c] = false; + } int c = 0; for ( auto itr = d["used_class"].Begin(); itr != d["used_class"].End(); ++itr ) { @@ -1328,6 +1598,10 @@ void loadAllScoresJSON(const std::string& scoresfilename) ++c; } + for ( int c = 0; c < NUMRACES; ++c ) + { + usedRace[c] = false; + } c = 0; for ( auto itr = d["used_race"].Begin(); itr != d["used_race"].End(); ++itr ) { @@ -1345,13 +1619,13 @@ void loadAllScoresJSON(const std::string& scoresfilename) for ( auto itr = d["scores_list"].Begin(); itr != d["scores_list"].End(); ++itr ) { node_t* node = nullptr; - if ( scoresfilename == "scores.json" ) + if ( scoresfilename == SCORESFILE ) { - node = list_AddNodeLast(&topscores); + node = list_AddNodeLast(&topscores_json); } - else if ( scoresfilename == "scores_multiplayer.json" ) + else if ( scoresfilename == SCORESFILE_MULTIPLAYER ) { - node = list_AddNodeLast(&topscoresMultiplayer); + node = list_AddNodeLast(&topscoresMultiplayer_json); } else { @@ -1376,6 +1650,10 @@ void loadAllScoresJSON(const std::string& scoresfilename) node->deconstructor = &scoreDeconstructor; node->size = sizeof(score_t); + for ( int c = 0; c < NUMMONSTERS; ++c ) + { + score->kills[c] = false; + } c = 0; for ( auto itr2 = (*itr)["kills"].Begin(); itr2 != (*itr)["kills"].End(); ++itr2 ) { @@ -1418,66 +1696,250 @@ void loadAllScoresJSON(const std::string& scoresfilename) score->stats->LVL = jsonGetInt(*itr, "LVL"); score->stats->GOLD = jsonGetInt(*itr, "GOLD"); score->stats->HUNGER = jsonGetInt(*itr, "HUNGER"); - } -} - -void loadAllScores(const std::string& scoresfilename) -{ - File* fp; - Uint32 c, i; - char path[PATH_MAX] = ""; - completePath(path, scoresfilename.c_str(), outputdir); - // clear top scores - if ( scoresfilename.compare(SCORESFILE) == 0 ) - { - list_FreeAll(&topscores); - } - else - { - list_FreeAll(&topscoresMultiplayer); - } + c = 0; + for ( auto itr2 = (*itr)["proficiencies"].Begin(); itr2 != (*itr)["proficiencies"].End(); ++itr2 ) + { + if ( c >= NUMPROFICIENCIES ) { break; } + score->stats->setProficiency(c, itr2->GetInt()); + ++c; + } - // open file - if ( (fp = FileIO::open(path, "rb")) == NULL ) - { - return; - } + c = 0; + for ( auto itr2 = (*itr)["effects"].Begin(); itr2 != (*itr)["effects"].End(); ++itr2 ) + { + if ( c >= NUMEFFECTS ) { break; } + score->stats->setEffectValueUnsafe(c, itr2->GetInt()); + ++c; + } - // magic number - char checkstr[64]; - fp->read(checkstr, sizeof(char), strlen("BARONYSCORES")); - if ( strncmp(checkstr, "BARONYSCORES", strlen("BARONYSCORES")) ) - { - printlog("error: '%s' is corrupt!\n", scoresfilename.c_str()); - FileIO::close(fp); - return; - } + c = 0; + for ( auto itr2 = (*itr)["effects_timers"].Begin(); itr2 != (*itr)["effects_timers"].End(); ++itr2 ) + { + if ( c >= NUMEFFECTS ) { break; } + score->stats->EFFECTS_TIMERS[c] = itr2->GetInt(); + ++c; + } - fp->read(checkstr, sizeof(char), strlen(VERSION)); + c = 0; + for ( auto itr2 = (*itr)["effects_accretion_time"].Begin(); itr2 != (*itr)["effects_accretion_time"].End(); ++itr2 ) + { + if ( c >= NUMEFFECTS ) { break; } + score->stats->EFFECTS_ACCRETION_TIME[c] = itr2->GetInt(); + ++c; + } - int versionNumber = 300; - char versionStr[4] = "000"; - i = 0; - for ( int j = 0; j < strlen(VERSION); ++j ) - { - if ( checkstr[j] >= '0' && checkstr[j] <= '9' ) + for ( int c = 0; c < NUM_CONDUCT_CHALLENGES; ++c ) { - versionStr[i] = checkstr[j]; // copy all integers into versionStr. - ++i; - if ( i == 3 ) - { - versionStr[i] = '\0'; - break; // written 3 characters, add termination and break loop. - } + score->conductGameChallenges[c] = 0; } - } + c = 0; + for ( auto itr2 = (*itr)["conducts"].Begin(); itr2 != (*itr)["conducts"].End(); ++itr2 ) + { + if ( c >= NUM_CONDUCT_CHALLENGES ) { break; } + score->conductGameChallenges[c] = itr2->GetInt(); + ++c; + } + + for ( int c = 0; c < NUM_GAMEPLAY_STATISTICS; ++c ) + { + score->gameStatistics[c] = 0; + } + c = 0; + for ( auto itr2 = (*itr)["statistics"].Begin(); itr2 != (*itr)["statistics"].End(); ++itr2 ) + { + if ( c >= NUM_GAMEPLAY_STATISTICS ) { break; } + score->gameStatistics[c] = itr2->GetInt(); + ++c; + } + + score->stats->leader_uid = 0; + score->stats->FOLLOWERS.first = NULL; + score->stats->FOLLOWERS.last = NULL; + + c = 0; + for ( auto itr2 = (*itr)["inventory"].Begin(); itr2 != (*itr)["inventory"].End(); ++itr2 ) + { + ItemType type = WOODEN_SHIELD; + Status status = EXCELLENT; + Sint16 beatitude = 0; + Sint16 count = 1; + Uint32 appearance = 0; + bool identified = true; + + for ( auto itemItr = itr2->MemberBegin(); itemItr != itr2->MemberEnd(); ++itemItr ) + { + if ( !strcmp(itemItr->name.GetString(), "type") && itemItr->value.IsInt() ) + { + int val = itemItr->value.GetInt(); + if ( val >= 0 && val < NUMITEMS ) + { + type = static_cast(val); + } + } + if ( !strcmp(itemItr->name.GetString(), "status") && itemItr->value.IsInt() ) + { + int val = itemItr->value.GetInt(); + if ( val >= 0 && val <= EXCELLENT ) + { + status = static_cast(val); + } + } + if ( !strcmp(itemItr->name.GetString(), "beatitude") && itemItr->value.IsInt() ) + { + beatitude = itemItr->value.GetInt(); + } + if ( !strcmp(itemItr->name.GetString(), "count") && itemItr->value.IsInt() ) + { + count = itemItr->value.GetInt(); + } + if ( !strcmp(itemItr->name.GetString(), "appearance") ) + { + if ( itemItr->value.IsUint() ) + { + appearance = itemItr->value.GetUint(); + } + else if ( itemItr->value.IsInt() ) + { + appearance = itemItr->value.GetInt(); + } + } + if ( !strcmp(itemItr->name.GetString(), "identified") ) + { + if ( itemItr->value.IsInt() ) + { + identified = itemItr->value.GetInt() ? true : 0; + } + else if ( itemItr->value.IsBool() ) + { + identified = itemItr->value.GetBool(); + } + } + } + newItem(type, status, beatitude, count, appearance, identified, &score->stats->inventory); + ++c; + } + + int num_inventory_items = c; + c = 0; + const std::vector> player_slots = { + {"helmet", &score->stats->helmet}, + {"breastplate", &score->stats->breastplate}, + {"gloves", &score->stats->gloves}, + {"shoes", &score->stats->shoes}, + {"shield", &score->stats->shield}, + {"weapon", &score->stats->weapon}, + {"cloak", &score->stats->cloak}, + {"amulet", &score->stats->amulet}, + {"ring", &score->stats->ring}, + {"mask", &score->stats->mask}, + }; + for ( auto equip_slot : player_slots ) + { + equip_slot.second = nullptr; + } + for ( auto itr2 = (*itr)["equipped"].Begin(); itr2 != (*itr)["equipped"].End(); ++itr2 ) + { + if ( itr2->IsInt() ) + { + int inventory_item_num = itr2->GetInt(); + if ( inventory_item_num >= 0 && inventory_item_num < num_inventory_items ) + { + if ( node_t* node = list_Node(&score->stats->inventory, inventory_item_num) ) + { + if ( c >= 0 && c < player_slots.size() ) + { + *(player_slots[c].second) = (Item*)node->element; + } + } + } + } + ++c; + } + + score->stats->monster_sound = NULL; + score->stats->monster_idlevar = 0; + } +} + +void loadAllScores(const std::string& scoresfilename) +{ + // clear top scores + if ( scoresfilename == SCORESFILE ) + { + list_FreeAll(&topscores_legacy); + } + else if ( scoresfilename == SCORESFILE_MULTIPLAYER ) + { + list_FreeAll(&topscoresMultiplayer_legacy); + } + + std::string filename = "scores.dat"; + if ( scoresfilename == SCORESFILE_MULTIPLAYER ) + { + filename = "scores_multiplayer.dat"; + } + + File* fp; + Uint32 c, i; + char path[PATH_MAX] = ""; + + // check for existence of new JSON format + { + completePath(path, scoresfilename.c_str(), outputdir); + if ( (fp = FileIO::open(path, "rb")) != NULL ) + { + // found new json file, let's read that instead + FileIO::close(fp); + loadAllScoresJSON(scoresfilename); + return; + } + } + + completePath(path, filename.c_str(), outputdir); + + // open file + if ( (fp = FileIO::open(path, "rb")) == NULL ) + { + return; + } + + printlog("notice: reading legacy scoresfile: '%s'...", filename.c_str()); + + // magic number + char checkstr[64]; + fp->read(checkstr, sizeof(char), strlen("BARONYSCORES")); + if ( strncmp(checkstr, "BARONYSCORES", strlen("BARONYSCORES")) ) + { + printlog("error: '%s' is corrupt!\n", filename.c_str()); + FileIO::close(fp); + return; + } + + fp->read(checkstr, sizeof(char), strlen(VERSION)); + + int versionNumber = 300; + char versionStr[4] = "000"; + i = 0; + for ( int j = 0; j < strlen(VERSION); ++j ) + { + if ( checkstr[j] >= '0' && checkstr[j] <= '9' ) + { + versionStr[i] = checkstr[j]; // copy all integers into versionStr. + ++i; + if ( i == 3 ) + { + versionStr[i] = '\0'; + break; // written 3 characters, add termination and break loop. + } + } + } versionNumber = atoi(versionStr); // convert from string to int. - printlog("notice: '%s' version number %d", scoresfilename.c_str(), versionNumber); + printlog("notice: '%s' version number %d", filename.c_str(), versionNumber); if ( versionNumber < 200 || versionNumber > 999 ) { // if version number less than v2.0.0, or more than 3 digits, abort and rebuild scores file. - printlog("error: '%s' is corrupt!\n", scoresfilename.c_str()); + printlog("error: '%s' is corrupt!\n", filename.c_str()); FileIO::close(fp); return; } @@ -1487,6 +1949,12 @@ void loadAllScores(const std::string& scoresfilename) fp->read(&c, sizeof(Uint32), 1); for ( int i = 0; i < c; i++ ) { + if ( i >= 1000 ) + { + // break early, probably something went tremendously wrong + break; + } + // to investigate Uint32 booknamelen = 0; fp->read(&booknamelen, sizeof(Uint32), 1); @@ -1529,6 +1997,17 @@ void loadAllScores(const std::string& scoresfilename) usedClass[c] = false; } } + else if ( versionNumber <= 432 ) + { + if ( c < 21 ) + { + fp->read(&usedClass[c], sizeof(bool), 1); + } + else + { + usedClass[c] = false; + } + } else { fp->read(&usedClass[c], sizeof(bool), 1); @@ -1542,6 +2021,17 @@ void loadAllScores(const std::string& scoresfilename) // don't read race info. usedRace[c] = false; } + else if ( versionNumber <= 432 ) + { + if ( c < 13 ) + { + fp->read(&usedRace[c], sizeof(bool), 1); + } + else + { + usedRace[c] = false; + } + } else { fp->read(&usedRace[c], sizeof(bool), 1); @@ -1553,14 +2043,36 @@ void loadAllScores(const std::string& scoresfilename) fp->read(&numscores, sizeof(Uint32), 1); for ( int i = 0; i < numscores; i++ ) { + if ( i >= 1000 ) + { + // break early, probably something went tremendously wrong + break; + } + node_t* node = nullptr; if ( scoresfilename.compare(SCORESFILE) == 0 ) { - node = list_AddNodeLast(&topscores); + if ( *cvar_scores_json ) + { + // we're migrating to new format + node = list_AddNodeLast(&topscores_json); + } + else + { + node = list_AddNodeLast(&topscores_legacy); + } } else { - node = list_AddNodeLast(&topscoresMultiplayer); + if ( *cvar_scores_json ) + { + // we're migrating to new format + node = list_AddNodeLast(&topscoresMultiplayer_json); + } + else + { + node = list_AddNodeLast(&topscoresMultiplayer_legacy); + } } score_t* score = (score_t*) malloc(sizeof(score_t)); if ( !score ) @@ -1625,6 +2137,21 @@ void loadAllScores(const std::string& scoresfilename) } } } + else if ( versionNumber <= 432 ) + { + // legacy nummonsters + for ( int c = 0; c < NUMMONSTERS; c++ ) + { + if ( c <= 37 ) + { + fp->read(&score->kills[c], sizeof(Sint32), 1); + } + else + { + score->kills[c] = 0; + } + } + } else { for ( int c = 0; c < NUMMONSTERS; c++ ) @@ -1700,14 +2227,17 @@ void loadAllScores(const std::string& scoresfilename) { if ( c < 16 ) { - fp->read(&score->stats->EFFECTS[c], sizeof(bool), 1); + bool effectVal = false; + fp->read(&effectVal, sizeof(bool), 1); + score->stats->setEffectValueUnsafe(c, effectVal ? 1 : 0); fp->read(&score->stats->EFFECTS_TIMERS[c], sizeof(Sint32), 1); } else { - score->stats->EFFECTS[c] = false; + score->stats->clearEffect(c); score->stats->EFFECTS_TIMERS[c] = 0; } + score->stats->EFFECTS_ACCRETION_TIME[c] = 0; } } else if ( versionNumber < 302 ) @@ -1716,14 +2246,17 @@ void loadAllScores(const std::string& scoresfilename) { if ( c < 19 ) { - fp->read(&score->stats->EFFECTS[c], sizeof(bool), 1); + bool effectVal = false; + fp->read(&effectVal, sizeof(bool), 1); + score->stats->setEffectValueUnsafe(c, effectVal ? 1 : 0); fp->read(&score->stats->EFFECTS_TIMERS[c], sizeof(Sint32), 1); } else { - score->stats->EFFECTS[c] = false; + score->stats->clearEffect(c); score->stats->EFFECTS_TIMERS[c] = 0; } + score->stats->EFFECTS_ACCRETION_TIME[c] = 0; } } else if ( versionNumber <= 323 ) @@ -1732,14 +2265,17 @@ void loadAllScores(const std::string& scoresfilename) { if ( c < 32 ) { - fp->read(&score->stats->EFFECTS[c], sizeof(bool), 1); + bool effectVal = false; + fp->read(&effectVal, sizeof(bool), 1); + score->stats->setEffectValueUnsafe(c, effectVal ? 1 : 0); fp->read(&score->stats->EFFECTS_TIMERS[c], sizeof(Sint32), 1); } else { - score->stats->EFFECTS[c] = false; + score->stats->clearEffect(c); score->stats->EFFECTS_TIMERS[c] = 0; } + score->stats->EFFECTS_ACCRETION_TIME[c] = 0; } } else if ( versionNumber <= 411 ) @@ -1748,22 +2284,50 @@ void loadAllScores(const std::string& scoresfilename) { if ( c < 40 ) { - fp->read(&score->stats->EFFECTS[c], sizeof(bool), 1); + bool effectVal = false; + fp->read(&effectVal, sizeof(bool), 1); + score->stats->setEffectValueUnsafe(c, effectVal ? 1 : 0); fp->read(&score->stats->EFFECTS_TIMERS[c], sizeof(Sint32), 1); } else { - score->stats->EFFECTS[c] = false; + score->stats->clearEffect(c); score->stats->EFFECTS_TIMERS[c] = 0; } + score->stats->EFFECTS_ACCRETION_TIME[c] = 0; + } + } + else if ( versionNumber <= 432 ) + { + for ( int c = 0; c < 64; c++ ) + { + bool effectVal = false; + fp->read(&effectVal, sizeof(bool), 1); + score->stats->setEffectValueUnsafe(c, effectVal ? 1 : 0); + fp->read(&score->stats->EFFECTS_TIMERS[c], sizeof(Sint32), 1); + score->stats->EFFECTS_ACCRETION_TIME[c] = 0; + } + } + else if ( versionNumber <= 433 ) + { + for ( int c = 0; c < 128; c++ ) + { + bool effectVal = false; + fp->read(&effectVal, sizeof(bool), 1); + score->stats->setEffectValueUnsafe(c, effectVal ? 1 : 0); + fp->read(&score->stats->EFFECTS_TIMERS[c], sizeof(Sint32), 1); + fp->read(&score->stats->EFFECTS_ACCRETION_TIME[c], sizeof(Sint32), 1); } } else { for ( int c = 0; c < NUMEFFECTS; c++ ) { - fp->read(&score->stats->EFFECTS[c], sizeof(bool), 1); + Uint8 effectVal = 0; + fp->read(&effectVal, sizeof(Uint8), 1); + score->stats->setEffectValueUnsafe(c, effectVal); fp->read(&score->stats->EFFECTS_TIMERS[c], sizeof(Sint32), 1); + fp->read(&score->stats->EFFECTS_ACCRETION_TIME[c], sizeof(Sint32), 1); } } @@ -1777,1454 +2341,31 @@ void loadAllScores(const std::string& scoresfilename) { fp->read(&score->gameStatistics[c], sizeof(Sint32), 1); } - } - else - { - for ( int c = 0; c < NUM_CONDUCT_CHALLENGES; ++c ) - { - score->conductGameChallenges[c] = 0; - } - } - score->stats->leader_uid = 0; - score->stats->FOLLOWERS.first = NULL; - score->stats->FOLLOWERS.last = NULL; - - // inventory - int numitems = 0; - fp->read(&numitems, sizeof(Uint32), 1); - score->stats->inventory.first = NULL; - score->stats->inventory.last = NULL; - for ( int c = 0; c < numitems; c++ ) - { - ItemType type; - Status status; - Sint16 beatitude; - Sint16 count; - Uint32 appearance; - bool identified; - fp->read(&type, sizeof(ItemType), 1); - fp->read(&status, sizeof(Status), 1); - fp->read(&beatitude, sizeof(Sint16), 1); - fp->read(&count, sizeof(Sint16), 1); - fp->read(&appearance, sizeof(Uint32), 1); - fp->read(&identified, sizeof(bool), 1); - newItem(type, status, beatitude, count, appearance, identified, &score->stats->inventory); - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->helmet = (Item*)node->element; - } - else - { - score->stats->helmet = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->breastplate = (Item*)node->element; - } - else - { - score->stats->breastplate = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->gloves = (Item*)node->element; - } - else - { - score->stats->gloves = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->shoes = (Item*)node->element; - } - else - { - score->stats->shoes = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->shield = (Item*)node->element; - } - else - { - score->stats->shield = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->weapon = (Item*)node->element; - } - else - { - score->stats->weapon = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->cloak = (Item*)node->element; - } - else - { - score->stats->cloak = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->amulet = (Item*)node->element; - } - else - { - score->stats->amulet = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->ring = (Item*)node->element; - } - else - { - score->stats->ring = NULL; - } - fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&score->stats->inventory, c); - if ( node ) - { - score->stats->mask = (Item*)node->element; - } - else - { - score->stats->mask = NULL; - } - - score->stats->monster_sound = NULL; - score->stats->monster_idlevar = 0; - } - - FileIO::close(fp); -} - -/*------------------------------------------------------------------------------- - - saveGameOld - - Saves the player character as they were at the start of the - last level - --------------------------------------------------------------------------------*/ - -int saveGameOld(int saveIndex) -{ - if ( gameModeManager.getMode() != GameModeManager_t::GAME_MODE_DEFAULT ) - { - return 1; - } - - File* fp; - char savefile[PATH_MAX] = ""; - char path[PATH_MAX] = ""; - - // open file - if ( !intro ) - { - messagePlayer(clientnum, MESSAGE_MISC, Language::get(1121)); - } - - if ( multiplayer == SINGLE ) - { - strncpy(savefile, setSaveGameFileName(true, SaveFileType::MAIN, saveIndex).c_str(), PATH_MAX - 1); - } - else - { - strncpy(savefile, setSaveGameFileName(false, SaveFileType::MAIN, saveIndex).c_str(), PATH_MAX - 1); - } - completePath(path, savefile, outputdir); - - if ( (fp = FileIO::open(path, "wb")) == NULL ) - { - printlog("warning: failed to save '%s'!\n", path); - return 1; - } - - // write header info - fp->printf("BARONYSAVEGAME"); - fp->printf(VERSION); - fp->write(&uniqueGameKey, sizeof(Uint32), 1); - - Uint32 hash = 0; -#ifdef WINDOWS - struct _stat result; - if ( _stat(path, &result) == 0 ) - { - struct tm *tm = localtime(&result.st_mtime); - if ( tm ) - { - hash = tm->tm_hour + tm->tm_mday * tm->tm_year + tm->tm_wday + tm->tm_yday; - } - } -#else - struct stat result; - if ( stat(path, &result) == 0 ) - { - struct tm *tm = localtime(&result.st_mtime); - if ( tm ) - { - hash = tm->tm_hour + tm->tm_mday * tm->tm_year + tm->tm_wday + tm->tm_yday; - } - } -#endif // WINDOWS - hash += (stats[clientnum]->STR + stats[clientnum]->LVL + stats[clientnum]->DEX * stats[clientnum]->INT); - hash += (stats[clientnum]->CON * stats[clientnum]->PER + std::min(stats[clientnum]->GOLD, 5000) - stats[clientnum]->CON); - hash += (stats[clientnum]->HP - stats[clientnum]->MP); - hash += (currentlevel); - Uint32 writeCurrentLevel = (hash << 8); - writeCurrentLevel |= (currentlevel & 0xFF); - - Sint16 players_connected = 0; - for (int c = 0; c < MAXPLAYERS; ++c) { - if (!client_disconnected[c]) { - players_connected |= 1 << c; - } - } - fp->write(&players_connected, sizeof(Sint16), 1); - - Sint16 mul = 0; - if ( multiplayer == SINGLE ) { - if (splitscreen) { - mul = SPLITSCREEN; - } else { - mul = SINGLE; - } - } else { - if (multiplayer == SERVER && directConnect) { - mul = DIRECTSERVER; - } - else if (multiplayer == SERVER && LobbyHandler.hostingType == LobbyHandler_t::LobbyServiceType::LOBBY_CROSSPLAY) { - mul = SERVERCROSSPLAY; - } - else if (multiplayer == SERVER || multiplayer == CLIENT) { - mul = directConnect ? multiplayer + 2 : multiplayer; - } - else { - assert(0 && "Unknown game save type!"); - } - } - fp->write(&mul, sizeof(Sint16), 1); - - fp->write(&clientnum, sizeof(Uint32), 1); - fp->write(&mapseed, sizeof(Uint32), 1); - fp->write(&writeCurrentLevel, sizeof(Uint32), 1); - fp->write(&secretlevel, sizeof(bool), 1); - fp->write(&completionTime, sizeof(Uint32), 1); - fp->write(&conductPenniless, sizeof(bool), 1); - fp->write(&conductFoodless, sizeof(bool), 1); - fp->write(&conductVegetarian, sizeof(bool), 1); - fp->write(&conductIlliterate, sizeof(bool), 1); - for ( int c = 0; c < NUM_CONDUCT_CHALLENGES; ++c ) - { - fp->write(&conductGameChallenges[c], sizeof(Sint32), 1); - } - for ( int c = 0; c < NUM_GAMEPLAY_STATISTICS; ++c ) - { - fp->write(&gameStatistics[c], sizeof(Sint32), 1); - } - fp->write(&svFlags, sizeof(Uint32), 1); - - if (splitscreen) - { - for ( int player = 0; player < MAXPLAYERS; player++ ) - { - // write hotbar items - for ( auto& hotbarSlot : players[player]->hotbar.slots() ) - { - int index = list_Size(&stats[player]->inventory); - Item* item = uidToItem(hotbarSlot.item); - if ( item ) - { - index = list_Index(item->node); - } - fp->write(&index, sizeof(Uint32), 1); - } - - // write spells - Uint32 numspells = list_Size(&players[player]->magic.spellList); - fp->write(&numspells, sizeof(Uint32), 1); - for ( node_t* node = players[player]->magic.spellList.first; node != NULL; node = node->next ) - { - spell_t* spell = (spell_t*)node->element; - fp->write(&spell->ID, sizeof(Uint32), 1); - } - - // write alchemy recipes - Uint32 numrecipes = clientLearnedAlchemyRecipes[player].size(); - fp->write(&numrecipes, sizeof(Uint32), 1); - for ( auto& entry : clientLearnedAlchemyRecipes[player] ) - { - fp->write(&entry.first, sizeof(Sint32), 1); - fp->write(&entry.second.first, sizeof(Sint32), 1); - fp->write(&entry.second.second, sizeof(Sint32), 1); - } - - // write scrolls known - Uint32 numscrolls = clientLearnedScrollLabels[player].size(); - fp->write(&numscrolls, sizeof(Uint32), 1); - for ( auto& entry : clientLearnedScrollLabels[player] ) - { - fp->write(&entry, sizeof(Sint32), 1); - } - } - } - else - { - // write hotbar items - for ( auto& hotbarSlot : players[clientnum]->hotbar.slots() ) - { - int index = list_Size(&stats[clientnum]->inventory); - Item* item = uidToItem(hotbarSlot.item); - if ( item ) - { - index = list_Index(item->node); - } - fp->write(&index, sizeof(Uint32), 1); - } - - // write spells - Uint32 numspells = list_Size(&players[clientnum]->magic.spellList); - fp->write(&numspells, sizeof(Uint32), 1); - for ( node_t* node = players[clientnum]->magic.spellList.first; node != NULL; node = node->next ) - { - spell_t* spell = (spell_t*)node->element; - fp->write(&spell->ID, sizeof(Uint32), 1); - } - - // write alchemy recipes - Uint32 numrecipes = clientLearnedAlchemyRecipes[clientnum].size(); - fp->write(&numrecipes, sizeof(Uint32), 1); - for ( auto& entry : clientLearnedAlchemyRecipes[clientnum] ) - { - fp->write(&entry.first, sizeof(Sint32), 1); - fp->write(&entry.second.first, sizeof(Sint32), 1); - fp->write(&entry.second.second, sizeof(Sint32), 1); - } - - // write scrolls known - Uint32 numscrolls = clientLearnedScrollLabels[clientnum].size(); - fp->write(&numscrolls, sizeof(Uint32), 1); - for ( auto& entry : clientLearnedScrollLabels[clientnum] ) - { - fp->write(&entry, sizeof(Sint32), 1); - } - } - - // player data - for ( int player = 0; player < MAXPLAYERS; player++ ) - { - fp->write(&client_classes[player], sizeof(Uint32), 1); - for ( int c = 0; c < NUMMONSTERS; c++ ) - { - fp->write(&kills[c], sizeof(Sint32), 1); - } - fp->write(&stats[player]->type, sizeof(Monster), 1); - fp->write(&stats[player]->sex, sizeof(sex_t), 1); - Uint32 raceAndAppearance = 0; - raceAndAppearance |= (stats[player]->playerRace << 8); - raceAndAppearance |= (stats[player]->stat_appearance); - fp->write(&raceAndAppearance, sizeof(Uint32), 1); - fp->write(stats[player]->name, sizeof(char), 32); - fp->write(&stats[player]->HP, sizeof(Sint32), 1); - fp->write(&stats[player]->MAXHP, sizeof(Sint32), 1); - fp->write(&stats[player]->MP, sizeof(Sint32), 1); - fp->write(&stats[player]->MAXMP, sizeof(Sint32), 1); - fp->write(&stats[player]->STR, sizeof(Sint32), 1); - fp->write(&stats[player]->DEX, sizeof(Sint32), 1); - fp->write(&stats[player]->CON, sizeof(Sint32), 1); - fp->write(&stats[player]->INT, sizeof(Sint32), 1); - fp->write(&stats[player]->PER, sizeof(Sint32), 1); - fp->write(&stats[player]->CHR, sizeof(Sint32), 1); - fp->write(&stats[player]->EXP, sizeof(Sint32), 1); - fp->write(&stats[player]->LVL, sizeof(Sint32), 1); - fp->write(&stats[player]->GOLD, sizeof(Sint32), 1); - fp->write(&stats[player]->HUNGER, sizeof(Sint32), 1); - for ( int c = 0; c < NUMPROFICIENCIES; c++ ) - { - Sint32 val = stats[player]->getProficiency(c); - fp->write(&val, sizeof(Sint32), 1); - } - for ( int c = 0; c < NUMEFFECTS; c++ ) - { - fp->write(&stats[player]->EFFECTS[c], sizeof(bool), 1); - fp->write(&stats[player]->EFFECTS_TIMERS[c], sizeof(Sint32), 1); - } - for ( int c = 0; c < 32; c++ ) - { - fp->write(&stats[player]->MISC_FLAGS[c], sizeof(Sint32), 1); - } - - // inventory - if ( player == clientnum || splitscreen ) - { - int c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - for ( node_t* node = stats[player]->inventory.first; node != NULL; node = node->next ) - { - Item* item = (Item*)node->element; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - fp->write(&item->x, sizeof(Sint32), 1); - fp->write(&item->y, sizeof(Sint32), 1); - } - if ( stats[player]->helmet ) - { - c = list_Index(stats[player]->helmet->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->breastplate ) - { - c = list_Index(stats[player]->breastplate->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->gloves ) - { - c = list_Index(stats[player]->gloves->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->shoes ) - { - c = list_Index(stats[player]->shoes->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->shield ) - { - c = list_Index(stats[player]->shield->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->weapon ) - { - c = list_Index(stats[player]->weapon->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->cloak ) - { - c = list_Index(stats[player]->cloak->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->amulet ) - { - c = list_Index(stats[player]->amulet->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->ring ) - { - c = list_Index(stats[player]->ring->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - if ( stats[player]->mask ) - { - c = list_Index(stats[player]->mask->node); - fp->write(&c, sizeof(Uint32), 1); - } - else - { - c = list_Size(&stats[player]->inventory); - fp->write(&c, sizeof(Uint32), 1); - } - } - else - { - if ( multiplayer == SERVER ) - { - if ( stats[player]->helmet ) - { - Item* item = stats[player]->helmet; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->breastplate ) - { - Item* item = stats[player]->breastplate; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->gloves ) - { - Item* item = stats[player]->gloves; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->shoes ) - { - Item* item = stats[player]->shoes; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->shield ) - { - Item* item = stats[player]->shield; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->weapon ) - { - Item* item = stats[player]->weapon; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->cloak ) - { - Item* item = stats[player]->cloak; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->amulet ) - { - Item* item = stats[player]->amulet; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->ring ) - { - Item* item = stats[player]->ring; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - if ( stats[player]->mask ) - { - Item* item = stats[player]->mask; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - } - else - { - int c = NUMITEMS; - fp->write(&c, sizeof(ItemType), 1); - } - } - } - FileIO::close(fp); - - // clients don't save follower info - if ( multiplayer == CLIENT ) - { - return 0; - } - - if ( multiplayer == SINGLE ) - { - strncpy(savefile, setSaveGameFileName(true, SaveFileType::FOLLOWERS, saveIndex).c_str(), PATH_MAX - 1); - } - else - { - strncpy(savefile, setSaveGameFileName(false, SaveFileType::FOLLOWERS, saveIndex).c_str(), PATH_MAX - 1); - } - completePath(path, savefile, outputdir); - - // now we save the follower information - if ( (fp = FileIO::open(path, "wb")) == NULL ) - { - printlog("warning: failed to save '%s'!\n", path); - return 1; - } - fp->printf("BARONYSAVEGAMEFOLLOWERS"); - fp->printf(VERSION); - - // write follower information - for ( int c = 0; c < MAXPLAYERS; c++ ) - { - // record number of followers for this player - Uint32 size = list_Size(&stats[c]->FOLLOWERS); - fp->write(&size, sizeof(Uint32), 1); - - // get followerStats - for ( int i = 0; i < size; i++ ) - { - node_t* node = list_Node(&stats[c]->FOLLOWERS, i); - if ( node ) - { - Entity* follower = uidToEntity(*((Uint32*)node->element)); - Stat* followerStats = (follower) ? follower->getStats() : NULL; - if ( followerStats ) - { - // record follower stats - fp->write(&followerStats->type, sizeof(Monster), 1); - fp->write(&followerStats->sex, sizeof(sex_t), 1); - fp->write(&followerStats->stat_appearance, sizeof(Uint32), 1); - fp->write(followerStats->name, sizeof(char), 32); - fp->write(&followerStats->HP, sizeof(Sint32), 1); - fp->write(&followerStats->MAXHP, sizeof(Sint32), 1); - fp->write(&followerStats->MP, sizeof(Sint32), 1); - fp->write(&followerStats->MAXMP, sizeof(Sint32), 1); - fp->write(&followerStats->STR, sizeof(Sint32), 1); - fp->write(&followerStats->DEX, sizeof(Sint32), 1); - fp->write(&followerStats->CON, sizeof(Sint32), 1); - fp->write(&followerStats->INT, sizeof(Sint32), 1); - fp->write(&followerStats->PER, sizeof(Sint32), 1); - fp->write(&followerStats->CHR, sizeof(Sint32), 1); - fp->write(&followerStats->EXP, sizeof(Sint32), 1); - fp->write(&followerStats->LVL, sizeof(Sint32), 1); - fp->write(&followerStats->GOLD, sizeof(Sint32), 1); - fp->write(&followerStats->HUNGER, sizeof(Sint32), 1); - - for ( int j = 0; j < NUMPROFICIENCIES; j++ ) - { - Sint32 val = followerStats->getProficiency(j); - fp->write(&val, sizeof(Sint32), 1); - } - for ( int j = 0; j < NUMEFFECTS; j++ ) - { - fp->write(&followerStats->EFFECTS[j], sizeof(bool), 1); - fp->write(&followerStats->EFFECTS_TIMERS[j], sizeof(Sint32), 1); - } - for ( int j = 0; j < 32; ++j ) - { - fp->write(&followerStats->MISC_FLAGS[j], sizeof(Sint32), 1); - } - - Uint32 numAttributes = followerStats->attributes.size(); - fp->write(&numAttributes, sizeof(Uint32), 1); - for ( auto& attribute : followerStats->attributes ) - { - fp->write(attribute.first.c_str(), sizeof(char), 32); - fp->write(attribute.second.c_str(), sizeof(char), 32); - } - - // record follower inventory - Uint32 invSize = list_Size(&followerStats->inventory); - fp->write(&invSize, sizeof(Uint32), 1); - for ( node_t* node = followerStats->inventory.first; node != NULL; node = node->next ) - { - Item* item = (Item*)node->element; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - fp->write(&item->x, sizeof(Sint32), 1); - fp->write(&item->y, sizeof(Sint32), 1); - } - - // record follower equipment (since NPCs never store equipment as inventory) - if ( followerStats->helmet ) - { - Item* item = followerStats->helmet; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->breastplate ) - { - Item* item = followerStats->breastplate; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->gloves ) - { - Item* item = followerStats->gloves; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->shoes ) - { - Item* item = followerStats->shoes; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->shield ) - { - Item* item = followerStats->shield; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->weapon ) - { - Item* item = followerStats->weapon; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->cloak ) - { - Item* item = followerStats->cloak; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->amulet ) - { - Item* item = followerStats->amulet; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->ring ) - { - Item* item = followerStats->ring; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - if ( followerStats->mask ) - { - Item* item = followerStats->mask; - fp->write(&item->type, sizeof(ItemType), 1); - fp->write(&item->status, sizeof(Status), 1); - fp->write(&item->beatitude, sizeof(Sint16), 1); - fp->write(&item->count, sizeof(Sint16), 1); - fp->write(&item->appearance, sizeof(Uint32), 1); - fp->write(&item->identified, sizeof(bool), 1); - } - else - { - ItemType tempItem = static_cast(NUMITEMS); - fp->write(&tempItem, sizeof(ItemType), 1); - } - } - } - } - } - - - FileIO::close(fp); - return 0; -} - -/*------------------------------------------------------------------------------- - - loadGameOld - - Loads a character savegame stored in SAVEGAMEFILE - --------------------------------------------------------------------------------*/ - -int loadGameOld(int player, int saveIndex) -{ - File* fp; - - char savefile[PATH_MAX] = ""; - char path[PATH_MAX] = ""; - if ( multiplayer == SINGLE ) - { - strncpy(savefile, setSaveGameFileName(true, SaveFileType::MAIN, saveIndex).c_str(), PATH_MAX - 1); - } - else - { - strncpy(savefile, setSaveGameFileName(false, SaveFileType::MAIN, saveIndex).c_str(), PATH_MAX - 1); - } - completePath(path, savefile, outputdir); - - // open file - if ( (fp = FileIO::open(path, "rb")) == NULL ) - { - printlog("error: failed to load '%s'!\n", path); - return 1; - } - - // read from file - char checkstr[64]; - fp->read(checkstr, sizeof(char), strlen("BARONYSAVEGAME")); - if ( strncmp(checkstr, "BARONYSAVEGAME", strlen("BARONYSAVEGAME")) ) - { - printlog("error: '%s' is corrupt!\n", path); - FileIO::close(fp); - return 1; - } - fp->read(checkstr, sizeof(char), strlen(VERSION)); - int versionNumber = getSavegameVersion(checkstr); - printlog("loadGameOld: '%s' version number %d", savefile, versionNumber); - if ( versionNumber == -1 ) - { - // if getSavegameVersion returned -1, abort. - printlog("error: '%s' is corrupt!\n", path); - FileIO::close(fp); - return 1; - } - - // assemble string - Uint32 hash = 0; - Uint32 loadedHash = 0; -#ifdef WINDOWS - struct _stat result; - if ( _stat(path, &result) == 0 ) - { - struct tm *tm = localtime(&result.st_mtime); - if ( tm ) - { - hash = tm->tm_hour + tm->tm_mday * tm->tm_year + tm->tm_wday + tm->tm_yday; - } - } -#else - struct stat result; - if ( stat(path, &result) == 0 ) - { - struct tm *tm = localtime(&result.st_mtime); - if ( tm ) - { - hash = tm->tm_hour + tm->tm_mday * tm->tm_year + tm->tm_wday + tm->tm_yday; - } - } -#endif // WINDOWS - - // read basic header info - fp->read(&uniqueGameKey, sizeof(Uint32), 1); - local_rng.seedBytes(&uniqueGameKey, sizeof(uniqueGameKey)); - net_rng.seedBytes(&uniqueGameKey, sizeof(uniqueGameKey)); - - Sint16 players_connected; - fp->read(&players_connected, sizeof(Sint16), 1); - Sint16 mul; - fp->read(&mul, sizeof(Sint16), 1); - - if (players_connected == 0) { - if (mul == SINGLE) { - players_connected = 1; - } else { - players_connected = - (1 << 0) | - (1 << 1) | - (1 << 2) | - (1 << 3); - } - } - switch (mul) { - default: - case SINGLE: multiplayer = SINGLE; splitscreen = false; directConnect = false; break; - case SERVER: multiplayer = SERVER; splitscreen = false; directConnect = false; break; - case CLIENT: multiplayer = CLIENT; splitscreen = false; directConnect = false; break; - case DIRECTSERVER: multiplayer = SERVER; splitscreen = false; directConnect = true; break; - case DIRECTCLIENT: multiplayer = CLIENT; splitscreen = false; directConnect = true; break; - case SERVERCROSSPLAY: multiplayer = SERVER; splitscreen = false; directConnect = false; break; // TODO! - case SPLITSCREEN: multiplayer = SINGLE; splitscreen = true; directConnect = false; break; - } - - if ( multiplayer == SINGLE ) - { - for (int c = 0; c < MAXPLAYERS; ++c) { - client_disconnected[c] = !(players_connected & (1<read(&clientnum, sizeof(Uint32), 1); - fp->read(&mapseed, sizeof(Uint32), 1); - fp->read(¤tlevel, sizeof(Uint32), 1); - if ( versionNumber >= 323 ) - { - loadedHash = (currentlevel & 0xFFFFFF00) >> 8; - currentlevel = currentlevel & 0xFF; - } - fp->read(&secretlevel, sizeof(bool), 1); - fp->read(&completionTime, sizeof(Uint32), 1); - fp->read(&conductPenniless, sizeof(bool), 1); - fp->read(&conductFoodless, sizeof(bool), 1); - fp->read(&conductVegetarian, sizeof(bool), 1); - fp->read(&conductIlliterate, sizeof(bool), 1); - if ( versionNumber >= 310 ) - { - for ( int c = 0; c < NUM_CONDUCT_CHALLENGES; ++c ) - { - fp->read(&conductGameChallenges[c], sizeof(Sint32), 1); - } - for ( int c = 0; c < NUM_GAMEPLAY_STATISTICS; ++c ) - { - fp->read(&gameStatistics[c], sizeof(Sint32), 1); - } - } - if ( versionNumber >= 335 ) - { - gameModeManager.currentSession.saveServerFlags(); - if ( multiplayer == CLIENT ) - { - fp->read(&lobbyWindowSvFlags, sizeof(Uint32), 1); - } - else - { - fp->read(&svFlags, sizeof(Uint32), 1); - } - printlog("[SESSION]: Using savegame server flags"); - } - - // load hotbar and spells list - Uint32 temp_hotbar[NUM_HOTBAR_SLOTS]; - if (splitscreen) - { - for (int c = 0; c < MAXPLAYERS; ++c) - { - if (c == player) - { - // read hotbar item offsets - for ( int i = 0; i < NUM_HOTBAR_SLOTS; i++ ) - { - fp->read(&temp_hotbar[i], sizeof(Uint32), 1); - } - - // read spells - list_FreeAll(&players[c]->magic.spellList); - Uint32 numspells = 0; - fp->read(&numspells, sizeof(Uint32), 1); - for ( int s = 0; s < numspells; ++s ) - { - int spellnum = 0; - fp->read(&spellnum, sizeof(Uint32), 1); - spell_t* spell = copySpell(getSpellFromID(spellnum)); - - node_t* node = list_AddNodeLast(&players[c]->magic.spellList); - node->element = spell; - node->deconstructor = &spellDeconstructor; - node->size = sizeof(spell); - } - - clientLearnedAlchemyRecipes[c].clear(); - if ( versionNumber >= 381 ) - { - // read alchemy recipes - Uint32 numrecipes = 0; - fp->read(&numrecipes, sizeof(Uint32), 1); - for ( int r = 0; r < numrecipes; ++r ) - { - std::pair> recipeEntry; - fp->read(&recipeEntry.first, sizeof(Sint32), 1); - fp->read(&recipeEntry.second.first, sizeof(Sint32), 1); - fp->read(&recipeEntry.second.second, sizeof(Sint32), 1); - clientLearnedAlchemyRecipes[c].push_back(recipeEntry); - } - } - - clientLearnedScrollLabels[c].clear(); - if ( versionNumber >= 382 ) - { - // read scroll labels - Uint32 numscrolls = 0; - fp->read(&numscrolls, sizeof(Uint32), 1); - for ( int s = 0; s < numscrolls; ++s ) - { - int scroll = 0; - fp->read(&scroll, sizeof(Sint32), 1); - clientLearnedScrollLabels[c].insert(scroll); - } - } - } - else - { - fp->seek(sizeof(Uint32) * NUM_HOTBAR_SLOTS, File::SeekMode::ADD); - - int numspells = 0; - fp->read(&numspells, sizeof(Uint32), 1); - for ( int i = 0; i < numspells; i++ ) - { - fp->seek(sizeof(Uint32), File::SeekMode::ADD); - } - - if ( versionNumber >= 381 ) - { - // read alchemy recipes - Uint32 numrecipes = 0; - fp->read(&numrecipes, sizeof(Uint32), 1); - for ( int r = 0; r < numrecipes; ++r ) - { - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - } - } - - if ( versionNumber >= 382 ) - { - // read scroll labels - Uint32 numscrolls = 0; - fp->read(&numscrolls, sizeof(Uint32), 1); - for ( int s = 0; s < numscrolls; ++s ) - { - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - } - } - } - } - } - else - { - // read hotbar item offsets - for ( int c = 0; c < NUM_HOTBAR_SLOTS; c++ ) - { - fp->read(&temp_hotbar[c], sizeof(Uint32), 1); - } - - // read spells - list_FreeAll(&players[player]->magic.spellList); - Uint32 numspells = 0; - fp->read(&numspells, sizeof(Uint32), 1); - for ( int c = 0; c < numspells; c++ ) - { - int spellnum = 0; - fp->read(&spellnum, sizeof(Uint32), 1); - spell_t* spell = copySpell(getSpellFromID(spellnum)); - - node_t* node = list_AddNodeLast(&players[player]->magic.spellList); - node->element = spell; - node->deconstructor = &spellDeconstructor; - node->size = sizeof(spell); - } - - clientLearnedAlchemyRecipes[player].clear(); - if ( versionNumber >= 381 ) - { - // read alchemy recipes - Uint32 numrecipes = 0; - fp->read(&numrecipes, sizeof(Uint32), 1); - for ( int r = 0; r < numrecipes; ++r ) - { - std::pair> recipeEntry; - fp->read(&recipeEntry.first, sizeof(Sint32), 1); - fp->read(&recipeEntry.second.first, sizeof(Sint32), 1); - fp->read(&recipeEntry.second.second, sizeof(Sint32), 1); - clientLearnedAlchemyRecipes[player].push_back(recipeEntry); - } - } - - clientLearnedScrollLabels[player].clear(); - if ( versionNumber >= 382 ) - { - // read scroll labels - Uint32 numscrolls = 0; - fp->read(&numscrolls, sizeof(Uint32), 1); - for ( int s = 0; s < numscrolls; ++s ) - { - int scroll = 0; - fp->read(&scroll, sizeof(Sint32), 1); - clientLearnedScrollLabels[player].insert(scroll); - } - } - } - - int monsters = NUMMONSTERS; - if ( versionNumber < 325 ) - { - monsters = 33; - } - - // skip through other player data until you get to the correct player - for ( int c = 0; c < player; c++ ) - { - fp->seek(sizeof(Uint32), File::SeekMode::ADD); - fp->seek(monsters * sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Monster), File::SeekMode::ADD); - fp->seek(sizeof(sex_t), File::SeekMode::ADD); - fp->seek(sizeof(Uint32), File::SeekMode::ADD); - fp->seek(sizeof(char) * 32, File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - - if ( versionNumber >= 323 ) - { - fp->seek( sizeof(Sint32)*NUMPROFICIENCIES, File::SeekMode::ADD); - } - else - { - fp->seek(sizeof(Sint32)*14, File::SeekMode::ADD); - } - - if ( versionNumber <= 323 ) // legacy - { - fp->seek(sizeof(bool)*32, File::SeekMode::ADD); - fp->seek(sizeof(Sint32)*32, File::SeekMode::ADD); - } - else - { - fp->seek(sizeof(bool)*NUMEFFECTS, File::SeekMode::ADD); - fp->seek(sizeof(Sint32)*NUMEFFECTS, File::SeekMode::ADD); - } - - if ( versionNumber >= 323 ) - { - fp->seek(sizeof(Sint32)*32, File::SeekMode::ADD); // stat flags - } - - if ( multiplayer == SINGLE ) - { - int numitems = 0; - fp->read(&numitems, sizeof(Uint32), 1); - for ( int i = 0; i < numitems; i++ ) - { - fp->seek(sizeof(ItemType), File::SeekMode::ADD); - fp->seek(sizeof(Status), File::SeekMode::ADD); - fp->seek(sizeof(Sint16), File::SeekMode::ADD); - fp->seek(sizeof(Sint16), File::SeekMode::ADD); - fp->seek(sizeof(Uint32), File::SeekMode::ADD); - fp->seek(sizeof(bool), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - } - fp->seek(sizeof(Uint32) * 10, File::SeekMode::ADD); // equipment slots - } - else if ( multiplayer == SERVER ) - { - if ( c == 0 ) { - // server needs to skip past its own inventory - int numitems = 0; - fp->read(&numitems, sizeof(Uint32), 1); - - for ( int i = 0; i < numitems; i++ ) - { - fp->seek(sizeof(ItemType), File::SeekMode::ADD); - fp->seek(sizeof(Status), File::SeekMode::ADD); - fp->seek(sizeof(Sint16), File::SeekMode::ADD); - fp->seek(sizeof(Sint16), File::SeekMode::ADD); - fp->seek(sizeof(Uint32), File::SeekMode::ADD); - fp->seek(sizeof(bool), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - fp->seek(sizeof(Sint32), File::SeekMode::ADD); - } - fp->seek(sizeof(Uint32) * 10, File::SeekMode::ADD); // equipment slots - } else { - // server needs to skip past other players' equipment - // (this is stored differently) - for ( int i = 0; i < 10; i++ ) - { - int itemtype = NUMITEMS; - fp->read(&itemtype, sizeof(ItemType), 1); - if ( itemtype < NUMITEMS ) - { - fp->seek(sizeof(Status), File::SeekMode::ADD); - fp->seek(sizeof(Sint16), File::SeekMode::ADD); - fp->seek(sizeof(Sint16), File::SeekMode::ADD); - fp->seek(sizeof(Uint32), File::SeekMode::ADD); - fp->seek(sizeof(bool), File::SeekMode::ADD); - } - } - } - } - else if ( multiplayer == CLIENT ) - { - // client needs only to skip the dummy byte - fp->seek(sizeof(ItemType), File::SeekMode::ADD); - } - } - - // read in player data - stats[player]->clearStats(); - fp->read(&client_classes[player], sizeof(Uint32), 1); - for ( int c = 0; c < monsters; c++ ) - { - fp->read(&kills[c], sizeof(Sint32), 1); - } - fp->read(&stats[player]->type, sizeof(Monster), 1); - fp->read(&stats[player]->sex, sizeof(sex_t), 1); - fp->read(&stats[player]->stat_appearance, sizeof(Uint32), 1); - if ( versionNumber >= 323 ) - { - stats[player]->playerRace = ((stats[player]->stat_appearance & 0xFF00) >> 8); - stats[player]->stat_appearance = (stats[player]->stat_appearance & 0xFF); - } - fp->read(&stats[player]->name, sizeof(char), 32); - fp->read(&stats[player]->HP, sizeof(Sint32), 1); - fp->read(&stats[player]->MAXHP, sizeof(Sint32), 1); - fp->read(&stats[player]->MP, sizeof(Sint32), 1); - fp->read(&stats[player]->MAXMP, sizeof(Sint32), 1); - fp->read(&stats[player]->STR, sizeof(Sint32), 1); - fp->read(&stats[player]->DEX, sizeof(Sint32), 1); - fp->read(&stats[player]->CON, sizeof(Sint32), 1); - fp->read(&stats[player]->INT, sizeof(Sint32), 1); - fp->read(&stats[player]->PER, sizeof(Sint32), 1); - fp->read(&stats[player]->CHR, sizeof(Sint32), 1); - fp->read(&stats[player]->EXP, sizeof(Sint32), 1); - fp->read(&stats[player]->LVL, sizeof(Sint32), 1); - fp->read(&stats[player]->GOLD, sizeof(Sint32), 1); - fp->read(&stats[player]->HUNGER, sizeof(Sint32), 1); - for ( int c = 0; c < NUMPROFICIENCIES; c++ ) - { - if ( versionNumber < 323 && c >= PRO_UNARMED ) - { - stats[player]->setProficiency(c, 0); - } - else - { - Sint32 val = 0; - fp->read(&val, sizeof(Sint32), 1); - stats[player]->setProficiency(c, val); - } - } - for ( int c = 0; c < NUMEFFECTS; c++ ) - { - if ( versionNumber <= 323 ) // legacy - { - if ( c < 32 ) - { - fp->read(&stats[player]->EFFECTS[c], sizeof(bool), 1); - fp->read(&stats[player]->EFFECTS_TIMERS[c], sizeof(Sint32), 1); - } - else - { - stats[player]->EFFECTS[c] = false; - stats[player]->EFFECTS_TIMERS[c] = 0; - } - } - else - { - fp->read(&stats[player]->EFFECTS[c], sizeof(bool), 1); - fp->read(&stats[player]->EFFECTS_TIMERS[c], sizeof(Sint32), 1); - } - } - if ( versionNumber >= 323 ) - { - for ( int c = 0; c < 32; c++ ) + } + else { - fp->read(&stats[player]->MISC_FLAGS[c], sizeof(Sint32), 1); - if ( c < STAT_FLAG_PLAYER_RACE ) + for ( int c = 0; c < NUM_CONDUCT_CHALLENGES; ++c ) { - stats[player]->MISC_FLAGS[c] = 0; // we don't really need these on load. + score->conductGameChallenges[c] = 0; } } - } + score->stats->leader_uid = 0; + score->stats->FOLLOWERS.first = NULL; + score->stats->FOLLOWERS.last = NULL; - if ( players[player]->isLocalPlayer() ) - { // inventory int numitems = 0; fp->read(&numitems, sizeof(Uint32), 1); - stats[player]->inventory.first = NULL; - stats[player]->inventory.last = NULL; + score->stats->inventory.first = NULL; + score->stats->inventory.last = NULL; for ( int c = 0; c < numitems; c++ ) { + if ( c >= 1000 ) + { + // break early, probably something went tremendously wrong + break; + } + ItemType type; Status status; Sint16 beatitude; @@ -3237,494 +2378,116 @@ int loadGameOld(int player, int saveIndex) fp->read(&count, sizeof(Sint16), 1); fp->read(&appearance, sizeof(Uint32), 1); fp->read(&identified, sizeof(bool), 1); - Item* item = newItem(type, status, beatitude, count, appearance, identified, &stats[player]->inventory); - fp->read(&item->x, sizeof(Sint32), 1); - fp->read(&item->y, sizeof(Sint32), 1); + newItem(type, status, beatitude, count, appearance, identified, &score->stats->inventory); } - - int c; - node_t* node; - - // equipment + score->stats->void_chest_inventory.first = NULL; + score->stats->void_chest_inventory.last = NULL; fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->helmet = (Item*)node->element; + score->stats->helmet = (Item*)node->element; } else { - stats[player]->helmet = NULL; + score->stats->helmet = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->breastplate = (Item*)node->element; + score->stats->breastplate = (Item*)node->element; } else { - stats[player]->breastplate = NULL; + score->stats->breastplate = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->gloves = (Item*)node->element; + score->stats->gloves = (Item*)node->element; } else { - stats[player]->gloves = NULL; + score->stats->gloves = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->shoes = (Item*)node->element; + score->stats->shoes = (Item*)node->element; } else { - stats[player]->shoes = NULL; + score->stats->shoes = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->shield = (Item*)node->element; + score->stats->shield = (Item*)node->element; } else { - stats[player]->shield = NULL; + score->stats->shield = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->weapon = (Item*)node->element; + score->stats->weapon = (Item*)node->element; } else { - stats[player]->weapon = NULL; + score->stats->weapon = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->cloak = (Item*)node->element; + score->stats->cloak = (Item*)node->element; } else { - stats[player]->cloak = NULL; + score->stats->cloak = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->amulet = (Item*)node->element; + score->stats->amulet = (Item*)node->element; } else { - stats[player]->amulet = NULL; + score->stats->amulet = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); + node = list_Node(&score->stats->inventory, c); if ( node ) { - stats[player]->ring = (Item*)node->element; + score->stats->ring = (Item*)node->element; } else { - stats[player]->ring = NULL; + score->stats->ring = NULL; } fp->read(&c, sizeof(Uint32), 1); - node = list_Node(&stats[player]->inventory, c); - if ( node ) - { - stats[player]->mask = (Item*)node->element; - } - else - { - stats[player]->mask = NULL; - } - } - else - { - stats[player]->inventory.first = NULL; - stats[player]->inventory.last = NULL; - stats[player]->helmet = NULL; - stats[player]->breastplate = NULL; - stats[player]->gloves = NULL; - stats[player]->shoes = NULL; - stats[player]->shield = NULL; - stats[player]->weapon = NULL; - stats[player]->cloak = NULL; - stats[player]->amulet = NULL; - stats[player]->ring = NULL; - stats[player]->mask = NULL; - - if ( multiplayer == SERVER ) - { - for ( int c = 0; c < 10; c++ ) - { - ItemType type; - Status status; - Sint16 beatitude; - Sint16 count; - Uint32 appearance; - bool identified; - - fp->read(&type, sizeof(ItemType), 1); - if ( (int)type < NUMITEMS ) - { - fp->read(&status, sizeof(Status), 1); - fp->read(&beatitude, sizeof(Sint16), 1); - fp->read(&count, sizeof(Sint16), 1); - fp->read(&appearance, sizeof(Uint32), 1); - fp->read(&identified, sizeof(bool), 1); - - Item* item = newItem(type, status, beatitude, count, appearance, identified, NULL); - - switch ( c ) - { - case 0: - stats[player]->helmet = item; - break; - case 1: - stats[player]->breastplate = item; - break; - case 2: - stats[player]->gloves = item; - break; - case 3: - stats[player]->shoes = item; - break; - case 4: - stats[player]->shield = item; - break; - case 5: - stats[player]->weapon = item; - break; - case 6: - stats[player]->cloak = item; - break; - case 7: - stats[player]->amulet = item; - break; - case 8: - stats[player]->ring = item; - break; - case 9: - stats[player]->mask = item; - break; - } - } - } - } - } - - // assign hotbar items - auto& hotbar = players[player]->hotbar.slots(); - auto& hotbar_alternate = players[player]->hotbar.slotsAlternate(); - for ( int c = 0; c < NUM_HOTBAR_SLOTS; c++ ) - { - node_t* node = list_Node(&stats[player]->inventory, temp_hotbar[c]); + node = list_Node(&score->stats->inventory, c); if ( node ) { - Item* item = (Item*)node->element; - hotbar[c].item = item->uid; + score->stats->mask = (Item*)node->element; } else { - hotbar[c].item = 0; - hotbar[c].resetLastItem(); - for ( int d = 0; d < NUM_HOTBAR_ALTERNATES; ++d ) - { - hotbar_alternate[d][c].item = 0; - hotbar_alternate[d][c].resetLastItem(); - } + score->stats->mask = NULL; } - } - - // reset some unused variables - stats[player]->monster_sound = NULL; - stats[player]->monster_idlevar = 0; - stats[player]->leader_uid = 0; - stats[player]->FOLLOWERS.first = NULL; - stats[player]->FOLLOWERS.last = NULL; - - - hash += (stats[clientnum]->STR + stats[clientnum]->LVL + stats[clientnum]->DEX * stats[clientnum]->INT); - hash += (stats[clientnum]->CON * stats[clientnum]->PER + std::min(stats[clientnum]->GOLD, 5000) - stats[clientnum]->CON); - hash += (stats[clientnum]->HP - stats[clientnum]->MP); - hash += (currentlevel); - - if ( hash != loadedHash ) - { - gameStatistics[STATISTICS_DISABLE_UPLOAD] = 1; - } - //printlog("%d, %d", hash, loadedHash); - - { - enchantedFeatherScrollsShuffled.clear(); - enchantedFeatherScrollsShuffled.reserve(enchantedFeatherScrollsFixedList.size()); - auto shuffle = enchantedFeatherScrollsFixedList; - BaronyRNG feather_rng; - feather_rng.seedBytes(&uniqueGameKey, sizeof(uniqueGameKey)); - while (!shuffle.empty()) { - int index = feather_rng.getU8() % shuffle.size(); - enchantedFeatherScrollsShuffled.push_back(shuffle[index]); - shuffle.erase(shuffle.begin() + index); - } - } - - FileIO::close(fp); - return 0; -} - -/*------------------------------------------------------------------------------- - - loadGameFollowersOld - - Loads follower data from a save game file - --------------------------------------------------------------------------------*/ - -list_t* loadGameFollowersOld(int saveIndex) -{ - File* fp; - int c; - - char savefile[PATH_MAX] = ""; - char path[PATH_MAX] = ""; - if ( multiplayer == SINGLE ) - { - strncpy(savefile, setSaveGameFileName(true, SaveFileType::FOLLOWERS, saveIndex).c_str(), PATH_MAX - 1); - } - else - { - strncpy(savefile, setSaveGameFileName(false, SaveFileType::FOLLOWERS, saveIndex).c_str(), PATH_MAX - 1); - } - completePath(path, savefile, outputdir); - - // open file - if ( (fp = FileIO::open(path, "rb")) == NULL ) - { - printlog("error: failed to load '%s'!\n", path); - return NULL; - } - - // read from file - char checkstr[64]; - fp->read(checkstr, sizeof(char), strlen("BARONYSAVEGAMEFOLLOWERS")); - if ( strncmp(checkstr, "BARONYSAVEGAMEFOLLOWERS", strlen("BARONYSAVEGAMEFOLLOWERS")) ) - { - printlog("error: '%s' is corrupt!\n", path); - FileIO::close(fp); - return NULL; - } - fp->read(checkstr, sizeof(char), strlen(VERSION)); - int versionNumber = getSavegameVersion(checkstr); - printlog("loadGameFollowersOld: '%s' version number %d", savefile, versionNumber); - if ( versionNumber == -1 ) - { - // if version number returned is invalid, abort - printlog("error: '%s' is corrupt!\n", path); - FileIO::close(fp); - return nullptr; - } - - // create followers list - list_t* followers = (list_t*) malloc(sizeof(list_t)); - followers->first = NULL; - followers->last = NULL; - - // read the follower data - for ( int c = 0; c < MAXPLAYERS; c++ ) - { - list_t* followerList = (list_t*) malloc(sizeof(list_t)); - followerList->first = NULL; - followerList->last = NULL; - node_t* node = list_AddNodeLast(followers); - node->element = followerList; - node->deconstructor = &listDeconstructor; - node->size = sizeof(list_t); - - // number of followers for this player - Uint32 numFollowers = 0; - fp->read(&numFollowers, sizeof(Uint32), 1); - - for ( int i = 0; i < numFollowers; i++ ) - { - // Stat set to 0 as monster type not needed, values will be overwritten by the saved follower data - Stat* followerStats = new Stat(0); - - node_t* node = list_AddNodeLast(followerList); - node->element = followerStats; - node->deconstructor = &statDeconstructor; - node->size = sizeof(followerStats); - - // read follower attributes - fp->read(&followerStats->type, sizeof(Monster), 1); - fp->read(&followerStats->sex, sizeof(sex_t), 1); - fp->read(&followerStats->stat_appearance, sizeof(Uint32), 1); - fp->read(&followerStats->name, sizeof(char), 32); - fp->read(&followerStats->HP, sizeof(Sint32), 1); - fp->read(&followerStats->MAXHP, sizeof(Sint32), 1); - fp->read(&followerStats->MP, sizeof(Sint32), 1); - fp->read(&followerStats->MAXMP, sizeof(Sint32), 1); - fp->read(&followerStats->STR, sizeof(Sint32), 1); - fp->read(&followerStats->DEX, sizeof(Sint32), 1); - fp->read(&followerStats->CON, sizeof(Sint32), 1); - fp->read(&followerStats->INT, sizeof(Sint32), 1); - fp->read(&followerStats->PER, sizeof(Sint32), 1); - fp->read(&followerStats->CHR, sizeof(Sint32), 1); - fp->read(&followerStats->EXP, sizeof(Sint32), 1); - fp->read(&followerStats->LVL, sizeof(Sint32), 1); - fp->read(&followerStats->GOLD, sizeof(Sint32), 1); - fp->read(&followerStats->HUNGER, sizeof(Sint32), 1); - - for ( int j = 0; j < NUMPROFICIENCIES; j++ ) - { - if ( versionNumber < 323 && j >= PRO_UNARMED ) - { - followerStats->setProficiency(j, 0); - } - else - { - Sint32 val = 0; - fp->read(&val, sizeof(Sint32), 1); - followerStats->setProficiency(j, val); - } - } - for ( int j = 0; j < NUMEFFECTS; j++ ) - { - if ( versionNumber <= 323 ) // legacy - { - if ( c < 32 ) - { - fp->read(&followerStats->EFFECTS[j], sizeof(bool), 1); - fp->read(&followerStats->EFFECTS_TIMERS[j], sizeof(Sint32), 1); - } - else - { - followerStats->EFFECTS[j] = false; - followerStats->EFFECTS_TIMERS[j] = 0; - } - } - else - { - fp->read(&followerStats->EFFECTS[j], sizeof(bool), 1); - fp->read(&followerStats->EFFECTS_TIMERS[j], sizeof(Sint32), 1); - } - } - if ( versionNumber >= 323 ) - { - for ( int j = 0; j < 32; ++j ) - { - fp->read(&followerStats->MISC_FLAGS[j], sizeof(Sint32), 1); - } - } - if ( versionNumber >= 384 ) - { - Uint32 numAttributes = 0; - fp->read(&numAttributes, sizeof(Uint32), 1); - while ( numAttributes > 0 ) - { - char key[32]; - memset(key, 0, sizeof(key)); - char value[32]; - memset(value, 0, sizeof(value)); - fp->read(&key, sizeof(char), 32); - fp->read(&value, sizeof(char), 32); - followerStats->attributes.emplace(std::make_pair(key, value)); - --numAttributes; - } - } - - /*printlog("\n\n ** FOLLOWER #%d **\n", i + 1); - printlog("Follower stats: \n"); - followerStats->printStats(); - printlog("\n\n");*/ - - // item variables - ItemType type; - Status status; - Sint16 beatitude; - Sint16 count; - Uint32 appearance; - bool identified; - - // read follower inventory - Uint32 invSize = 0; - fp->read(&invSize, sizeof(Uint32), 1); - for ( int j = 0; j < invSize; j++ ) - { - fp->read(&type, sizeof(ItemType), 1); - fp->read(&status, sizeof(Status), 1); - fp->read(&beatitude, sizeof(Sint16), 1); - fp->read(&count, sizeof(Sint16), 1); - fp->read(&appearance, sizeof(Uint32), 1); - fp->read(&identified, sizeof(bool), 1); - - Item* item = newItem(type, status, beatitude, count, appearance, identified, &followerStats->inventory); - fp->read(&item->x, sizeof(Sint32), 1); - fp->read(&item->y, sizeof(Sint32), 1); - } - - // read follower equipment - for ( int b = 0; b < 10; b++ ) - { - fp->read(&type, sizeof(ItemType), 1); - if ( (int)type < NUMITEMS ) - { - fp->read(&status, sizeof(Status), 1); - fp->read(&beatitude, sizeof(Sint16), 1); - fp->read(&count, sizeof(Sint16), 1); - fp->read(&appearance, sizeof(Uint32), 1); - fp->read(&identified, sizeof(bool), 1); - Item* item = newItem(type, status, beatitude, count, appearance, identified, NULL); - - switch ( b ) - { - case 0: - followerStats->helmet = item; - break; - case 1: - followerStats->breastplate = item; - break; - case 2: - followerStats->gloves = item; - break; - case 3: - followerStats->shoes = item; - break; - case 4: - followerStats->shield = item; - break; - case 5: - followerStats->weapon = item; - break; - case 6: - followerStats->cloak = item; - break; - case 7: - followerStats->amulet = item; - break; - case 8: - followerStats->ring = item; - break; - case 9: - followerStats->mask = item; - break; - } - } - } - } + score->stats->monster_sound = NULL; + score->stats->monster_idlevar = 0; } FileIO::close(fp); - return followers; } /*------------------------------------------------------------------------------- @@ -4061,11 +2824,15 @@ void updatePlayerConductsInMainLoop() } if ( !conductGameChallenges[CONDUCT_CLASSIC_MODE] ) { - if ( (svFlags & SV_FLAG_CLASSIC) ) + if ( (svFlags & SV_FLAG_CLASSIC) && currentlevel < 25 ) { conductGameChallenges[CONDUCT_CLASSIC_MODE] = 1; } } + else if ( currentlevel >= 25 ) + { + conductGameChallenges[CONDUCT_CLASSIC_MODE] = 0; + } if ( !conductGameChallenges[CONDUCT_MODDED] ) { if ( Mods::numCurrentModsLoaded >= 0 ) @@ -4088,6 +2855,13 @@ void updatePlayerConductsInMainLoop() achievementObserver.updateClientBounties(false); + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( achievementObserver.playerAchievements[i].hellsKitchen >= 0 ) + { + achievementObserver.playerAchievements[i].hellsKitchen = 0; + } + } achievementObserver.achievementTimersTickDown(); } @@ -4126,13 +2900,45 @@ void updateGameplayStatisticsInMainLoop() { steamAchievement("BARONY_ACH_OHAI_MARK"); } - if ( gameStatistics[STATISTICS_PIMPING_AINT_EASY] >= 6 ) + if ( gameStatistics[STATISTICS_PIMPING_AINT_EASY] >= 6 ) + { + steamAchievement("BARONY_ACH_PIMPIN"); + } + if ( gameStatistics[STATISTICS_TRIBE_SUBSCRIBE] >= 4 ) + { + steamAchievement("BARONY_ACH_TRIBE_SUBSCRIBE"); + } + if ( gameStatistics[STATISTICS_SKID_ROW] >= 50 ) + { + steamAchievement("BARONY_ACH_SKID_ROW"); + } + if ( gameStatistics[STATISTICS_WRECKING_CREW] >= 20 ) + { + steamAchievement("BARONY_ACH_WRECKING_CREW"); + } + if ( gameStatistics[STATISTICS_EAT_ME] >= 50 ) + { + steamAchievement("BARONY_ACH_EAT_ME"); + } + if ( gameStatistics[STATISTICS_BONK] >= 20 ) + { + steamAchievement("BARONY_ACH_BONK"); + } + if ( gameStatistics[STATISTICS_RIGHTEOUS_FURY] >= 50 ) + { + steamAchievement("BARONY_ACH_RIGHTEOUS_FURY"); + } + if ( gameStatistics[STATISTICS_BARDIC_INSPIRATION] >= 10 ) + { + steamAchievement("BARONY_ACH_BARDIC_INSPIRATION"); + } + if ( gameStatistics[STATISTICS_PARRY_TANK] >= 20 ) { - steamAchievement("BARONY_ACH_PIMPIN"); + steamAchievement("BARONY_ACH_PARRY_TANK"); } - if ( gameStatistics[STATISTICS_TRIBE_SUBSCRIBE] >= 4 ) + if ( gameStatistics[STATISTICS_THATS_CHEATING] >= 10 ) { - steamAchievement("BARONY_ACH_TRIBE_SUBSCRIBE"); + steamAchievement("BARONY_ACH_THATS_CHEATING"); } if ( gameStatistics[STATISTICS_FORUM_TROLL] > 0 ) { @@ -4195,22 +3001,48 @@ void updateGameplayStatisticsInMainLoop() } } - if ( (ticks % (TICKS_PER_SECOND * 8) == 0) && (gameStatistics[STATISTICS_POP_QUIZ_1] != 0 || gameStatistics[STATISTICS_POP_QUIZ_2] != 0) ) + if ( (ticks % (TICKS_PER_SECOND * 8) == 0) ) { - int numSpellsCast = 0; - int stat1 = gameStatistics[STATISTICS_POP_QUIZ_1]; - int stat2 = gameStatistics[STATISTICS_POP_QUIZ_1]; - for ( int i = 0; i < 30; ++i ) + if ( stats[clientnum]->getProficiency(PRO_SORCERY) >= SKILL_LEVEL_LEGENDARY + && stats[clientnum]->getProficiency(PRO_MYSTICISM) >= SKILL_LEVEL_LEGENDARY + && stats[clientnum]->getProficiency(PRO_THAUMATURGY) >= SKILL_LEVEL_LEGENDARY ) { - // count the bits set. - numSpellsCast += (stat1 & 1); - numSpellsCast += (stat2 & 1); - stat1 = stat1 >> 1; - stat2 = stat2 >> 1; + steamAchievement("BARONY_ACH_MASTER_MAGIC"); + } + + if ( gameStatistics[STATISTICS_FLAVORTOWN] != 0 ) + { + int numflavors = 0; + for ( int i = 0; i < 6; ++i ) + { + if ( gameStatistics[STATISTICS_FLAVORTOWN] & (1 << i) ) + { + ++numflavors; + } + } + if ( numflavors >= 6 ) + { + steamAchievement("BARONY_ACH_FLAVORTOWN"); + } } - if ( numSpellsCast >= 20 ) + + if ( (gameStatistics[STATISTICS_POP_QUIZ_1] != 0 || gameStatistics[STATISTICS_POP_QUIZ_2] != 0) ) { - steamAchievement("BARONY_ACH_POP_QUIZ"); + int numSpellsCast = 0; + int stat1 = gameStatistics[STATISTICS_POP_QUIZ_1]; + int stat2 = gameStatistics[STATISTICS_POP_QUIZ_2]; + for ( int i = 0; i < 30; ++i ) + { + // count the bits set. + numSpellsCast += (stat1 & 1); + numSpellsCast += (stat2 & 1); + stat1 = stat1 >> 1; + stat2 = stat2 >> 1; + } + if ( numSpellsCast >= 20 ) + { + steamAchievement("BARONY_ACH_POP_QUIZ"); + } } } @@ -4262,6 +3094,24 @@ void updateGameplayStatisticsInMainLoop() } } + if ( multiplayer != CLIENT && ticks % (TICKS_PER_SECOND / 2) == 0 ) + { + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + if ( achievementObserver.playerAchievements[i].parryTank > 0 ) + { + serverUpdatePlayerGameplayStats(i, STATISTICS_PARRY_TANK, achievementObserver.playerAchievements[i].parryTank); + achievementObserver.playerAchievements[i].parryTank = 0; + } + else if ( achievementObserver.playerAchievements[i].parryTank < 0 ) + { + serverUpdatePlayerGameplayStats(i, STATISTICS_PARRY_TANK, 0); + achievementObserver.playerAchievements[i].parryTank = 0; + } + } + } + + if ( ticks % (TICKS_PER_SECOND * 5) == 0 ) { std::unordered_set potionList; @@ -4305,11 +3155,12 @@ void updateGameplayStatisticsInMainLoop() } else if ( client_classes[clientnum] == CLASS_HUNTER && isRangedWeapon(*item) ) { - if ( item->type == CROSSBOW || item->type == HEAVY_CROSSBOW ) + if ( item->type == CROSSBOW || item->type == HEAVY_CROSSBOW || item->type == BLACKIRON_CROSSBOW ) { bowList.insert(CROSSBOW); } - else if ( item->type == SHORTBOW || item->type == LONGBOW || item->type == COMPOUND_BOW ) + else if ( item->type == SHORTBOW || item->type == LONGBOW || item->type == COMPOUND_BOW + || item->type == BRANCH_BOW || item->type == BRANCH_BOW_INFECTED || item->type == BONE_SHORTBOW ) { bowList.insert(SHORTBOW); } @@ -4361,6 +3212,31 @@ void updateGameplayStatisticsInMainLoop() steamStatisticUpdateClient(i, STEAM_STAT_TRASH_COMPACTOR, STEAM_STAT_INT, achievementObserver.playerAchievements[i].trashCompactor); achievementObserver.playerAchievements[i].trashCompactor = 0; } + if ( achievementObserver.playerAchievements[i].sourceEngine > 0 ) + { + steamStatisticUpdateClient(i, STEAM_STAT_SOURCE_ENGINE, STEAM_STAT_INT, achievementObserver.playerAchievements[i].sourceEngine); + achievementObserver.playerAchievements[i].sourceEngine = 0; + } + if ( achievementObserver.playerAchievements[i].skidRow > 0 ) + { + serverUpdatePlayerGameplayStats(i, STATISTICS_SKID_ROW, achievementObserver.playerAchievements[i].skidRow); + achievementObserver.playerAchievements[i].skidRow = 0; + } + if ( achievementObserver.playerAchievements[i].bonk > 0 ) + { + serverUpdatePlayerGameplayStats(i, STATISTICS_BONK, achievementObserver.playerAchievements[i].bonk); + achievementObserver.playerAchievements[i].bonk = 0; + } + if ( achievementObserver.playerAchievements[i].righteousFury > 0 ) + { + serverUpdatePlayerGameplayStats(i, STATISTICS_RIGHTEOUS_FURY, achievementObserver.playerAchievements[i].righteousFury); + achievementObserver.playerAchievements[i].righteousFury = 0; + } + if ( achievementObserver.playerAchievements[i].eatMe > 0 ) + { + serverUpdatePlayerGameplayStats(i, STATISTICS_EAT_ME, achievementObserver.playerAchievements[i].eatMe); + achievementObserver.playerAchievements[i].eatMe = 0; + } } } } @@ -5235,6 +4111,8 @@ bool AchievementObserver::updateOnLevelChange() playerAchievements[i].updatedBountyTargets = false; playerAchievements[i].wearingBountyHat = false; playerAchievements[i].totalKillsTickUpdate = false; + playerAchievements[i].manifestDestinyChests.clear(); + playerAchievements[i].manifestDestinyChestSequence = 0; } levelObserved = currentlevel; return true; @@ -5244,6 +4122,7 @@ bool AchievementObserver::updateOnLevelChange() int AchievementObserver::checkUidIsFromPlayer(Uint32 uid) { + if ( uid == 0 ) { return -1; } for ( int i = 0; i < MAXPLAYERS; ++i ) { if ( achievementObserver.playerUids[i] == uid ) @@ -5399,6 +4278,39 @@ void AchievementObserver::updateData() } } + + if ( multiplayer != CLIENT ) + { + std::vector chestsOnLevel; + for ( node_t* node = map.entities->first; node; node = node->next ) + { + Entity* entity = (Entity*)node->element; + if ( entity && (entity->behavior == &actChest || (entity->behavior == &actMonster && entity->getStats() && entity->getStats()->type == MIMIC) ) ) + { + chestsOnLevel.push_back(entity->getUID()); + } + } + if ( chestsOnLevel.size() ) + { + BaronyRNG chestSeed; + chestSeed.seedBytes(&mapseed, sizeof(mapseed)); + + std::vector chances(chestsOnLevel.size()); + for ( int i = 0; i < MAXPLAYERS; ++i ) + { + playerAchievements[i].manifestDestinyChests.clear(); + std::fill(chances.begin(), chances.end(), 1); + while ( playerAchievements[i].manifestDestinyChests.size() < chestsOnLevel.size() ) + { + int pick = chestSeed.discrete(chances.data(), chances.size()); + playerAchievements[i].manifestDestinyChests.push_back(chestsOnLevel[pick]); + chances[pick] = 0; + } + playerAchievements[i].manifestDestinyChestSequence = chestSeed.rand() % playerAchievements[i].manifestDestinyChests.size(); + } + } + } + BaronyRNG bountySeed; bountySeed.seedBytes(&mapseed, sizeof(mapseed)); if ( multiplayer != CLIENT ) @@ -5561,6 +4473,13 @@ void AchievementObserver::awardAchievementIfActive(int player, Entity* entity, i awardAchievement(player, achievement); } } + else if ( achievement == BARONY_ACH_FOOD_FIGHT ) + { + if ( (*it).second[achievement].second >= 4 ) + { + awardAchievement(player, achievement); + } + } else if ( achievement == BARONY_ACH_OHAI_MARK ) { serverUpdatePlayerGameplayStats(player, STATISTICS_OHAI_MARK, 1); @@ -5573,6 +4492,10 @@ void AchievementObserver::awardAchievementIfActive(int player, Entity* entity, i { steamStatisticUpdateClient(player, STEAM_STAT_COWBOY_FROM_HELL, STEAM_STAT_INT, 1); } + else if ( achievement == BARONY_ACH_THATS_A_WRAP ) + { + steamStatisticUpdateClient(player, STEAM_STAT_THATS_A_WRAP, STEAM_STAT_INT, 1); + } else { awardAchievement(player, achievement); @@ -5617,7 +4540,14 @@ std::set AchievementObserver::PlayerAchievements::startingClassItems = PUNISHER_HOOD, MASK_SHAMAN, TOOL_BLINDFOLD_TELEPATHY, - TOOL_BLINDFOLD + TOOL_BLINDFOLD, + MASK_EYEPATCH, + HOOD_TEAL, + MASK_PIPE, + HAT_PLUMED_CAP, + CHAIN_COIF + /*, + HAT_CIRCLET_WISDOM*/ }; int AchievementObserver::PlayerAchievements::getItemIndexForDapperAchievement(Item* item) @@ -5870,6 +4800,7 @@ void AchievementObserver::updatePlayerAchievement(int player, Achievement achiev case BARONY_ACH_COOP_ESCAPE_MINES: { std::unordered_set races; + std::unordered_set short_races; std::unordered_set classes; std::vector awardAchievementsToAllPlayers; int num = 0; @@ -5880,8 +4811,16 @@ void AchievementObserver::updatePlayerAchievement(int player, Achievement achiev if ( stats[i] && stats[i]->playerRace != RACE_HUMAN && stats[i]->stat_appearance == 0 ) { races.insert(stats[i]->playerRace); + + if ( stats[i]->playerRace == RACE_GNOME + || stats[i]->playerRace == RACE_GREMLIN + || (stats[i]->playerRace == RACE_DRYAD && stats[i]->sex == FEMALE) + || (stats[i]->playerRace == RACE_MYCONID && stats[i]->sex == MALE) ) + { + short_races.insert(stats[i]->playerRace); + } } - if ( client_classes[i] > CLASS_MONK ) + //if ( client_classes[i] > CLASS_MONK ) { classes.insert(client_classes[i]); } @@ -5910,6 +4849,33 @@ void AchievementObserver::updatePlayerAchievement(int player, Achievement achiev { awardAchievementsToAllPlayers.push_back(BARONY_ACH_TRIBAL); } + + std::set expansions; + for ( auto race : races ) + { + if ( race >= RACE_SKELETON && race <= RACE_GOATMAN ) + { + expansions.insert(1); + } + if ( race >= RACE_AUTOMATON && race <= RACE_INSECTOID ) + { + expansions.insert(2); + } + if ( race >= RACE_GNOME && race <= RACE_SALAMANDER ) + { + expansions.insert(3); + } + } + + if ( expansions.size() == 3 ) + { + awardAchievementsToAllPlayers.push_back(BARONY_ACH_FOREIGN_EXCHANGE); + } + + if ( races.size() >= 3 && short_races.size() == races.size() ) + { + awardAchievementsToAllPlayers.push_back(BARONY_ACH_SHORT_SHORTS); + } } if ( !classes.empty() ) @@ -5930,6 +4896,62 @@ void AchievementObserver::updatePlayerAchievement(int player, Achievement achiev { awardAchievementsToAllPlayers.push_back(BARONY_ACH_SURVIVALISTS); } + + if ( classes.find(CLASS_SCION) != classes.end() + && (classes.find(CLASS_WIZARD) != classes.end() + || classes.find(CLASS_HEALER) != classes.end() + || classes.find(CLASS_ARCANIST) != classes.end()) ) + { + awardAchievementsToAllPlayers.push_back(BARONY_ACH_APPRENTICES); + } + if ( classes.find(CLASS_PALADIN) != classes.end() + && (classes.find(CLASS_MONK) != classes.end() + || classes.find(CLASS_SEXTON) != classes.end() + || classes.find(CLASS_CLERIC) != classes.end()) ) + { + awardAchievementsToAllPlayers.push_back(BARONY_ACH_HOLY_ORDER); + } + if ( classes.find(CLASS_SAPPER) != classes.end() + && (classes.find(CLASS_WARRIOR) != classes.end() + || classes.find(CLASS_BARBARIAN) != classes.end()) ) + { + awardAchievementsToAllPlayers.push_back(BARONY_ACH_CONSCRIPTED); + } + if ( classes.find(CLASS_BARD) != classes.end() + && (classes.find(CLASS_ROGUE) != classes.end() + || classes.find(CLASS_MERCHANT) != classes.end() + || classes.find(CLASS_JOKER) != classes.end()) ) + { + awardAchievementsToAllPlayers.push_back(BARONY_ACH_RIZZLERS); + } + if ( classes.find(CLASS_HERMIT) != classes.end() + && (classes.find(CLASS_WANDERER) != classes.end() + || classes.find(CLASS_NINJA) != classes.end()) ) + { + awardAchievementsToAllPlayers.push_back(BARONY_ACH_LONER_LEAGUE); + } + + std::set expansions; + for ( auto classnum : classes ) + { + if ( classnum >= CLASS_CONJURER && classnum <= CLASS_BREWER ) + { + expansions.insert(1); + } + if ( classnum >= CLASS_MACHINIST && classnum <= CLASS_HUNTER ) + { + expansions.insert(2); + } + if ( classnum >= CLASS_BARD && classnum <= CLASS_PALADIN ) + { + expansions.insert(3); + } + } + + if ( expansions.size() == 3 ) + { + awardAchievementsToAllPlayers.push_back(BARONY_ACH_STUDY_ABROAD); + } } if ( !awardAchievementsToAllPlayers.empty() ) { @@ -5979,6 +5001,13 @@ void AchievementObserver::clearPlayerAchievementData() playerAchievements[i].socialButterfly = 0; playerAchievements[i].rollTheBones = 0; playerAchievements[i].trashCompactor = 0; + playerAchievements[i].skidRow = 0; + playerAchievements[i].bonk = 0; + playerAchievements[i].righteousFury = 0; + playerAchievements[i].hellsKitchen = 0; + playerAchievements[i].eatMe = 0; + playerAchievements[i].sourceEngine = 0; + playerAchievements[i].parryTank = 0; playerAchievements[i].realBoy = std::make_pair(0, 0); playerAchievements[i].caughtInAMoshTargets.clear(); @@ -6067,6 +5096,36 @@ void AchievementObserver::awardAchievement(int player, int achievement) case BARONY_ACH_BY_THE_BOOK: steamAchievementClient(player, "BARONY_ACH_BY_THE_BOOK"); break; + case BARONY_ACH_THATS_A_WRAP: + steamAchievementClient(player, "BARONY_ACH_THATS_A_WRAP"); + break; + case BARONY_ACH_APPRENTICES: + steamAchievementClient(player, "BARONY_ACH_APPRENTICES"); + break; + case BARONY_ACH_SHORT_SHORTS: + steamAchievementClient(player, "BARONY_ACH_SHORT_SHORTS"); + break; + case BARONY_ACH_HOLY_ORDER: + steamAchievementClient(player, "BARONY_ACH_HOLY_ORDER"); + break; + case BARONY_ACH_CONSCRIPTED: + steamAchievementClient(player, "BARONY_ACH_CONSCRIPTED"); + break; + case BARONY_ACH_LONER_LEAGUE: + steamAchievementClient(player, "BARONY_ACH_LONER_LEAGUE"); + break; + case BARONY_ACH_RIZZLERS: + steamAchievementClient(player, "BARONY_ACH_RIZZLERS"); + break; + case BARONY_ACH_FOREIGN_EXCHANGE: + steamAchievementClient(player, "BARONY_ACH_FOREIGN_EXCHANGE"); + break; + case BARONY_ACH_STUDY_ABROAD: + steamAchievementClient(player, "BARONY_ACH_STUDY_ABROAD"); + break; + case BARONY_ACH_FOOD_FIGHT: + steamAchievementClient(player, "BARONY_ACH_FOOD_FIGHT"); + break; default: messagePlayer(player, MESSAGE_DEBUG, "[WARNING]: Unhandled achievement: %d", achievement); break; @@ -6411,6 +5470,10 @@ void SaveGameInfo::computeHash(const int playernum, Uint32& hash) { item.computeHash(hash, shift); } + for ( auto& item : stats->void_chest_inventory ) + { + item.computeHash(hash, shift); + } for ( auto& bag : stats->player_lootbags ) { hash += (Uint32)((Uint32)bag.first << (shift % 32)); ++shift; @@ -6430,6 +5493,52 @@ void SaveGameInfo::computeHash(const int playernum, Uint32& hash) hash += djb2Hash(const_cast(pair.first.c_str())); hash += djb2Hash(const_cast(pair.second.c_str())); } + + for ( auto& val : players[playernum].itemDegradeRNG ) + { + hash += (Uint32)((Uint32)val.first << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)val.second << (shift % 32)); ++shift; + } + for ( auto& val : players[playernum].escalatingRngRolls ) + { + hash += (Uint32)((Uint32)val.first << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)val.second << (shift % 32)); ++shift; + } + for ( auto& val : players[playernum].escalatingSpellRngRolls ) + { + hash += (Uint32)((Uint32)val.first << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)val.second << (shift % 32)); ++shift; + } + for ( auto& val : players[playernum].appraisal_item_progress ) + { + hash += (Uint32)((Uint32)val.first << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)val.second << (shift % 32)); ++shift; + } + for ( auto& val : players[playernum].learnedSpells ) + { + hash += (Uint32)((Uint32)val << (shift % 32)); ++shift; + } + for ( auto& val : players[playernum].sustainedSpellIDCounter ) + { + hash += (Uint32)((Uint32)val.first << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)val.second << (shift % 32)); ++shift; + } + for ( auto& val : players[playernum].ducksInARow ) + { + hash += (Uint32)((Uint32)val.first << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)val.second << (shift % 32)); ++shift; + } + for ( auto& val : players[playernum].favoriteBooksAchievement ) + { + hash += (Uint32)((Uint32)val.first << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)val.second << (shift % 32)); ++shift; + } + hash += (Uint32)((Uint32)players[playernum].sustainedSpellMPUsedSorcery << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)players[playernum].sustainedSpellMPUsedMysticism << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)players[playernum].sustainedSpellMPUsedThaumaturgy << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)players[playernum].baseSpellMPUsedSorcery << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)players[playernum].baseSpellMPUsedMysticism << (shift % 32)); ++shift; + hash += (Uint32)((Uint32)players[playernum].baseSpellMPUsedThaumaturgy << (shift % 32)); ++shift; } void SaveGameInfo::Player::stat_t::item_t::computeHash(Uint32& hash, Uint32& shift) @@ -6508,7 +5617,7 @@ int SaveGameInfo::populateFromSession(const int playernum) info->players_connected.resize(MAXPLAYERS); info->players.resize(MAXPLAYERS); for ( int c = 0; c < MAXPLAYERS; ++c ) { - info->players_connected[c] = client_disconnected[c] ? 0 : 1; + info->players_connected[c] = client_disconnected[c] && !::players[c]->was_connected_to_game ? 0 : 1; if ( info->players_connected[c] ) { auto& player = info->players[c]; player.char_class = client_classes[c]; @@ -6648,9 +5757,11 @@ int SaveGameInfo::populateFromSession(const int playernum) } player.stats.EFFECTS.resize(NUMEFFECTS); player.stats.EFFECTS_TIMERS.resize(NUMEFFECTS); + player.stats.EFFECTS_ACCRETION_TIME.resize(NUMEFFECTS); for ( int i = 0; i < NUMEFFECTS; ++i ) { - player.stats.EFFECTS[i] = stats[c]->EFFECTS[i]; + player.stats.EFFECTS[i] = stats[c]->getEffectActive(i); player.stats.EFFECTS_TIMERS[i] = stats[c]->EFFECTS_TIMERS[i]; + player.stats.EFFECTS_ACCRETION_TIME[i] = stats[c]->EFFECTS_ACCRETION_TIME[i]; } constexpr int NUMMISCFLAGS = sizeof(Stat::MISC_FLAGS) / sizeof(Stat::MISC_FLAGS[0]); player.stats.MISC_FLAGS.resize(NUMMISCFLAGS); @@ -6676,7 +5787,37 @@ int SaveGameInfo::populateFromSession(const int playernum) { player.itemDegradeRNG.push_back(pair); } - player.sustainedSpellMPUsed = ::players[c]->mechanics.sustainedSpellMPUsed; + for ( auto learnedSpell : ::players[c]->mechanics.learnedSpells ) + { + player.learnedSpells.push_back(learnedSpell); + } + for ( auto& pair : ::players[c]->mechanics.sustainedSpellIDCounter ) + { + player.sustainedSpellIDCounter.push_back(pair); + } + for ( auto& pair : ::players[c]->mechanics.ducksInARow ) + { + player.ducksInARow.push_back(pair); + } + for ( auto& pair : ::players[c]->mechanics.favoriteBooksAchievement ) + { + player.favoriteBooksAchievement.push_back(pair); + } + for ( auto& pair : ::players[c]->mechanics.escalatingRngRolls ) + { + player.escalatingRngRolls.push_back(pair); + } + for ( auto& pair : ::players[c]->mechanics.escalatingSpellRngRolls ) + { + player.escalatingSpellRngRolls.push_back(pair); + } + player.sustainedSpellMPUsedSorcery = ::players[c]->mechanics.sustainedSpellMPUsedSorcery; + player.sustainedSpellMPUsedMysticism = ::players[c]->mechanics.sustainedSpellMPUsedMysticism; + player.sustainedSpellMPUsedThaumaturgy = ::players[c]->mechanics.sustainedSpellMPUsedThaumaturgy; + + player.baseSpellMPUsedSorcery = ::players[c]->mechanics.baseSpellMPUsedSorcery; + player.baseSpellMPUsedMysticism = ::players[c]->mechanics.baseSpellMPUsedMysticism; + player.baseSpellMPUsedThaumaturgy = ::players[c]->mechanics.baseSpellMPUsedThaumaturgy; for ( auto& pair : ::players[c]->compendiumProgress.itemEvents ) { @@ -6759,6 +5900,12 @@ int SaveGameInfo::populateFromSession(const int playernum) for ( node_t* node = stats[c]->inventory.first; node != nullptr; node = node->next ) { auto item = (Item*)node->element; + if ( ::players[c]->inventoryUI.appraisal.appraisalProgressionItems.find(item->uid) + != ::players[c]->inventoryUI.appraisal.appraisalProgressionItems.end() ) + { + player.appraisal_item_progress.push_back(std::make_pair(player.stats.inventory.size(), + ::players[c]->inventoryUI.appraisal.appraisalProgressionItems[item->uid])); + } player.stats.inventory.push_back( SaveGameInfo::Player::stat_t::item_t{ (Uint32)item->type, @@ -6772,6 +5919,23 @@ int SaveGameInfo::populateFromSession(const int playernum) }); } + // void chest inventory + for ( node_t* node = stats[c]->void_chest_inventory.first; + node != nullptr; node = node->next ) { + auto item = (Item*)node->element; + player.stats.void_chest_inventory.push_back( + SaveGameInfo::Player::stat_t::item_t{ + (Uint32)item->type, + (Uint32)item->status, + item->appearance, + item->beatitude, + item->count, + item->identified, + item->x, + item->y, + }); + } + // followers for ( node_t* node = stats[c]->FOLLOWERS.first; node != nullptr; node = node->next ) { @@ -6803,9 +5967,11 @@ int SaveGameInfo::populateFromSession(const int playernum) } stats.EFFECTS.resize(NUMEFFECTS); stats.EFFECTS_TIMERS.resize(NUMEFFECTS); + stats.EFFECTS_ACCRETION_TIME.resize(NUMEFFECTS); for ( int i = 0; i < NUMEFFECTS; ++i ) { - stats.EFFECTS[i] = follower->EFFECTS[i]; + stats.EFFECTS[i] = follower->getEffectActive(i); stats.EFFECTS_TIMERS[i] = follower->EFFECTS_TIMERS[i]; + stats.EFFECTS_ACCRETION_TIME[i] = follower->EFFECTS_ACCRETION_TIME[i]; } stats.MISC_FLAGS.resize(NUMMISCFLAGS); for ( int i = 0; i < NUMMISCFLAGS; ++i ) { @@ -6901,7 +6067,7 @@ int saveGame(int saveIndex) { char path[PATH_MAX] = ""; std::string savefile = setSaveGameFileName(multiplayer == SINGLE, SaveFileType::JSON, saveIndex); completePath(path, savefile.c_str(), outputdir); - auto result = FileHelper::writeObject(path, *cvar_saveText ? EFileFormat::Json : EFileFormat::Binary, info); + auto result = FileHelper::writeObject(path, *cvar_saveText ? EFileFormat::Json_Compact : EFileFormat::Binary, info); return result == true ? 0 : 1; } @@ -6913,7 +6079,7 @@ int SaveGameInfo::getTotalScore(const int playernum, const int victory) for ( auto& item : stats->inventory ) { - amount += items[item.type].value; + amount += items[item.type].gold_value; } amount += stats->GOLD; amount += stats->EXP; @@ -6927,7 +6093,18 @@ int SaveGameInfo::getTotalScore(const int playernum, const int victory) { if ( c != HUMAN ) { - amount += player.kills[c] * 100; + if ( c == DEVIL || c == LICH_ICE || c == LICH_FIRE ) + { + amount += std::min(1, player.kills[c]) * 100; + } + else if ( c == LICH ) + { + amount += std::min(3, player.kills[c]) * 100; + } + else + { + amount += player.kills[c] * 100; + } } else { @@ -7109,6 +6286,16 @@ std::string SaveGameInfo::serializeToOnlineHiscore(const int playernum, const in } attrObj.AddMember("statistics", statisticsArr, d.GetAllocator()); + rapidjson::Value effectsObj(rapidjson::kObjectType); + for ( int i = 0; i < NUMEFFECTS; ++i ) + { + if ( myStats.EFFECTS[i] > 0 ) + { + effectsObj.AddMember(rapidjson::Value(std::to_string(i).c_str(), d.GetAllocator()), rapidjson::Value(myStats.EFFECTS[i]), d.GetAllocator()); + } + } + attrObj.AddMember("effects", effectsObj, d.GetAllocator()); + character.AddMember("attributes", attrObj, d.GetAllocator()); } @@ -7342,14 +6529,29 @@ int loadGame(int player, const SaveGameInfo& info) { for (int c = 0; c < NUMEFFECTS; ++c) { if ( c < p.EFFECTS.size() ) { - stats[statsPlayer]->EFFECTS[c] = p.EFFECTS[c]; - stats[statsPlayer]->EFFECTS_TIMERS[c] = p.EFFECTS_TIMERS[c]; + stats[statsPlayer]->setEffectValueUnsafe(c, (Uint8)p.EFFECTS[c]); + if ( c < p.EFFECTS_TIMERS.size() ) + { + stats[statsPlayer]->EFFECTS_TIMERS[c] = p.EFFECTS_TIMERS[c]; + } + else + { + stats[statsPlayer]->EFFECTS_TIMERS[c] = 0; + } } else { - stats[statsPlayer]->EFFECTS[c] = 0; + stats[statsPlayer]->clearEffect(c); stats[statsPlayer]->EFFECTS_TIMERS[c] = 0; } + if ( c < p.EFFECTS_ACCRETION_TIME.size() ) + { + stats[statsPlayer]->EFFECTS_ACCRETION_TIME[c] = p.EFFECTS_ACCRETION_TIME[c]; + } + else + { + stats[statsPlayer]->EFFECTS_ACCRETION_TIME[c] = 0; + } } constexpr int NUMMISCFLAGS = sizeof(Stat::MISC_FLAGS) / sizeof(Stat::MISC_FLAGS[0]); for (int c = 0; c < NUMMISCFLAGS && c < p.MISC_FLAGS.size(); ++c) { @@ -7378,8 +6580,21 @@ int loadGame(int player, const SaveGameInfo& info) { } } + players[statsPlayer]->inventoryUI.appraisal.appraisalProgressionItems.clear(); + bool checkAppraisalProgress = players[statsPlayer]->isLocalPlayer(); + std::map appraisalMap; + if ( checkAppraisalProgress ) + { + for ( auto& pair : info.players[player].appraisal_item_progress ) + { + appraisalMap[pair.first] = pair.second; + } + } + // inventory + int inventory_index = -1; for (auto& item : p.inventory) { + ++inventory_index; ItemType type = static_cast(item.type); Status status = static_cast(item.status); Sint16 beatitude = item.beatitude; @@ -7390,6 +6605,25 @@ int loadGame(int player, const SaveGameInfo& info) { appearance, identified, &stats[statsPlayer]->inventory); i->x = item.x; i->y = item.y; + + if ( appraisalMap.find(inventory_index) != appraisalMap.end() ) + { + players[statsPlayer]->inventoryUI.appraisal.appraisalProgressionItems[i->uid] = appraisalMap[inventory_index]; + } + } + + // void chest inventory + for ( auto& item : p.void_chest_inventory ) { + ItemType type = static_cast(item.type); + Status status = static_cast(item.status); + Sint16 beatitude = item.beatitude; + Sint16 count = item.count; + Uint32 appearance = item.appearance; + bool identified = item.identified; + Item* i = newItem(type, status, beatitude, count, + appearance, identified, &stats[statsPlayer]->void_chest_inventory); + i->x = item.x; + i->y = item.y; } // equipment @@ -7408,6 +6642,7 @@ int loadGame(int player, const SaveGameInfo& info) { if (players[statsPlayer]->isLocalPlayer()) { // if this is a local player, we have their inventory, and can // restore equipment using item indexes in the player_equipment table + for (auto& item : p.player_equipment) { auto find = slots.find(item.first); if (find != slots.end()) { @@ -7497,6 +6732,7 @@ int loadGame(int player, const SaveGameInfo& info) { // generate mimics { mimic_generator.init(); + treasure_room_generator.init(); } // shopkeeper hostility @@ -7557,12 +6793,54 @@ int loadGame(int player, const SaveGameInfo& info) { { auto& mechanics = players[statsPlayer]->mechanics; mechanics.itemDegradeRng.clear(); + mechanics.learnedSpells.clear(); + mechanics.ducksInARow.clear(); + mechanics.favoriteBooksAchievement.clear(); + mechanics.sustainedSpellIDCounter.clear(); + hamletShopkeeperSkillLimit[statsPlayer].clear(); + mechanics.baseSpellLevelUpProcs.clear(); + mechanics.escalatingRngRolls.clear(); + mechanics.escalatingSpellRngRolls.clear(); for ( auto& pair : info.players[player].itemDegradeRNG ) { mechanics.itemDegradeRng[pair.first] = pair.second; } - mechanics.sustainedSpellMPUsed = 0; - mechanics.sustainedSpellMPUsed = info.players[player].sustainedSpellMPUsed; + for ( auto learnedSpell : info.players[player].learnedSpells ) + { + mechanics.learnedSpells.insert(learnedSpell); + } + for ( auto& duck : info.players[player].ducksInARow ) + { + mechanics.ducksInARow.push_back(duck); + } + for ( auto& pair : info.players[player].favoriteBooksAchievement ) + { + mechanics.favoriteBooksAchievement[pair.first] = pair.second; + } + for ( auto& pair : info.players[player].sustainedSpellIDCounter ) + { + mechanics.sustainedSpellIDCounter[pair.first] = pair.second; + } + for ( auto& pair : info.players[player].escalatingRngRolls ) + { + mechanics.escalatingRngRolls[pair.first] = pair.second; + } + for ( auto& pair : info.players[player].escalatingSpellRngRolls ) + { + mechanics.escalatingSpellRngRolls[pair.first] = pair.second; + } + mechanics.sustainedSpellMPUsedSorcery = 0; + mechanics.sustainedSpellMPUsedMysticism = 0; + mechanics.sustainedSpellMPUsedThaumaturgy = 0; + mechanics.baseSpellMPUsedSorcery = 0; + mechanics.baseSpellMPUsedMysticism = 0; + mechanics.baseSpellMPUsedThaumaturgy = 0; + mechanics.sustainedSpellMPUsedSorcery = info.players[player].sustainedSpellMPUsedSorcery; + mechanics.sustainedSpellMPUsedMysticism = info.players[player].sustainedSpellMPUsedMysticism; + mechanics.sustainedSpellMPUsedThaumaturgy = info.players[player].sustainedSpellMPUsedThaumaturgy; + mechanics.baseSpellMPUsedSorcery = info.players[player].baseSpellMPUsedSorcery; + mechanics.baseSpellMPUsedMysticism = info.players[player].baseSpellMPUsedMysticism; + mechanics.baseSpellMPUsedThaumaturgy = info.players[player].baseSpellMPUsedThaumaturgy; } Player::Minimap_t::mapDetails = info.map_messages; @@ -7646,14 +6924,22 @@ list_t* loadGameFollowers(const SaveGameInfo& info) { for (int c = 0; c < NUMEFFECTS; ++c) { if ( c < follower.EFFECTS.size() ) { - stats->EFFECTS[c] = follower.EFFECTS[c]; + stats->setEffectValueUnsafe(c, (Uint8)follower.EFFECTS[c]); stats->EFFECTS_TIMERS[c] = follower.EFFECTS_TIMERS[c]; } else { - stats->EFFECTS[c] = 0; + stats->clearEffect(c); stats->EFFECTS_TIMERS[c] = 0; } + if ( c < follower.EFFECTS_ACCRETION_TIME.size() ) + { + stats->EFFECTS_ACCRETION_TIME[c] = follower.EFFECTS_ACCRETION_TIME[c]; + } + else + { + stats->EFFECTS_ACCRETION_TIME[c] = 0; + } } constexpr int NUMMISCFLAGS = sizeof(Stat::MISC_FLAGS) / sizeof(Stat::MISC_FLAGS[0]); for (int c = 0; c < NUMMISCFLAGS && c < follower.MISC_FLAGS.size(); ++c) { @@ -7721,6 +7007,14 @@ list_t* loadGameFollowers(const SaveGameInfo& info) { int SaveGameInfo::Player::isCharacterValidFromDLC() { + if ( this->race == RACE_RAT || this->race == RACE_TROLL || this->race == RACE_IMP || this->race == RACE_SPIDER ) + { + if ( !enabledDLCPack2 ) + { + return INVALID_REQUIREDLC2; + } + } + switch ( this->char_class ) { case CLASS_CONJURER: @@ -7741,6 +7035,16 @@ int SaveGameInfo::Player::isCharacterValidFromDLC() return INVALID_REQUIREDLC2; } break; + case CLASS_BARD: + case CLASS_SAPPER: + case CLASS_SCION: + case CLASS_HERMIT: + case CLASS_PALADIN: + if ( !enabledDLCPack3 ) + { + return INVALID_REQUIREDLC3; + } + break; default: break; } @@ -7765,6 +7069,16 @@ int SaveGameInfo::Player::isCharacterValidFromDLC() return INVALID_REQUIREDLC2; } break; + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_GREMLIN: + case RACE_SALAMANDER: + case RACE_GNOME: + if ( !enabledDLCPack3 ) + { + return INVALID_REQUIREDLC3; + } + break; default: break; } @@ -7840,6 +7154,41 @@ int SaveGameInfo::Player::isCharacterValidFromDLC() } return isAchievementUnlockedForClassUnlock(RACE_INSECTOID) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; break; + case CLASS_BARD: + if ( this->race == RACE_GNOME ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_GNOME) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; + case CLASS_SAPPER: + if ( this->race == RACE_GREMLIN ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_GREMLIN) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; + case CLASS_SCION: + if ( this->race == RACE_DRYAD ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_DRYAD) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; + case CLASS_HERMIT: + if ( this->race == RACE_MYCONID ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_MYCONID) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; + case CLASS_PALADIN: + if ( this->race == RACE_SALAMANDER ) + { + return VALID_OK_CHARACTER; + } + return isAchievementUnlockedForClassUnlock(RACE_SALAMANDER) ? VALID_OK_CHARACTER : INVALID_REQUIRE_ACHIEVEMENT; + break; default: break; } diff --git a/src/scores.hpp b/src/scores.hpp index 14611c7b3..643c02c38 100644 --- a/src/scores.hpp +++ b/src/scores.hpp @@ -14,8 +14,8 @@ #include "json.hpp" #include "player.hpp" -#define SCORESFILE "scores.dat" -#define SCORESFILE_MULTIPLAYER "scores_multiplayer.dat" +#define SCORESFILE "savegames/scores.json" +#define SCORESFILE_MULTIPLAYER "savegames/scores_multiplayer.json" // game score structure #define MAXTOPSCORES 100 @@ -55,7 +55,22 @@ static const int STATISTICS_TRIBE_SUBSCRIBE = 13; static const int STATISTICS_POP_QUIZ_1 = 14; static const int STATISTICS_POP_QUIZ_2 = 15; static const int STATISTICS_TOTAL_KILLS = 16; +static const int STATISTICS_FLAVORTOWN = 17; +static const int STATISTICS_BARDIC_INSPIRATION = 18; +static const int STATISTICS_SKID_ROW = 19; +static const int STATISTICS_WRECKING_CREW = 20; +static const int STATISTICS_RIGHTEOUS_FURY = 21; +static const int STATISTICS_PARRY_TANK = 22; +static const int STATISTICS_EAT_ME = 23; +static const int STATISTICS_BONK = 24; +static const int STATISTICS_NO_CAP = 25; +static const int STATISTICS_DONT_TOUCH_HAIR = 26; +static const int STATISTICS_GARGOYLES_QUEST = 27; +static const int STATISTICS_QUACKERY = 28; +static const int STATISTICS_THATS_CHEATING = 29; +static const int STATISTICS_DISCIPLINE = 30; static const int STATISTICS_DISABLE_UPLOAD = 31; +static const int STATISTICS_FIRE_FIGHTER = 32; enum SteamStatIndexes : int { @@ -116,7 +131,22 @@ enum SteamStatIndexes : int STEAM_STAT_DUNGEONSEED, STEAM_STAT_PITCH_PERFECT, STEAM_STAT_RUNG_OUT, - STEAM_STAT_SMASH_MELEE + STEAM_STAT_SMASH_MELEE, + STEAM_STAT_CALL_LOCKSMITH, + STEAM_STAT_PREMIUM_LOOTBOX, + STEAM_STAT_WITCHES_BREW, + STEAM_STAT_HOBBYIST, + STEAM_STAT_BLESSED_ADDITION, + STEAM_STAT_THATS_A_WRAP, + STEAM_STAT_LET_HIM_COOK, + STEAM_STAT_TOUCHE, + STEAM_STAT_MERCENARY_ARMY, + STEAM_STAT_COLONIST, + STEAM_STAT_PRICKLY_PERSONALITY, + STEAM_STAT_BOOM_DYNAMITE, + STEAM_STAT_PAY_TO_WIN, + STEAM_STAT_DOESNT_COUNT, + STEAM_STAT_SOURCE_ENGINE }; enum SteamGlobalStatIndexes : int @@ -328,7 +358,23 @@ static const std::pair steamStatAchStringsAndMaxVals[] = std::make_pair("BARONY_ACH_DUNGEONSEED", 12), // STEAM_STAT_DUNGEONSEED std::make_pair("BARONY_ACH_BAT1000", 81), // STEAM_STAT_PITCH_PERFECT std::make_pair("BARONY_ACH_RUNG_OUT", 20), // STEAM_STAT_RUNG_OUT - std::make_pair("BARONY_ACH_SMASH_MELEE", 500) // STEAM_STAT_SMASH_MELEE + std::make_pair("BARONY_ACH_SMASH_MELEE", 500), // STEAM_STAT_SMASH_MELEE + + std::make_pair("BARONY_ACH_CALL_LOCKSMITH", 20), // STEAM_STAT_LOCKSMITH, + std::make_pair("BARONY_ACH_PREMIUM_LOOTBOX", 20), // STEAM_STAT_PREMIUM_LOOTBOX, + std::make_pair("BARONY_ACH_WITCHES_BREW", 50), // STEAM_STAT_WITCHES_BREW, + std::make_pair("BARONY_ACH_HOBBYIST", 50), // STEAM_STAT_HOBBYIST, + std::make_pair("BARONY_ACH_BLESSED_ADDITION", 50), // STEAM_STAT_BLESSED_ADDITION, + std::make_pair("BARONY_ACH_THATS_A_WRAP", 50), // STEAM_STAT_THATS_A_WRAP, + std::make_pair("BARONY_ACH_LET_HIM_COOK", 100), // STEAM_STAT_LET_HIM_COOK, + std::make_pair("BARONY_ACH_TOUCHE", 1000), // STEAM_STAT_TOUCHE, + std::make_pair("BARONY_ACH_MERCENARY_ARMY", 20), // STEAM_STAT_MERCENARY_ARMY, + std::make_pair("BARONY_ACH_COLONIST", 50), // STEAM_STAT_COLONIST, + std::make_pair("BARONY_ACH_PRICKLY_PERSONALITY", 20), // STEAM_STAT_PRICKLY_PERSONALITY, + std::make_pair("BARONY_ACH_BOOM_DYNAMITE", 50), // STEAM_STAT_BOOM_DYNAMITE, + std::make_pair("BARONY_ACH_PAY_TO_WIN", 5000), // STEAM_STAT_PAY_TO_WIN, + std::make_pair("BARONY_ACH_DOESNT_COUNT", 50), // STEAM_STAT_DOESNT_COUNT, + std::make_pair("BARONY_ACH_SOURCE_ENGINE", 1000) // STEAM_STAT_SOURCE_ENGINE }; typedef struct score_t @@ -348,8 +394,10 @@ typedef struct score_t Sint32 conductGameChallenges[NUM_CONDUCT_CHALLENGES]; Sint32 gameStatistics[NUM_GAMEPLAY_STATISTICS]; } score_t; -extern list_t topscores; -extern list_t topscoresMultiplayer; +extern list_t topscores_json; +extern list_t topscoresMultiplayer_json; +extern list_t topscores_legacy; +extern list_t topscoresMultiplayer_legacy; extern int victory; extern Uint32 completionTime; @@ -402,9 +450,6 @@ enum SaveFileType { extern int savegameCurrentFileIndex; std::string setSaveGameFileName(bool singleplayer, SaveFileType type, int saveIndex = savegameCurrentFileIndex); -int saveGameOld(int saveIndex = savegameCurrentFileIndex); -int loadGameOld(int player, int saveIndex = savegameCurrentFileIndex); -list_t* loadGameFollowersOld(int saveIndex = savegameCurrentFileIndex); int deleteSaveGame(int gametype, int saveIndex = savegameCurrentFileIndex); bool saveGameExists(bool singleplayer, int saveIndex = savegameCurrentFileIndex); @@ -509,7 +554,19 @@ struct SaveGameInfo { std::vector> shopkeeperHostility; std::vector>> compendium_item_events; std::vector> itemDegradeRNG; - int sustainedSpellMPUsed = 0; + std::vector> escalatingRngRolls; + std::vector> escalatingSpellRngRolls; + std::vector> appraisal_item_progress; + std::vector learnedSpells; + std::vector> sustainedSpellIDCounter; + std::vector> ducksInARow; + std::vector> favoriteBooksAchievement; + int sustainedSpellMPUsedSorcery = 0; + int sustainedSpellMPUsedMysticism = 0; + int sustainedSpellMPUsedThaumaturgy = 0; + int baseSpellMPUsedSorcery = 0; + int baseSpellMPUsedMysticism = 0; + int baseSpellMPUsedThaumaturgy = 0; struct stat_t { struct item_t { @@ -543,7 +600,6 @@ struct SaveGameInfo { bool identified = false; int x = 0; int y = 0; - bool serialize(FileInterface* fp) { fp->property("type", type); fp->property("status", status); @@ -612,11 +668,13 @@ struct SaveGameInfo { std::vector PROFICIENCIES; std::vector EFFECTS; std::vector EFFECTS_TIMERS; + std::vector EFFECTS_ACCRETION_TIME; std::vector MISC_FLAGS; std::vector> attributes; std::vector> player_equipment; std::vector> npc_equipment; std::vector inventory; + std::vector void_chest_inventory; std::vector> player_lootbags; bool serialize(FileInterface* fp) { @@ -641,10 +699,12 @@ struct SaveGameInfo { fp->property("PROFICIENCIES", PROFICIENCIES); fp->property("EFFECTS", EFFECTS); fp->property("EFFECTS_TIMERS", EFFECTS_TIMERS); + fp->property("EFFECTS_ACCRETION_TIME", EFFECTS_ACCRETION_TIME); fp->property("MISC_FLAGS", MISC_FLAGS); fp->property("player_equipment", player_equipment); fp->property("npc_equipment", npc_equipment); fp->property("inventory", inventory); + fp->property("void_chest_inventory", void_chest_inventory); fp->property("attributes", attributes); fp->property("lootbags", player_lootbags); return true; @@ -694,7 +754,19 @@ struct SaveGameInfo { fp->property("shopkeeper_hostility", shopkeeperHostility); fp->property("compendium_item_events", compendium_item_events); fp->property("item_degrade_rng", itemDegradeRNG); - fp->property("sustained_mp_used", sustainedSpellMPUsed); + fp->property("sustained_mp_used_sorcery", sustainedSpellMPUsedSorcery); + fp->property("sustained_mp_used_mysticism", sustainedSpellMPUsedMysticism); + fp->property("sustained_mp_used_thaumaturgy", sustainedSpellMPUsedThaumaturgy); + fp->property("base_mp_used_sorcery", baseSpellMPUsedSorcery); + fp->property("base_mp_used_mysticism", baseSpellMPUsedMysticism); + fp->property("base_mp_used_thaumaturgy", baseSpellMPUsedThaumaturgy); + fp->property("learned_spells", learnedSpells); + fp->property("ducks_in_a_row", ducksInARow); + fp->property("favorite_books_achievement", favoriteBooksAchievement); + fp->property("sustained_spell_id_counters", sustainedSpellIDCounter); + fp->property("escalating_rng_rolls", escalatingRngRolls); + fp->property("escalating_spell_rng_rolls", escalatingSpellRngRolls); + fp->property("appraisal_time_progress", appraisal_item_progress); return true; } @@ -805,7 +877,17 @@ class AchievementObserver BARONY_ACH_MASTER, BARONY_ACH_DAPPER, BARONY_ACH_SPROUTS, - BARONY_ACH_BY_THE_BOOK + BARONY_ACH_BY_THE_BOOK, + BARONY_ACH_THATS_A_WRAP, + BARONY_ACH_APPRENTICES, + BARONY_ACH_SHORT_SHORTS, + BARONY_ACH_HOLY_ORDER, + BARONY_ACH_CONSCRIPTED, + BARONY_ACH_LONER_LEAGUE, + BARONY_ACH_RIZZLERS, + BARONY_ACH_FOREIGN_EXCHANGE, + BARONY_ACH_STUDY_ABROAD, + BARONY_ACH_FOOD_FIGHT }; enum AchievementEvent : int { @@ -897,6 +979,14 @@ class AchievementObserver Uint32 ticksByTheBookViewed = 0; static bool allPlayersDeadEvent; + int skidRow = 0; + int bonk = 0; + int righteousFury = 0; + int hellsKitchen = 0; + int eatMe = 0; + int sourceEngine = 0; + int parryTank = 0; + std::pair realBoy; std::unordered_map caughtInAMoshTargets; std::vector strungOutTicks; @@ -906,6 +996,8 @@ class AchievementObserver std::unordered_set rat5000secondRule; std::unordered_set phantomMaskFirstStrikes; std::unordered_set bountyTargets; + std::vector manifestDestinyChests; + Uint32 manifestDestinyChestSequence = 0; bool updatedBountyTargets = false; bool wearingBountyHat = false; static std::set startingClassItems; diff --git a/src/shops.cpp b/src/shops.cpp index fb391c156..156876ba3 100644 --- a/src/shops.cpp +++ b/src/shops.cpp @@ -29,6 +29,7 @@ std::string shopspeech[MAXPLAYERS] = { "" }; int shopkeepertype[MAXPLAYERS] = { 0 }; std::string shopkeepername[MAXPLAYERS] = { "" }; char shopkeepername_client[MAXPLAYERS][64]; +std::map hamletShopkeeperSkillLimit[MAXPLAYERS]; std::unordered_map> shopkeeperMysteriousItems( { @@ -211,7 +212,7 @@ bool buyItemFromShop(const int player, Item* item, bool& bOutConsumedEntireStack { shopspeech[player] = Language::get(4255 + local_rng.rand() % 5); } - else if ( items[item->type].value * 1.5 >= item->buyValue(player) ) + else if ( item->getGoldValue() * 1.5 >= item->buyValue(player) ) { shopspeech[player] = Language::get(200 + local_rng.rand() % 3); } @@ -273,12 +274,28 @@ bool buyItemFromShop(const int player, Item* item, bool& bOutConsumedEntireStack } else { - if ( rand() % 100 <= (std::max(10, buyValue)) ) // 10% to 100% from 1-100 gold + if ( local_rng.rand() % 100 <= (std::max(10, buyValue)) ) // 10% to 100% from 1-100 gold { increaseSkill = true; } } + if ( increaseSkill && entity ) + { + if ( !strcmp(map.name, "Mages Guild") ) + { + int increases = hamletShopkeeperSkillLimit[player][entity->getUID()]; + if ( increases >= hamletTradingSkillLimit ) + { + increaseSkill = false; + if ( local_rng.rand() % 2 ) + { + messagePlayer(player, MESSAGE_HINT | MESSAGE_INTERACTION, Language::get(6868)); + } + } + } + } + if ( increaseSkill ) { if ( buyValue <= 1 ) @@ -286,11 +303,25 @@ bool buyItemFromShop(const int player, Item* item, bool& bOutConsumedEntireStack if ( stats[player]->getProficiency(PRO_TRADING) < SKILL_LEVEL_SKILLED ) { players[player]->entity->increaseSkill(PRO_TRADING); + if ( entity ) + { + if ( !strcmp(map.name, "Mages Guild") ) + { + hamletShopkeeperSkillLimit[player][entity->getUID()]++; + } + } } } else { players[player]->entity->increaseSkill(PRO_TRADING); + if ( entity ) + { + if ( !strcmp(map.name, "Mages Guild") ) + { + hamletShopkeeperSkillLimit[player][entity->getUID()]++; + } + } } } //if ( local_rng.rand() % 2 ) @@ -440,7 +471,7 @@ bool isItemSellableToShop(const int player, Item* item) } break; case 3: // bookstore - if ( itemCategory(item) != SPELLBOOK && itemCategory(item) != SCROLL && itemCategory(item) != BOOK ) + if ( itemCategory(item) != SPELLBOOK && itemCategory(item) != SCROLL && itemCategory(item) != BOOK && itemCategory(item) != TOME_SPELL ) { deal = false; } @@ -456,6 +487,10 @@ bool isItemSellableToShop(const int player, Item* item) { deal = false; } + if ( itemTypeIsFoci(item->type) ) + { + deal = true; + } break; case 6: // food if ( itemCategory(item) != FOOD ) @@ -468,6 +503,10 @@ bool isItemSellableToShop(const int player, Item* item) { deal = false; } + if ( itemTypeIsFoci(item->type) || itemTypeIsInstrument(item->type) ) + { + deal = false; + } break; case 8: // hunting if ( itemCategory(item) != WEAPON @@ -552,7 +591,8 @@ bool sellItemToShop(const int player, Item* item) } else if ( itemCategory(item) == SPELLBOOK || itemCategory(item) == BOOK - || itemCategory(item) == SCROLL ) + || itemCategory(item) == SCROLL + || itemCategory(item) == TOME_SPELL ) { shopspeech[player] = Language::get(3915); } @@ -583,7 +623,7 @@ bool sellItemToShop(const int player, Item* item) return false; } - if ( items[item->type].value * .75 <= item->sellValue(player) ) + if ( item->getGoldValue() * .75 <= item->sellValue(player) ) { shopspeech[player] = Language::get(209 + local_rng.rand() % 3); } diff --git a/src/shops.hpp b/src/shops.hpp index 36bf491ad..71fbf6774 100644 --- a/src/shops.hpp +++ b/src/shops.hpp @@ -24,6 +24,8 @@ extern std::string shopspeech[MAXPLAYERS]; extern int shopkeepertype[MAXPLAYERS]; extern std::string shopkeepername[MAXPLAYERS]; extern char shopkeepername_client[MAXPLAYERS][64]; +extern std::map hamletShopkeeperSkillLimit[MAXPLAYERS]; +static const int hamletTradingSkillLimit = 7; void startTradingServer(Entity* entity, int player); bool isItemSellableToShop(const int player, Item* item); diff --git a/src/stat.cpp b/src/stat.cpp index 059b4e4b1..f4db321b2 100644 --- a/src/stat.cpp +++ b/src/stat.cpp @@ -20,6 +20,7 @@ #include "player.hpp" #include "prng.hpp" #include "mod_tools.hpp" +#include "collision.hpp" Stat* stats[MAXPLAYERS]; @@ -171,20 +172,92 @@ Sint32 Stat::getModifiedProficiency(int skill) const equipmentBonus -= abs(helmet->beatitude) * 10; } } - else if ( (helmet->type == HAT_CIRCLET || helmet->type == HAT_CIRCLET_WISDOM) && skill == PRO_SPELLCASTING ) + else if ( (helmet->type == HAT_CIRCLET && skill == PRO_MYSTICISM) + || (helmet->type == HAT_CIRCLET_SORCERY && skill == PRO_SORCERY) + || (helmet->type == HAT_CIRCLET_THAUMATURGY && skill == PRO_THAUMATURGY) ) { if ( helmet->beatitude >= 0 || cursedItemIsBuff ) { - equipmentBonus += std::min(maxEquipmentBonusToSkill, (1 + abs(helmet->beatitude)) * 10); + equipmentBonus += std::min(maxEquipmentBonusToSkill, (1 + abs(helmet->beatitude)) * 5); } else { - equipmentBonus -= abs(helmet->beatitude) * 10; + equipmentBonus -= abs(helmet->beatitude) * 5; } } } - return std::min(100, std::max(0, base + equipmentBonus)); + int effectBonus = 0; + if ( getEffectActive(EFF_NIMBLENESS) + && (skill == PRO_LOCKPICKING + || skill == PRO_SWORD + || skill == PRO_RANGED + || skill == PRO_STEALTH) ) + { + effectBonus += 10 + 5 * std::max(0, ((int)(getEffectActive(EFF_NIMBLENESS) & 0xF) - 1)); + } + if ( getEffectActive(EFF_GREATER_MIGHT) + && (skill == PRO_POLEARM + || skill == PRO_AXE + || skill == PRO_MACE) ) + { + effectBonus += 10 + 5 * std::max(0, ((int)(getEffectActive(EFF_GREATER_MIGHT) & 0xF) - 1)); + } + if ( getEffectActive(EFF_COUNSEL) + && (skill == PRO_SORCERY + || skill == PRO_MYSTICISM) ) + { + effectBonus += 10 + 5 * std::max(0, ((int)(getEffectActive(EFF_COUNSEL) & 0xF) - 1)); + } + if ( getEffectActive(EFF_STURDINESS) + && (skill == PRO_SHIELD) ) + { + effectBonus += 10 + 5 * std::max(0, ((int)(getEffectActive(EFF_STURDINESS) & 0xF) - 1)); + } + int result = std::min(100, std::max(0, base + equipmentBonus + effectBonus)); + if ( skill == PRO_STEALTH && getEffectActive(EFF_DUSTED) ) + { + result *= 0.3; + } + return result; +} + +Sint32 Stat::getThaumProficiencySpellStatBonus(int whichStat, Sint32 currentBonus) +{ + Sint32 bonus = 0; + if ( whichStat == STAT_INT ) + { + if ( getEffectActive(EFF_COUNSEL) ) + { + real_t ratio = std::max(0.0, 0.1 * ((int)(getEffectActive(EFF_COUNSEL) & 0xF) - 1)); + bonus = (std::max(2 + (getEffectActive(EFF_COUNSEL) & 0xF), (int)(currentBonus * ratio))); + } + } + else if ( whichStat == STAT_DEX ) + { + if ( getEffectActive(EFF_NIMBLENESS) ) + { + real_t ratio = std::max(0.0, 0.1 * ((int)(getEffectActive(EFF_NIMBLENESS) & 0xF) - 1)); + bonus = (std::max(2 + (getEffectActive(EFF_NIMBLENESS) & 0xF), (int)(currentBonus * ratio))); + } + } + else if ( whichStat == STAT_STR ) + { + if ( getEffectActive(EFF_GREATER_MIGHT) ) + { + real_t ratio = std::max(0.0, 0.1 * ((int)(getEffectActive(EFF_GREATER_MIGHT) & 0xF) - 1)); + bonus = (std::max(2 + (getEffectActive(EFF_GREATER_MIGHT) & 0xF), (int)(currentBonus * ratio))); + } + } + else if ( whichStat == STAT_CON ) + { + if ( getEffectActive(EFF_STURDINESS) ) + { + real_t ratio = std::max(0.0, 0.1 * ((int)(getEffectActive(EFF_STURDINESS) & 0xF) - 1)); + bonus = (std::max(2 + (getEffectActive(EFF_STURDINESS) & 0xF), (int)(currentBonus * ratio))); + } + } + return bonus; } //Destructor @@ -322,6 +395,7 @@ Stat::~Stat() } list_FreeAll(&this->magic_effects); list_FreeAll(&this->inventory); + list_FreeAll(&this->void_chest_inventory); } void Stat::clearStats() @@ -359,8 +433,9 @@ void Stat::clearStats() } if (x < NUMEFFECTS) { - this->EFFECTS[x] = false; + this->EFFECTS[x] = 0; this->EFFECTS_TIMERS[x] = 0; + this->EFFECTS_ACCRETION_TIME[x] = 0; } } @@ -407,6 +482,7 @@ void Stat::clearStats() freePlayerEquipment(); list_FreeAll(&this->inventory); + list_FreeAll(&this->void_chest_inventory); } /*------------------------------------------------------------------------------- @@ -590,6 +666,7 @@ Stat* Stat::copyStats() { newStat->EFFECTS[c] = this->EFFECTS[c]; newStat->EFFECTS_TIMERS[c] = this->EFFECTS_TIMERS[c]; + newStat->EFFECTS_ACCRETION_TIME[c] = this->EFFECTS_ACCRETION_TIME[c]; } for ( c = 0; c < ITEM_SLOT_NUM; c++ ) @@ -626,6 +703,14 @@ Stat* Stat::copyStats() Item* item = (Item*)node->element; item->node = node; } + newStat->void_chest_inventory.first = nullptr; + newStat->void_chest_inventory.last = nullptr; + list_Copy(&newStat->void_chest_inventory, &this->void_chest_inventory); + for ( node = newStat->void_chest_inventory.first; node != NULL; node = node->next ) + { + Item* item = (Item*)node->element; + item->node = node; + } if (this->helmet) { @@ -839,20 +924,25 @@ void Stat::printStats() printlog("Effects & timers: "); for (int i = 0; i < NUMEFFECTS; ++i) { - printlog("[%d] = %s. timer[%d] = %d", i, (this->EFFECTS[i]) ? "true" : "false", i, this->EFFECTS_TIMERS[i]); + printlog("[%d] = %s. timer[%d] = %d", i, (this->getEffectActive(i)) ? "true" : "false", i, this->EFFECTS_TIMERS[i]); } } int Stat::pickRandomEquippedItemToDegradeOnHit(Item** returnItem, bool excludeWeapon, bool excludeShield, bool excludeArmor, bool excludeJewelry) { - if ( EFFECTS[EFF_SHAPESHIFT] ) + if ( getEffectActive(EFF_SHAPESHIFT) ) { returnItem = nullptr; return -1; } if ( shield && (itemTypeIsQuiver(shield->type) || itemCategory(shield) == SPELLBOOK - || shield->type == TOOL_TINKERING_KIT) ) + || shield->type == TOOL_TINKERING_KIT + || shield->type == TOOL_FRYING_PAN + || itemTypeIsFoci(shield->type) + || itemTypeIsInstrument(shield->type) + || shield->type == TOOL_DUCK ) + ) { excludeShield = true; } @@ -1417,7 +1507,10 @@ int Stat::getActiveShieldBonus(bool checkShield, bool excludeSkill, Item* shield } if ( item ) { - if ( itemCategory(item) == SPELLBOOK || itemTypeIsQuiver(item->type) ) + if ( itemCategory(item) == SPELLBOOK || itemTypeIsQuiver(item->type) + || itemTypeIsFoci(item->type) + || itemTypeIsInstrument(item->type) + || item->type == TOOL_DUCK ) { return 0; } @@ -1434,6 +1527,38 @@ int Stat::getActiveShieldBonus(bool checkShield, bool excludeSkill, Item* shield } } +int Stat::getParryingACBonus(Stat* myStats, Item* myWeapon, bool checkWeapon, bool excludeSkill, int weaponSkill) +{ + if ( !checkWeapon ) + { + if ( excludeSkill ) + { + return 0; + } + return (myStats ? myStats->getModifiedProficiency(weaponSkill) / 25 : 0); + } + + if ( myWeapon ) + { + if ( itemCategory(myWeapon) != WEAPON ) + { + return 0; + } + if ( excludeSkill ) + { + return 0; + } + int bonus = (myStats ? myStats->getModifiedProficiency(weaponSkill) / 25 : 0); + bonus += myWeapon->weaponGetAttack(myStats); + bonus += (5 + (excludeSkill ? 0 : std::min(SKILL_LEVEL_SKILLED, myStats ? myStats->getModifiedProficiency(weaponSkill) : 0) / 5)); + return bonus; + } + else + { + return 0; + } +} + int Stat::getPassiveShieldBonus(bool checkShield, bool excludeSkill) const { if ( !checkShield ) @@ -1464,6 +1589,57 @@ int Stat::getPassiveShieldBonus(bool checkShield, bool excludeSkill) const } } +int Stat::numShillelaghDebuffsActive(Entity* my) +{ + static std::set effs = { + EFF_ASLEEP, + EFF_POISONED, + EFF_CONFUSED, + EFF_BLIND, + EFF_GREASY, + EFF_MESSY, + EFF_PARALYZED, + EFF_BLEEDING, + EFF_SLOW, + EFF_PACIFY, + EFF_WEBBED, + EFF_FEAR, + //EFF_DISORIENTED, + EFF_ROOTED, + EFF_STATIC, + EFF_DRUNK, + EFF_WEAKNESS, + EFF_INCOHERENCE, + EFF_MINIMISE, + EFF_DUCKED, + EFF_MAGIC_GREASE, + EFF_NUMBING_BOLT, + EFF_CURSE_FLESH, + EFF_TABOO, + EFF_COWARDICE, + EFF_DIZZY, + EFF_SPIN, + EFF_DUSTED, + EFF_DISRUPTED, + EFF_FROST, + EFF_HOLY_FIRE + }; + int result = 0; + if ( my && my->flags[BURNING] ) + { + ++result; + } + for ( auto eff : effs ) + { + if ( getEffectActive(eff) ) + { + ++result; + } + } + + return result; +} + bool Stat::statusEffectRemovedByCureAilment(const int effect, Entity* my) { switch ( effect ) @@ -1481,6 +1657,22 @@ bool Stat::statusEffectRemovedByCureAilment(const int effect, Entity* my) case EFF_WEBBED: case EFF_FEAR: case EFF_DISORIENTED: + case EFF_ROOTED: + case EFF_STATIC: + case EFF_WEAKNESS: + case EFF_INCOHERENCE: + case EFF_MINIMISE: + case EFF_NUMBING_BOLT: + case EFF_CURSE_FLESH: + case EFF_TABOO: + case EFF_COWARDICE: + case EFF_DIZZY: + case EFF_SPIN: + case EFF_DUSTED: + case EFF_STASIS: + case EFF_DISRUPTED: + case EFF_FROST: + case EFF_HOLY_FIRE: return true; break; case EFF_DRUNK: @@ -1510,6 +1702,12 @@ Uint32 Stat::getLootingBagKey(const int player) void Stat::addItemToLootingBag(const int player, const real_t x, const real_t y, Item& item) { + if ( item.type == TOOL_DUCK ) + { + item.applyDuck(achievementObserver.playerUids[player], x, y, nullptr, false); + return; + } + Uint32 lootingBagKey = getLootingBagKey(player); if ( player_lootbags.find(lootingBagKey) == player_lootbags.end() ) { @@ -1618,4 +1816,478 @@ bool Stat::emptyLootingBag(const int player, Uint32 key) } } return false; +} + +real_t Stat::getEnsembleEffectBonus(Stat::EnsembleEffectsBonusType bonusType, int checkEffectStrength) +{ + static const Sint32 kBreakPoint4 = 41; + static const Sint32 kBreakPoint3 = 20; + static const Sint32 kBreakPoint2 = 6; + static const Sint32 kBreakPoint1 = 1; + + real_t result = 0.0; + Uint8 effectStrength = getEffectActive(EFF_ENSEMBLE_FLUTE); + if ( checkEffectStrength >= 0 ) + { + effectStrength = checkEffectStrength; + } + if ( effectStrength > 0 ) + { + if ( effectStrength > 0 ) + { + --effectStrength; // offset by 1. + } + if ( bonusType == ENSEMBLE_FLUTE_EFF_1 ) + { + Sint32 total = 1; // bonus at effect strength 0 + static const Sint32 mult4 = 1; + static const Sint32 mult3 = 1; + static const Sint32 mult2 = 1; + static const Sint32 mult1 = 1; + if ( effectStrength >= kBreakPoint4 ) + { + total += mult4 * (1 + (effectStrength - kBreakPoint4) / 4); + effectStrength -= (effectStrength - kBreakPoint4 + 1); + } + if ( effectStrength >= kBreakPoint3 ) + { + total += mult3 * (1 + (effectStrength - kBreakPoint3) / 3); + effectStrength -= (effectStrength - kBreakPoint3 + 1); + } + if ( effectStrength >= kBreakPoint2 ) + { + total += mult2 * (1 + (effectStrength - kBreakPoint2) / 2); + effectStrength -= (effectStrength - kBreakPoint2 + 1); + } + if ( effectStrength >= kBreakPoint1 ) + { + total += mult1 * (1 + (effectStrength - kBreakPoint1) / 1); + effectStrength -= (effectStrength - kBreakPoint1 + 1); + } + result += total; + } + if ( bonusType == ENSEMBLE_FLUTE_EFF_2 ) + { + } + if ( bonusType == ENSEMBLE_FLUTE_TIER ) + { + if ( effectStrength >= kEnsembleBreakPointTier4 ) + { + result = 50.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier3 ) + { + result = 40.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier2 ) + { + result = 30.0; + } + else if(effectStrength >= kEnsembleBreakPointTier1 ) + { + result = 20.0; + } + } + } + + effectStrength = getEffectActive(EFF_ENSEMBLE_LUTE); + if ( checkEffectStrength >= 0 ) + { + effectStrength = checkEffectStrength; + } + if ( effectStrength > 0 ) + { + if ( effectStrength > 0 ) + { + --effectStrength; // offset by 1. + } + if ( bonusType == ENSEMBLE_LUTE_EFF_1 ) + { + Sint32 total = 3; // bonus at effect strength 0 + static const Sint32 mult4 = 3; + static const Sint32 mult3 = 3; + static const Sint32 mult2 = 3; + static const Sint32 mult1 = 3; + if ( effectStrength >= kBreakPoint4 ) + { + total += mult4 * (1 + (effectStrength - kBreakPoint4) / 4); + effectStrength -= (effectStrength - kBreakPoint4 + 1); + } + if ( effectStrength >= kBreakPoint3 ) + { + total += mult3 * (1 + (effectStrength - kBreakPoint3) / 3); + effectStrength -= (effectStrength - kBreakPoint3 + 1); + } + if ( effectStrength >= kBreakPoint2 ) + { + total += mult2 * (1 + (effectStrength - kBreakPoint2) / 2); + effectStrength -= (effectStrength - kBreakPoint2 + 1); + } + if ( effectStrength >= kBreakPoint1 ) + { + total += mult1 * (1 + (effectStrength - kBreakPoint1) / 1); + effectStrength -= (effectStrength - kBreakPoint1 + 1); + } + result += total; + } + if ( bonusType == ENSEMBLE_LUTE_EFF_2 ) + { + } + if ( bonusType == ENSEMBLE_LUTE_TIER ) + { + if ( effectStrength >= kEnsembleBreakPointTier4 ) + { + result = 18.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier3 ) + { + result = 15.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier2 ) + { + result = 12.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier1 ) + { + result = 10.0; + } + } + } + + effectStrength = getEffectActive(EFF_ENSEMBLE_DRUM); + if ( checkEffectStrength >= 0 ) + { + effectStrength = checkEffectStrength; + } + if ( effectStrength > 0 ) + { + if ( effectStrength > 0 ) + { + --effectStrength; // offset by 1. + } + if ( bonusType == ENSEMBLE_DRUM_EFF_1 ) + { + Sint32 total = 1; // bonus at effect strength 0 + static const Sint32 mult4 = 1; + static const Sint32 mult3 = 1; + static const Sint32 mult2 = 1; + static const Sint32 mult1 = 1; + if ( effectStrength >= kBreakPoint4 ) + { + total += mult4 * (1 + (effectStrength - kBreakPoint4) / 4); + effectStrength -= (effectStrength - kBreakPoint4 + 1); + } + if ( effectStrength >= kBreakPoint3 ) + { + total += mult3 * (1 + (effectStrength - kBreakPoint3) / 3); + effectStrength -= (effectStrength - kBreakPoint3 + 1); + } + if ( effectStrength >= kBreakPoint2 ) + { + total += mult2 * (1 + (effectStrength - kBreakPoint2) / 2); + effectStrength -= (effectStrength - kBreakPoint2 + 1); + } + if ( effectStrength >= kBreakPoint1 ) + { + total += mult1 * (1 + (effectStrength - kBreakPoint1) / 1); + effectStrength -= (effectStrength - kBreakPoint1 + 1); + } + result += total; + } + if ( bonusType == ENSEMBLE_DRUM_EFF_2 ) + { + } + if ( bonusType == ENSEMBLE_DRUM_TIER ) + { + if ( effectStrength >= kEnsembleBreakPointTier4 ) + { + result = 25.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier3 ) + { + result = 20.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier2 ) + { + result = 15.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier1 ) + { + result = 10.0; + } + } + } + + effectStrength = getEffectActive(EFF_ENSEMBLE_HORN); + if ( checkEffectStrength >= 0 ) + { + effectStrength = checkEffectStrength; + } + if ( effectStrength > 0 ) + { + if ( effectStrength > 0 ) + { + --effectStrength; // offset by 1. + } + if ( bonusType == ENSEMBLE_HORN_EFF_1 ) + { + Sint32 total = 1; // bonus at effect strength 0 + static const Sint32 mult4 = 1; + static const Sint32 mult3 = 1; + static const Sint32 mult2 = 1; + static const Sint32 mult1 = 1; + if ( effectStrength >= kBreakPoint4 ) + { + total += mult4 * (1 + (effectStrength - kBreakPoint4) / 4); + effectStrength -= (effectStrength - kBreakPoint4 + 1); + } + if ( effectStrength >= kBreakPoint3 ) + { + total += mult3 * (1 + (effectStrength - kBreakPoint3) / 3); + effectStrength -= (effectStrength - kBreakPoint3 + 1); + } + if ( effectStrength >= kBreakPoint2 ) + { + total += mult2 * (1 + (effectStrength - kBreakPoint2) / 2); + effectStrength -= (effectStrength - kBreakPoint2 + 1); + } + if ( effectStrength >= kBreakPoint1 ) + { + total += mult1 * (1 + (effectStrength - kBreakPoint1) / 1); + effectStrength -= (effectStrength - kBreakPoint1 + 1); + } + result += total; + } + if ( bonusType == ENSEMBLE_HORN_EFF_2 ) + { + } + if ( bonusType == ENSEMBLE_HORN_TIER ) + { + if ( effectStrength >= kEnsembleBreakPointTier4 ) + { + result = 10.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier3 ) + { + result = 8.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier2 ) + { + result = 5.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier1 ) + { + result = 2.0; + } + } + } + + effectStrength = getEffectActive(EFF_ENSEMBLE_LYRE); + if ( checkEffectStrength >= 0 ) + { + effectStrength = checkEffectStrength; + } + if ( effectStrength > 0 ) + { + if ( effectStrength > 0 ) + { + --effectStrength; // offset by 1. + } + if ( bonusType == ENSEMBLE_LYRE_EFF_1 ) + { + Sint32 total = 1; // bonus at effect strength 0 + static const Sint32 mult4 = 1; + static const Sint32 mult3 = 1; + static const Sint32 mult2 = 1; + static const Sint32 mult1 = 1; + if ( effectStrength >= kBreakPoint4 ) + { + total += mult4 * (1 + (effectStrength - kBreakPoint4) / 4); + effectStrength -= (effectStrength - kBreakPoint4 + 1); + } + if ( effectStrength >= kBreakPoint3 ) + { + total += mult3 * (1 + (effectStrength - kBreakPoint3) / 3); + effectStrength -= (effectStrength - kBreakPoint3 + 1); + } + if ( effectStrength >= kBreakPoint2 ) + { + total += mult2 * (1 + (effectStrength - kBreakPoint2) / 2); + effectStrength -= (effectStrength - kBreakPoint2 + 1); + } + if ( effectStrength >= kBreakPoint1 ) + { + total += mult1 * (1 + (effectStrength - kBreakPoint1) / 1); + effectStrength -= (effectStrength - kBreakPoint1 + 1); + } + result += total; + } + if ( bonusType == ENSEMBLE_LYRE_EFF_2 ) + { + } + if ( bonusType == ENSEMBLE_LYRE_TIER ) + { + if ( effectStrength >= kEnsembleBreakPointTier4 ) + { + result = 35.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier3 ) + { + result = 30.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier2 ) + { + result = 25.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier1 ) + { + result = 20.0; + } + } + if ( bonusType == ENSEMBLE_LYRE_TIER_2 ) + { + if ( effectStrength >= kEnsembleBreakPointTier4 ) + { + result = 5.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier3 ) + { + result = 4.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier2 ) + { + result = 3.0; + } + else if ( effectStrength >= kEnsembleBreakPointTier1 ) + { + result = 2.0; + } + } + } + + return result; +} + +int Stat::getMaxAttackCharge(Stat* myStats) +{ + int charge = MAXCHARGE; + if ( myStats && myStats->getEffectActive(EFF_ENSEMBLE_FLUTE) ) + { + real_t mult = (100.0 - myStats->getEnsembleEffectBonus(ENSEMBLE_FLUTE_TIER)) / 100.0; + charge *= mult; + } + return std::max(5, charge); // failsafe min 5 +} + +real_t Stat::MonsterRangedAccuracy::getAccuracy(Uint32 target) +{ + if ( lastTarget != target ) + { + accuracy = 0.0; + } + if ( ::ticks - lastTick > 5 * TICKS_PER_SECOND ) + { + accuracy /= 2; + } + lastTarget = target; + lastTick = ::ticks; + return accuracy; +} + +void Stat::MonsterRangedAccuracy::incrementAccuracy() +{ + accuracy += 10.0; + if ( local_rng.rand() % 4 == 0 ) + { + accuracy += 20.0; + } + accuracy = std::min(100.0, accuracy); +} +void Stat::MonsterRangedAccuracy::modifyProjectile(Entity& my, Entity& projectile) +{ + Stat* myStats = my.getStats(); + if ( !myStats ) { return; } + if ( myStats->type == LICH + || myStats->type == LICH_FIRE + || myStats->type == LICH_ICE + || myStats->type == DEVIL ) + { + return; + } + int accuracy = this->accuracy; + if ( accuracy == 0 ) { return; } + if ( Entity* target = uidToEntity(this->lastTarget) ) + { + real_t velocity = sqrt(pow(projectile.vel_x, 2) + pow(projectile.vel_y, 2)); + + if ( velocity > 0.01 ) + { + const real_t dist = entityDist(&my, target); + const real_t ticksToHit = (dist / std::max(0.01, velocity)); + const real_t predictx = target->x + (target->vel_x * ticksToHit); + const real_t predicty = target->y + (target->vel_y * ticksToHit); + const real_t tangent = atan2(predicty - projectile.y, predictx - projectile.x); // assume target will be here when attack lands. + + real_t projectileYaw = atan2(projectile.vel_y, projectile.vel_x); + int diff = static_cast((projectileYaw - tangent) * 180.0 / PI) % 360; + if ( diff < 0 ) + { + diff += 360; + } + if ( diff > 180 ) + { + diff -= 360; + } + static ConsoleVariable cvar_monster_ranged_accuracy("/monster_ranged_accuracy", 0); + int maxDiff = std::min(15, currentlevel / 2); + if ( myStats->type == SHOPKEEPER ) + { + maxDiff = 15; + } + if ( svFlags & SV_FLAG_CHEATS ) + { + maxDiff = std::max(maxDiff, *cvar_monster_ranged_accuracy); + } + + if ( diff > 0 ) + { + diff = std::min(maxDiff, diff); + } + else if ( diff < 0 ) + { + diff = std::max(-maxDiff, diff); + } + const real_t yawChange = (diff * PI / 180.0); + const real_t testYaw = projectileYaw - yawChange; + //messagePlayer(0, MESSAGE_DEBUG, "acc %d, changed: %.2f", accuracy, yawChange); + Entity* ohitentity = hit.entity; + const real_t oldx = target->x; + const real_t oldy = target->y; + target->x = predictx; + target->y = predicty; + lineTraceTarget(&my, my.x, my.y, testYaw, 256.0, 0, false, target); + target->x = oldx; + target->y = oldy; + if ( hit.entity == target ) + { + real_t factor = (accuracy / 2.0) + (local_rng.rand() % ((accuracy / 2) + 1)); + if ( accuracy > 50 && local_rng.rand() % 5 == 0 ) + { + factor = (local_rng.rand() % (accuracy + 1)); // full range + } + projectileYaw -= yawChange * factor / 100.0; + if ( projectile.behavior == &actArrow || projectile.behavior == &actMagicMissile ) + { + projectile.yaw = projectileYaw; + } + projectile.vel_x = cos(projectileYaw) * velocity; + projectile.vel_y = sin(projectileYaw) * velocity; + } + else + { + //messagePlayer(0, MESSAGE_DEBUG, "can't see"); + } + hit.entity = ohitentity; + } + } } \ No newline at end of file diff --git a/src/stat.hpp b/src/stat.hpp index 7f7c56507..011c29bed 100644 --- a/src/stat.hpp +++ b/src/stat.hpp @@ -14,7 +14,7 @@ #ifdef USE_FMOD #include #endif - +#include #include "items.hpp" enum Monster : int; @@ -68,7 +68,95 @@ static const int EFF_PWR = 43; static const int EFF_AGILITY = 44; static const int EFF_RALLY = 45; static const int EFF_MARIGOLD = 46; -static const int NUMEFFECTS = 64; +static const int EFF_ENSEMBLE_FLUTE = 47; +static const int EFF_ENSEMBLE_LYRE = 48; +static const int EFF_ENSEMBLE_DRUM = 49; +static const int EFF_ENSEMBLE_LUTE = 50; +static const int EFF_ENSEMBLE_HORN = 51; +static const int EFF_LIFT = 52; +static const int EFF_GUARD_SPIRIT = 53; +static const int EFF_GUARD_BODY = 54; +static const int EFF_DIVINE_GUARD = 55; +static const int EFF_NIMBLENESS = 56; +static const int EFF_GREATER_MIGHT = 57; +static const int EFF_COUNSEL = 58; +static const int EFF_STURDINESS = 59; +static const int EFF_BLESS_FOOD = 60; +static const int EFF_PINPOINT = 61; +static const int EFF_PENANCE = 62; +static const int EFF_SACRED_PATH = 63; +static const int EFF_DETECT_ENEMY = 64; +static const int EFF_BLOOD_WARD = 65; +static const int EFF_TRUE_BLOOD = 66; +static const int EFF_DIVINE_ZEAL = 67; +static const int EFF_MAXIMISE = 68; +static const int EFF_MINIMISE = 69; +static const int EFF_WEAKNESS = 70; +static const int EFF_INCOHERENCE = 71; +static const int EFF_OVERCHARGE = 72; +static const int EFF_ENVENOM_WEAPON = 73; +static const int EFF_MAGIC_GREASE = 74; +static const int EFF_COMMAND = 75; +static const int EFF_MIMIC_VOID = 76; +static const int EFF_CURSE_FLESH = 77; +static const int EFF_NUMBING_BOLT = 78; +static const int EFF_DELAY_PAIN = 79; +static const int EFF_SEEK_CREATURE = 80; +static const int EFF_TABOO = 81; +static const int EFF_COURAGE = 82; +static const int EFF_COWARDICE = 83; +static const int EFF_SPORES = 84; +static const int EFF_ABUNDANCE = 85; +static const int EFF_GREATER_ABUNDANCE = 86; +static const int EFF_PRESERVE = 87; +static const int EFF_MIST_FORM = 88; +static const int EFF_FORCE_SHIELD = 89; +static const int EFF_LIGHTEN_LOAD = 90; +static const int EFF_ATTRACT_ITEMS = 91; +static const int EFF_RETURN_ITEM = 92; +static const int EFF_DEMESNE_DOOR = 93; +static const int EFF_REFLECTOR_SHIELD = 94; +static const int EFF_DIZZY = 95; +static const int EFF_SPIN = 96; +static const int EFF_CRITICAL_SPELL = 97; +static const int EFF_MAGIC_WELL = 98; +static const int EFF_STATIC = 99; +static const int EFF_ABSORB_MAGIC = 100; +static const int EFF_FLAME_CLOAK = 101; +static const int EFF_DUSTED = 102; +static const int EFF_NOISE_VISIBILITY = 103; +static const int EFF_RATION_SPICY = 104; +static const int EFF_RATION_SOUR = 105; +static const int EFF_RATION_BITTER = 106; +static const int EFF_RATION_HEARTY = 107; +static const int EFF_RATION_HERBAL = 108; +static const int EFF_RATION_SWEET = 109; +static const int EFF_GROWTH = 110; +static const int EFF_THORNS = 111; +static const int EFF_BLADEVINES = 112; +static const int EFF_BASTION_MUSHROOM = 113; +static const int EFF_BASTION_ROOTS = 114; +static const int EFF_FOCI_LIGHT_PEACE = 115; +static const int EFF_FOCI_LIGHT_JUSTICE = 116; +static const int EFF_FOCI_LIGHT_PROVIDENCE = 117; +static const int EFF_FOCI_LIGHT_PURITY = 118; +static const int EFF_FOCI_LIGHT_SANCTUARY = 119; +static const int EFF_STASIS = 120; +static const int EFF_HP_MP_REGEN = 121; +static const int EFF_DISRUPTED = 122; +static const int EFF_FROST = 123; +static const int EFF_MAGICIANS_ARMOR = 124; +static const int EFF_PROJECT_SPIRIT = 125; +static const int EFF_DEFY_FLESH = 126; +static const int EFF_PINPOINT_DAMAGE = 127; +static const int EFF_SALAMANDER_HEART = 128; +static const int EFF_DIVINE_FIRE = 129; +static const int EFF_HEALING_WORD = 130; +static const int EFF_HOLY_FIRE = 131; +static const int EFF_SIGIL = 132; +static const int EFF_SANCTUARY = 133; +static const int EFF_DUCKED = 134; +static const int NUMEFFECTS = 160; // stats static const int STAT_STR = 0; @@ -85,10 +173,10 @@ static const int PRO_LOCKPICKING = 0; // base attribute: dex static const int PRO_STEALTH = 1; // base attribute: dex static const int PRO_TRADING = 2; // base attribute: chr static const int PRO_APPRAISAL = 3; // base attribute: per -static const int PRO_SWIMMING = 4; // base attribute: con +static const int PRO_THAUMATURGY = 4; // base attribute: con static const int PRO_LEADERSHIP = 5; // base attribute: chr -static const int PRO_SPELLCASTING = 6; // base attribute: int -static const int PRO_MAGIC = 7; // base attribute: int +static const int PRO_MYSTICISM = 6; // base attribute: int +static const int PRO_SORCERY = 7; // base attribute: int static const int PRO_RANGED = 8; // base attribute: dex static const int PRO_SWORD = 9; // base attribute: str static const int PRO_MACE = 10; // base attribute: str @@ -98,6 +186,9 @@ static const int PRO_SHIELD = 13; // base attribute: con static const int PRO_UNARMED = 14; // base attribute: str static const int PRO_ALCHEMY = 15; // base attribute: int static const int NUMPROFICIENCIES = 16; +static const int PRO_LEGACY_SWIMMING = 32; // for image lookups +static const int PRO_LEGACY_MAGIC = 33; // for image lookups +static const int PRO_LEGACY_SPELLCASTING = 34; // for image lookups //Start levels for the various proficiency ranges. //0 = "none" @@ -131,8 +222,6 @@ static const int CAPSTONE_UNLOCK_LEVEL[NUMPROFICIENCIES] = static const int CAPSTONE_LOCKPICKING_CHEST_GOLD_AMOUNT = 100; -static const int NUMCATEGORIES = 14; - #define ITEM_SLOT_NUMPROPERTIES 7 #define ITEM_SLOT_HELM 0 #define ITEM_SLOT_WEAPON ITEM_SLOT_HELM + ITEM_SLOT_NUMPROPERTIES @@ -215,12 +304,16 @@ enum KilledBy { SINK, FAILED_ALCHEMY, FAILED_CHALLENGE, - BELL + BELL, + MUSHROOM, + LEAVES, + DEATH_KNOCKBACK }; class Stat { Sint32 PROFICIENCIES[NUMPROFICIENCIES]; + Uint8 EFFECTS[NUMEFFECTS]; public: Monster type; sex_t sex; @@ -278,9 +371,43 @@ class Stat PROFICIENCIES[skill] = value; } int getGoldWeight() const; - bool EFFECTS[NUMEFFECTS]; + static constexpr Uint8 nullEffectValue = 0; + const Uint8& getEffectActive(int effect) const + { + if ( effect >= 0 && effect < NUMEFFECTS ) + { + return EFFECTS[effect]; + } + return Stat::nullEffectValue; + } + void clearEffect(int effect) + { + if ( effect >= 0 && effect < NUMEFFECTS ) + { + EFFECTS[effect] = 0; + } + } + void setEffectActive(int effect, Uint8 effectStrength) + { +#ifndef EDITOR + assert(effectStrength > 0); +#endif + if ( effect >= 0 && effect < NUMEFFECTS ) + { + EFFECTS[effect] = std::max(EFFECTS[effect], effectStrength); // strongest value remains + } + } + void setEffectValueUnsafe(int effect, Uint8 effectStrength) + { + if ( effect >= 0 && effect < NUMEFFECTS ) + { + EFFECTS[effect] = effectStrength; + } + } + Uint32 EFFECTS_ACCRETION_TIME[NUMEFFECTS]; Sint32 EFFECTS_TIMERS[NUMEFFECTS]; bool defending; + Uint32 parrying = 0; Sint32& sneaking; // MISC_FLAGS[1] Sint32& allyItemPickup; // MISC_FLAGS[2] Sint32& allyClass; // MISC_FLAGS[3] @@ -336,6 +463,7 @@ class Stat std::vector items; }; std::map player_lootbags; + list_t void_chest_inventory; list_t magic_effects; //Makes things like the invisibility spell work. Stat(Sint32 sprite); ~Stat(); @@ -356,6 +484,7 @@ class Stat }; int getPassiveShieldBonus(bool checkShield, bool excludeSkill) const; int getActiveShieldBonus(bool checkShield, bool excludeSkill, Item* shieldItem = nullptr, bool checkNonShieldBonus = false) const; + static int getParryingACBonus(Stat* myStats, Item* myWeapon, bool checkWeapon, bool excludeSkill, int weaponSkill); std::string getAttribute(std::string key) const { if ( attributes.find(key) != attributes.end() ) @@ -369,10 +498,48 @@ class Stat } void setAttribute(std::string key, std::string value); bool statusEffectRemovedByCureAilment(const int effect, Entity* my); + int numShillelaghDebuffsActive(Entity* my); void addItemToLootingBag(const int player, const real_t x, const real_t y, Item& item); Uint32 getLootingBagKey(const int player); static bool emptyLootingBag(const int player, Uint32 key); static int maxEquipmentBonusToSkill; + enum EnsembleEffectsBonusType + { + ENSEMBLE_FLUTE_EFF_1, + ENSEMBLE_FLUTE_EFF_2, + ENSEMBLE_FLUTE_TIER, + ENSEMBLE_LUTE_EFF_1, + ENSEMBLE_LUTE_EFF_2, + ENSEMBLE_LUTE_TIER, + ENSEMBLE_DRUM_EFF_1, + ENSEMBLE_DRUM_EFF_2, + ENSEMBLE_DRUM_TIER, + ENSEMBLE_HORN_EFF_1, + ENSEMBLE_HORN_EFF_2, + ENSEMBLE_HORN_TIER, + ENSEMBLE_LYRE_EFF_1, + ENSEMBLE_LYRE_EFF_2, + ENSEMBLE_LYRE_TIER, + ENSEMBLE_LYRE_TIER_2 + }; + static const Sint32 kEnsembleBreakPointTier4 = 40; + static const Sint32 kEnsembleBreakPointTier3 = 20; + static const Sint32 kEnsembleBreakPointTier2 = 5; + static const Sint32 kEnsembleBreakPointTier1 = 0; + real_t getEnsembleEffectBonus(EnsembleEffectsBonusType bonusType, int checkEffectStrength = -1); + Sint32 getThaumProficiencySpellStatBonus(int whichStat, Sint32 currentBonus); + static int getMaxAttackCharge(Stat* myStats); + struct MonsterRangedAccuracy + { + Uint32 lastTarget = 0; + real_t accuracy = 0.0; + Uint32 lastTick = 0; + real_t getAccuracy(Uint32 target); + void incrementAccuracy(); + void modifyProjectile(Entity& my, Entity& projectile); + }; + MonsterRangedAccuracy monsterRangedAccuracy; + std::map itemLastDegradeTick; }; extern Stat* stats[MAXPLAYERS]; diff --git a/src/stat_shared.cpp b/src/stat_shared.cpp index 2948acee5..bbe8464fc 100644 --- a/src/stat_shared.cpp +++ b/src/stat_shared.cpp @@ -92,11 +92,12 @@ Stat::Stat(Sint32 sprite) : } if ( c < NUMEFFECTS ) { - this->EFFECTS[c] = false; + this->EFFECTS[c] = 0; } if ( c < NUMEFFECTS ) { this->EFFECTS_TIMERS[c] = 0; + this->EFFECTS_ACCRETION_TIME[c] = 0; } } @@ -130,6 +131,8 @@ Stat::Stat(Sint32 sprite) : this->FOLLOWERS.last = NULL; this->inventory.first = NULL; this->inventory.last = NULL; + this->void_chest_inventory.first = NULL; + this->void_chest_inventory.last = NULL; this->helmet = NULL; this->breastplate = NULL; this->gloves = NULL; @@ -227,11 +230,11 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->LVL = 30; stats->HUNGER = 900; - stats->EFFECTS[EFF_LEVITATING] = true; + stats->setEffectActive(EFF_LEVITATING, 1); stats->EFFECTS_TIMERS[EFF_LEVITATING] = 0; - stats->setProficiency(PRO_MAGIC, 100); - stats->setProficiency(PRO_SPELLCASTING, 100); + stats->setProficiency(PRO_SORCERY, 100); + stats->setProficiency(PRO_MYSTICISM, 100); break; case 62: @@ -358,8 +361,8 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->FOLLOWERS.first = NULL; stats->FOLLOWERS.last = NULL; - stats->setProficiency(PRO_MAGIC, 50); - stats->setProficiency(PRO_SPELLCASTING, 50); + stats->setProficiency(PRO_SORCERY, 50); + stats->setProficiency(PRO_MYSTICISM, 50); stats->setProficiency(PRO_TRADING, 75); stats->setProficiency(PRO_APPRAISAL, 75); @@ -528,8 +531,8 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->EDITOR_ITEMS[ITEM_SLOT_INV_1] = 1; stats->EDITOR_ITEMS[ITEM_SLOT_INV_1 + ITEM_CHANCE] = 50; - stats->setProficiency(PRO_MAGIC, 50); - stats->setProficiency(PRO_SPELLCASTING, 50); + stats->setProficiency(PRO_SORCERY, 50); + stats->setProficiency(PRO_MYSTICISM, 50); break; case 86: @@ -612,7 +615,7 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->setProficiency(PRO_MACE, 75); stats->setProficiency(PRO_POLEARM, 60); stats->setProficiency(PRO_RANGED, 75); - stats->setProficiency(PRO_MAGIC, 100); + stats->setProficiency(PRO_SORCERY, 100); stats->setProficiency(PRO_LEADERSHIP, 60); stats->EDITOR_ITEMS[ITEM_SLOT_INV_1] = 1; @@ -671,8 +674,8 @@ void setDefaultMonsterStats(Stat* stats, int sprite) //stats->setProficiency(PRO_POLEARM, 45); stats->setProficiency(PRO_RANGED, 25); stats->setProficiency(PRO_SHIELD, 25); - stats->setProficiency(PRO_MAGIC, 80); - stats->setProficiency(PRO_SPELLCASTING, 80); + stats->setProficiency(PRO_SORCERY, 80); + stats->setProficiency(PRO_MYSTICISM, 80); stats->EDITOR_ITEMS[ITEM_SLOT_INV_1] = 1; stats->EDITOR_ITEMS[ITEM_SLOT_INV_1 + ITEM_CHANCE] = 10; // doublet @@ -716,8 +719,8 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->setProficiency(PRO_POLEARM, 90); stats->setProficiency(PRO_RANGED, 60); stats->setProficiency(PRO_SHIELD, 25); - stats->setProficiency(PRO_MAGIC, 80); - stats->setProficiency(PRO_SPELLCASTING, 80); + stats->setProficiency(PRO_SORCERY, 80); + stats->setProficiency(PRO_MYSTICISM, 80); stats->EDITOR_ITEMS[ITEM_SLOT_INV_1] = 1; stats->EDITOR_ITEMS[ITEM_SLOT_INV_1 + ITEM_CHANCE] = 5; //Spooky mask @@ -871,8 +874,8 @@ void setDefaultMonsterStats(Stat* stats, int sprite) //stats->setProficiency(PRO_POLEARM, 25); stats->setProficiency(PRO_RANGED, 60); //Chuck booze at you. //stats->setProficiency(PRO_SHIELD, 35); - stats->setProficiency(PRO_SPELLCASTING, 60); - stats->setProficiency(PRO_MAGIC, 60); + stats->setProficiency(PRO_SORCERY, 60); + stats->setProficiency(PRO_MYSTICISM, 60); break; case 93: case (1000 + AUTOMATON): @@ -932,8 +935,8 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->HUNGER = 900; stats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] = 1; stats->setProficiency(PRO_RANGED, 100); - stats->setProficiency(PRO_MAGIC, 100); - stats->setProficiency(PRO_SPELLCASTING, 100); + stats->setProficiency(PRO_SORCERY, 100); + stats->setProficiency(PRO_MYSTICISM, 100); break; case 95: case (1000 + LICH_FIRE): @@ -960,8 +963,8 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->HUNGER = 900; stats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] = 1; stats->setProficiency(PRO_SWORD, 80); - stats->setProficiency(PRO_MAGIC, 100); - stats->setProficiency(PRO_SPELLCASTING, 100); + stats->setProficiency(PRO_SORCERY, 100); + stats->setProficiency(PRO_MYSTICISM, 100); break; case 83: case (1000 + SKELETON): @@ -1169,7 +1172,7 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->GOLD = 0; stats->HUNGER = 900; - stats->setProficiency(PRO_MAGIC, 60); + stats->setProficiency(PRO_SORCERY, 60); stats->setProficiency(PRO_LEADERSHIP, 40); stats->EDITOR_ITEMS[ITEM_SLOT_INV_1] = 1; @@ -1365,6 +1368,380 @@ void setDefaultMonsterStats(Stat* stats, int sprite) stats->EDITOR_ITEMS[ITEM_SLOT_INV_1] = 1; stats->EDITOR_ITEMS[ITEM_SLOT_INV_1 + ITEM_CHANCE] = 33; //Random Items break; + case 204: + case (1000 + DRYAD): + stats->type = DRYAD; + stats->sex = static_cast(local_rng.rand() % 2); + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->HP = 110; + stats->RANDOM_HP = 0; + stats->MAXHP = stats->HP; + stats->RANDOM_MAXHP = 10; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->MP = 30; + stats->MAXMP = 30; + stats->OLDHP = stats->HP; + + stats->STR = 8; + stats->RANDOM_STR = 0; + stats->DEX = 6; + stats->CON = 7; + stats->INT = -2; + stats->PER = 5; + stats->CHR = 5; + stats->EXP = 0; + stats->LVL = 10; + + stats->GOLD = 0; + stats->HUNGER = 900; + stats->setProficiency(PRO_RANGED, 80); + + stats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] = 1; + break; + case 205: + case (1000 + MYCONID): + stats->type = MYCONID; + stats->sex = static_cast(local_rng.rand() % 2); + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->HP = 110; + stats->RANDOM_HP = 0; + stats->MAXHP = stats->HP; + stats->RANDOM_MAXHP = 10; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->MP = 30; + stats->MAXMP = 30; + stats->OLDHP = stats->HP; + + stats->HP = 100; + stats->RANDOM_HP = 20; + stats->MAXHP = stats->HP; + stats->RANDOM_MAXHP = stats->RANDOM_HP; + stats->MP = 30; + stats->MAXMP = 30; + stats->OLDHP = stats->HP; + stats->STR = 10; + stats->DEX = -2; + stats->CON = 5; + stats->INT = -4; + stats->PER = 5; + stats->CHR = -1; + stats->EXP = 0; + stats->LVL = 12; + + stats->GOLD = 0; + stats->HUNGER = 900; + + stats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] = 1; + break; + case 206: + case (1000 + SALAMANDER): + stats->type = SALAMANDER; + stats->sex = static_cast(local_rng.rand() % 2); + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->HP = 150; + stats->RANDOM_HP = 0; + stats->MAXHP = stats->HP; + stats->RANDOM_MAXHP = stats->RANDOM_HP; + stats->MP = 30; + stats->MAXMP = 30; + stats->OLDHP = stats->HP; + stats->STR = 10; + stats->DEX = 6; + stats->CON = 8; + stats->INT = -4; + stats->PER = 5; + stats->CHR = -1; + stats->EXP = 0; + stats->LVL = 16; + stats->GOLD = 0; + stats->HUNGER = 900; + break; + case 207: + case (1000 + GREMLIN): + stats->type = GREMLIN; + stats->sex = static_cast(local_rng.rand() % 2); + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->HP = 80; + stats->MAXHP = stats->HP; + stats->OLDHP = stats->HP; + stats->RANDOM_HP = 0; + stats->RANDOM_MAXHP = stats->RANDOM_HP; + stats->MP = 30; + stats->MAXMP = 30; + stats->OLDHP = stats->HP; + stats->STR = 7; + stats->DEX = 5; + stats->CON = 8; + stats->PER = 5; + stats->INT = 0; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->HUNGER = 900; + + stats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_SHIELD] = 1; + + stats->EDITOR_ITEMS[ITEM_SLOT_HELM] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_ARMOR] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_CLOAK] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_GLOVES] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_BOOTS] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_AMULET] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_RING] = 1; + + stats->EDITOR_ITEMS[ITEM_SLOT_INV_1] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_INV_1 + ITEM_CHANCE] = 100; + stats->EDITOR_ITEMS[ITEM_SLOT_INV_2] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_INV_2 + ITEM_CHANCE] = 100; + stats->EDITOR_ITEMS[ITEM_SLOT_INV_3] = 1; + stats->EDITOR_ITEMS[ITEM_SLOT_INV_3 + ITEM_CHANCE] = 33; + break; + case 246: + case (1000 + REVENANT_SKULL): + stats->type = REVENANT_SKULL; + stats->sex = static_cast(local_rng.rand() % 2); + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->HP = 80; + stats->MAXHP = stats->HP; + stats->OLDHP = stats->HP; + stats->RANDOM_HP = 0; + stats->RANDOM_MAXHP = stats->RANDOM_HP; + stats->MP = 30; + stats->MAXMP = 30; + stats->OLDHP = stats->HP; + stats->STR = 10; + stats->DEX = 3; + stats->CON = 5; + stats->PER = 8; + stats->INT = -1; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->HUNGER = 900; + break; + case (1000 + MINIMIMIC): + stats->type = MINIMIMIC; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 90; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 20; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 0; + stats->DEX = 0; + stats->CON = 0; + stats->PER = 5; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; + case 247: + case (1000 + MONSTER_ADORCISED_WEAPON): + stats->type = MONSTER_ADORCISED_WEAPON; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 150; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 20; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 15; + stats->DEX = 0; + stats->CON = 0; + stats->PER = 5; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + stats->setProficiency(PRO_SWORD, 60); + stats->setProficiency(PRO_AXE, 60); + stats->setProficiency(PRO_POLEARM, 60); + stats->setProficiency(PRO_MACE, 60); + stats->EDITOR_ITEMS[ITEM_SLOT_WEAPON] = 1; + break; + case (1000 + FLAME_ELEMENTAL): + stats->type = FLAME_ELEMENTAL; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 10; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 20; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 0; + stats->DEX = 0; + stats->CON = 0; + stats->PER = 5; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; + case (1000 + HOLOGRAM): + stats->type = HOLOGRAM; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 1; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 0; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 0; + stats->DEX = 0; + stats->CON = 0; + stats->PER = 0; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 1; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; + case (1000 + MOTH_SMALL): + stats->type = MOTH_SMALL; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 100; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 0; + stats->DEX = 0; + stats->CON = 0; + stats->PER = 5; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; + case (1000 + EARTH_ELEMENTAL): + stats->type = EARTH_ELEMENTAL; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 120; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 20; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->MAXMP = 1000; + stats->MP = 0; + stats->STR = 25; + stats->DEX = 3; + stats->CON = 15; + stats->INT = -3; + stats->PER = 5; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 15; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; + case (1000 + DUCK_SMALL): + stats->type = DUCK_SMALL; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 100; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 20; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 0; + stats->DEX = 1; + stats->CON = 0; + stats->PER = 15; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; + case (1000 + MONSTER_UNUSED_6): + stats->type = MONSTER_UNUSED_6; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 10; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 20; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 0; + stats->DEX = 0; + stats->CON = 0; + stats->PER = 5; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; + case (1000 + MONSTER_UNUSED_7): + stats->type = MONSTER_UNUSED_7; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 10; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 20; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 0; + stats->DEX = 0; + stats->CON = 0; + stats->PER = 5; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; + case (1000 + MONSTER_UNUSED_8): + stats->type = MONSTER_UNUSED_8; + stats->stat_appearance = local_rng.rand(); + stats->inventory.first = NULL; + stats->inventory.last = NULL; + stats->MAXHP = 10; + stats->HP = stats->MAXHP; + stats->OLDHP = stats->HP; + stats->RANDOM_MAXHP = 20; + stats->RANDOM_HP = stats->RANDOM_MAXHP; + stats->STR = 0; + stats->DEX = 0; + stats->CON = 0; + stats->PER = 5; + stats->CHR = 0; + stats->EXP = 0; + stats->LVL = 10; + stats->GOLD = 0; + stats->RANDOM_GOLD = 0; + break; case 10: default: break; diff --git a/src/steam.cpp b/src/steam.cpp index 7266bf3db..6b23b0d6a 100644 --- a/src/steam.cpp +++ b/src/steam.cpp @@ -485,6 +485,13 @@ void SteamServerClientWrapper::OnLobbyMemberUpdate(LobbyChatUpdate_t* pCallback) } } } + + int numInLobby = SteamMatchmaking()->GetNumLobbyMembers(*static_cast(currentLobby)); + for ( int i = 0; i < numInLobby; ++i ) + { + CSteamID memberID = SteamMatchmaking()->GetLobbyMemberByIndex(*static_cast(currentLobby), i); + SteamFriends()->SetPlayedWith(memberID); + } } else { @@ -730,6 +737,7 @@ SteamAPICall_t cpp_SteamMatchmaking_CreateLobby(ELobbyType eLobbyType, int cMaxM { SteamMatchmaking()->LeaveLobby(*old_lobby); cpp_Free_CSteamID((void*)old_lobby); + currentLobby = nullptr; } steamAwaitingLobbyCreation = true; SteamAPICall_t steamAPICall = SteamMatchmaking()->CreateLobby(eLobbyType, cMaxMembers); @@ -1041,6 +1049,17 @@ void steamStatisticUpdate(int statisticNum, ESteamStatTypes type, int value) case STEAM_STAT_I_NEEDED_THAT: case STEAM_STAT_DUNGEONSEED: case STEAM_STAT_RUNG_OUT: + case STEAM_STAT_CALL_LOCKSMITH: + case STEAM_STAT_PREMIUM_LOOTBOX: + case STEAM_STAT_WITCHES_BREW: + case STEAM_STAT_HOBBYIST: + case STEAM_STAT_BLESSED_ADDITION: + case STEAM_STAT_THATS_A_WRAP: + case STEAM_STAT_MERCENARY_ARMY: + case STEAM_STAT_COLONIST: + case STEAM_STAT_PRICKLY_PERSONALITY: + case STEAM_STAT_BOOM_DYNAMITE: + case STEAM_STAT_DOESNT_COUNT: g_SteamStats[statisticNum].m_iValue = std::min(g_SteamStats[statisticNum].m_iValue, steamStatAchStringsAndMaxVals[statisticNum].second); break; @@ -1095,6 +1114,49 @@ void steamStatisticUpdate(int statisticNum, ESteamStatTypes type, int value) indicateProgress = true; } break; + case STEAM_STAT_SOURCE_ENGINE: + case STEAM_STAT_TOUCHE: + indicateProgress = false; + g_SteamStats[statisticNum].m_iValue = + std::min(g_SteamStats[statisticNum].m_iValue, steamStatAchStringsAndMaxVals[statisticNum].second); + if ( g_SteamStats[statisticNum].m_iValue == steamStatAchStringsAndMaxVals[statisticNum].second ) + { + indicateProgress = true; + } + else if ( oldValue == 0 && g_SteamStats[statisticNum].m_iValue > 0 ) + { + indicateProgress = true; + } + else if ( oldValue < 100 && ((oldValue / 100) < (g_SteamStats[statisticNum].m_iValue / 100)) ) + { + indicateProgress = true; + } + else if ( ((oldValue / 200) < (g_SteamStats[statisticNum].m_iValue / 200)) ) + { + indicateProgress = true; + } + break; + case STEAM_STAT_PAY_TO_WIN: + indicateProgress = false; + g_SteamStats[statisticNum].m_iValue = + std::min(g_SteamStats[statisticNum].m_iValue, steamStatAchStringsAndMaxVals[statisticNum].second); + if ( g_SteamStats[statisticNum].m_iValue == steamStatAchStringsAndMaxVals[statisticNum].second ) + { + indicateProgress = true; + } + else if ( oldValue == 0 && g_SteamStats[statisticNum].m_iValue > 0 ) + { + indicateProgress = true; + } + else if ( oldValue < 100 && ((oldValue / 100) < (g_SteamStats[statisticNum].m_iValue / 100)) ) + { + indicateProgress = true; + } + else if ( ((oldValue / 500) < (g_SteamStats[statisticNum].m_iValue / 500)) ) + { + indicateProgress = true; + } + break; case STEAM_STAT_SUPER_SHREDDER: case STEAM_STAT_SMASH_MELEE: indicateProgress = false; @@ -1142,6 +1204,7 @@ void steamStatisticUpdate(int statisticNum, ESteamStatTypes type, int value) case STEAM_STAT_TRASH_COMPACTOR: case STEAM_STAT_TORCHERER: case STEAM_STAT_FIXER_UPPER: + case STEAM_STAT_LET_HIM_COOK: indicateProgress = false; g_SteamStats[statisticNum].m_iValue = std::min(g_SteamStats[statisticNum].m_iValue, steamStatAchStringsAndMaxVals[statisticNum].second); @@ -1378,6 +1441,13 @@ void steamIndicateStatisticProgress(int statisticNum, ESteamStatTypes type) case STEAM_STAT_ITS_A_LIVING: case STEAM_STAT_CHOPPING_BLOCK: case STEAM_STAT_MANY_PEDI_PALP: + case STEAM_STAT_WITCHES_BREW: + case STEAM_STAT_HOBBYIST: + case STEAM_STAT_BLESSED_ADDITION: + case STEAM_STAT_THATS_A_WRAP: + case STEAM_STAT_BOOM_DYNAMITE: + case STEAM_STAT_COLONIST: + case STEAM_STAT_DOESNT_COUNT: if ( !achievementUnlocked(steamStatAchStringsAndMaxVals[statisticNum].first.c_str()) ) { if ( iVal == 1 || (iVal > 0 && iVal % 5 == 0) ) @@ -1397,6 +1467,10 @@ void steamIndicateStatisticProgress(int statisticNum, ESteamStatTypes type) case STEAM_STAT_RAGE_AGAINST: case STEAM_STAT_GUERILLA_RADIO: case STEAM_STAT_RUNG_OUT: + case STEAM_STAT_CALL_LOCKSMITH: + case STEAM_STAT_PREMIUM_LOOTBOX: + case STEAM_STAT_MERCENARY_ARMY: + case STEAM_STAT_PRICKLY_PERSONALITY: if ( !achievementUnlocked(steamStatAchStringsAndMaxVals[statisticNum].first.c_str()) ) { if ( iVal == 1 || (iVal > 0 && iVal % 4 == 0) ) @@ -1408,6 +1482,8 @@ void steamIndicateStatisticProgress(int statisticNum, ESteamStatTypes type) break; case STEAM_STAT_BAD_BLOOD: case STEAM_STAT_ALTER_EGO: + case STEAM_STAT_PAY_TO_WIN: + case STEAM_STAT_SOURCE_ENGINE: if ( !achievementUnlocked(steamStatAchStringsAndMaxVals[statisticNum].first.c_str()) ) { indicateAchievementProgressAndUnlock(steamStatAchStringsAndMaxVals[statisticNum].first.c_str(), @@ -1421,8 +1497,10 @@ void steamIndicateStatisticProgress(int statisticNum, ESteamStatTypes type) case STEAM_STAT_FIXER_UPPER: case STEAM_STAT_SMASH_MELEE: case STEAM_STAT_PITCH_PERFECT: + case STEAM_STAT_LET_HIM_COOK: // below are 1000 max value case STEAM_STAT_SUPER_SHREDDER: + case STEAM_STAT_TOUCHE: if ( !achievementUnlocked(steamStatAchStringsAndMaxVals[statisticNum].first.c_str()) ) { indicateAchievementProgressAndUnlock(steamStatAchStringsAndMaxVals[statisticNum].first.c_str(), diff --git a/src/ui/Frame.cpp b/src/ui/Frame.cpp index 260b893ad..2dff9fa60 100644 --- a/src/ui/Frame.cpp +++ b/src/ui/Frame.cpp @@ -2530,7 +2530,9 @@ void Frame::drawImage(const image_t* image, const SDL_Rect& _size, const SDL_Rec src.x = std::max((float)image->section.x, image->section.x + (_size.x - pos.x) * (w / (float)image->pos.w)); src.y = std::max((float)image->section.y, image->section.y + (_size.y - pos.y) * (h / (float)image->pos.h)); src.w = ((float)dest.w / pos.w) * w; + src.w = std::max(1, src.w); src.h = ((float)dest.h / pos.h) * h; + src.h = std::max(1, src.h); //src.x += image->section.x - std::min(0, _size.x - pos.x); //src.y += image->section.y - std::min(0, _size.y - pos.y); } diff --git a/src/ui/GameUI.cpp b/src/ui/GameUI.cpp index 3e5eb40d9..7c0ef225f 100644 --- a/src/ui/GameUI.cpp +++ b/src/ui/GameUI.cpp @@ -300,6 +300,8 @@ int GAMEUI_FRAMEDATA_ALCHEMY_RECIPE_SLOT = 3; int GAMEUI_FRAMEDATA_ALCHEMY_RECIPE_ENTRY = 4; int GAMEUI_FRAMEDATA_WORLDTOOLTIP_ITEM = 5; int GAMEUI_FRAMEDATA_SHOP_ITEM = 6; +int GAMEUI_FRAMEDATA_ALCHEMY_MISSING_QTY = 7; +int GAMEUI_FRAMEDATA_SPELL_LEARNABLE = 8; MinotaurWarning_t minotaurWarning[MAXPLAYERS]; @@ -329,6 +331,18 @@ void uppercaseString(std::string& str) } } +void lowercaseString(std::string& str) +{ + if ( str.size() < 1 ) { return; } + for ( auto& letter : str ) + { + if ( letter >= 'A' && letter <= 'Z' ) + { + letter = tolower(letter); + } + } +} + void camelCaseString(std::string& str) { if ( str.size() < 1 ) { return; } @@ -388,6 +402,10 @@ std::string EnemyBarSettings_t::getEnemyBarSpriteName(Entity* entity) { return "door"; } + else if ( entity->behavior == &actIronDoor ) + { + return "iron_door"; + } else if ( entity->behavior == &actChest ) { return "chest"; @@ -507,6 +525,10 @@ struct MPBarPaths_t }*/ return automatonSTBars; } + else if ( stats[player]->type == SALAMANDER || (stats[player]->playerRace == RACE_SALAMANDER && stats[player]->stat_appearance == 0) ) + { + return automatonHTBars; + } else if ( stats[player]->playerRace == RACE_INSECTOID && stats[player]->stat_appearance == 0 ) { return insectoidENBars; @@ -1339,7 +1361,7 @@ Frame* createAllyPlayerFrame(const int player, Frame* baseFrame) } static ConsoleVariable cvar_assist_icon_txt_x("/assist_icon_txt_x", 0); -static ConsoleVariable cvar_assist_icon_txt_y("/assist_icon_txt_y", 8); +static ConsoleVariable cvar_assist_icon_txt_y("/assist_icon_txt_y", 14); Frame* createAllyPlayerEntry(const int player, Frame* baseFrame) { @@ -1469,7 +1491,6 @@ Frame* createAllyPlayerEntry(const int player, Frame* baseFrame) Image* warningImg = nullptr; if ( value > 0 ) { - char buf[32]; if ( value < PingNetworkStatus_t::pingLimitGreen ) { if ( PingNetworkStatus[player].hudDisplayOKTicks == 0 ) @@ -3555,7 +3576,11 @@ Frame* createPauseMenuPlayerBars() frame->setInvisible(false); if ( clientnum < 0 || players[clientnum]->hud.playerBars.size() == 0 - || voice_no_recv & (1 << clientnum) || !voice_any_send ) + || voice_no_recv & (1 << clientnum) || !voice_any_send +#ifdef NINTENDO + || directConnect +#endif + ) { //frame->setOpacity(0.0); //frame->setInheritParentFrameOpacity(false); @@ -5098,6 +5123,7 @@ void Player::HUD_t::updateUINavigation() if ( !GenericGUI[player.playernum].tinkerGUI.bOpen && !GenericGUI[player.playernum].alchemyGUI.bOpen && !GenericGUI[player.playernum].featherGUI.bOpen && !GenericGUI[player.playernum].assistShrineGUI.bOpen + && !GenericGUI[player.playernum].mailboxGUI.bOpen && !GenericGUI[player.playernum].itemfxGUI.bOpen ) { justify = PANEL_JUSTIFY_LEFT; @@ -5153,6 +5179,7 @@ void Player::HUD_t::updateUINavigation() if ( !player.inventoryUI.chestGUI.bOpen && !player.shopGUI.bOpen && !GenericGUI[player.playernum].tinkerGUI.bOpen && !GenericGUI[player.playernum].alchemyGUI.bOpen && !GenericGUI[player.playernum].featherGUI.bOpen + && !GenericGUI[player.playernum].mailboxGUI.bOpen && !GenericGUI[player.playernum].assistShrineGUI.bOpen && !GenericGUI[player.playernum].itemfxGUI.bOpen ) { @@ -6091,6 +6118,7 @@ int StatusEffectQueue_t::getBaseEffectPosY() return statusEffectFrame->getSize().h / 2 - 50; } +static ConsoleVariable cvar_statusfx_spell_size("/statusfx_spell_size", 36); int StatusEffectQueueEntry_t::getEffectSpriteNormalWidth() { if ( effect == StatusEffectQueue_t::kEffectBread @@ -6102,6 +6130,27 @@ int StatusEffectQueueEntry_t::getEffectSpriteNormalWidth() { return 64; } + + if ( effect >= StatusEffectQueue_t::kSpellEffectOffset ) + { + int effectID = effect - StatusEffectQueue_t::kSpellEffectOffset; + if ( StatusEffectQueue_t::StatusEffectDefinitions_t::sustainedSpellDefinitionExists(effectID) ) + { + auto& definition = StatusEffectQueue_t::StatusEffectDefinitions_t::getSustainedSpell(effectID); + if ( definition.useSpellIDForImg >= 0 || definition.useSpellIDForImgVariations.size() > 0 ) + { + return *cvar_statusfx_spell_size; + } + } + } + else if ( StatusEffectQueue_t::StatusEffectDefinitions_t::effectDefinitionExists(effect) ) + { + auto& definition = StatusEffectQueue_t::StatusEffectDefinitions_t::getEffect(effect); + if ( definition.useSpellIDForImg >= 0 || definition.useSpellIDForImgVariations.size() > 0 ) + { + return *cvar_statusfx_spell_size; + } + } return 32; } int StatusEffectQueueEntry_t::getEffectSpriteNormalHeight() @@ -6115,6 +6164,26 @@ int StatusEffectQueueEntry_t::getEffectSpriteNormalHeight() { return 64; } + if ( effect >= StatusEffectQueue_t::kSpellEffectOffset ) + { + int effectID = effect - StatusEffectQueue_t::kSpellEffectOffset; + if ( StatusEffectQueue_t::StatusEffectDefinitions_t::sustainedSpellDefinitionExists(effectID) ) + { + auto& definition = StatusEffectQueue_t::StatusEffectDefinitions_t::getSustainedSpell(effectID); + if ( definition.useSpellIDForImg >= 0 || definition.useSpellIDForImgVariations.size() > 0 ) + { + return *cvar_statusfx_spell_size; + } + } + } + else if ( StatusEffectQueue_t::StatusEffectDefinitions_t::effectDefinitionExists(effect) ) + { + auto& definition = StatusEffectQueue_t::StatusEffectDefinitions_t::getEffect(effect); + if ( definition.useSpellIDForImg >= 0 || definition.useSpellIDForImgVariations.size() > 0 ) + { + return *cvar_statusfx_spell_size; + } + } return 32; } @@ -6261,6 +6330,7 @@ void StatusEffectQueueEntry_t::animateNotification(int player) pos.h = animateStartH + destH * animateH; } +static ConsoleVariable cvar_statusfx_align_text_right("/statusfx_align_text_right", false); void draw_status_effect_numbers_fn(const Widget& widget, SDL_Rect pos) { Frame* frame = (Frame*)&widget; if ( auto parent = frame->getParent() ) @@ -6268,23 +6338,803 @@ void draw_status_effect_numbers_fn(const Widget& widget, SDL_Rect pos) { int player = parent->getOwner(); if ( player >= 0 && player < MAXPLAYERS ) { - if ( stats[player]->MISC_FLAGS[STAT_FLAG_ASSISTANCE_PLAYER_PTS] > 0 ) + if ( stats[player]->MISC_FLAGS[STAT_FLAG_ASSISTANCE_PLAYER_PTS] > 0 + || stats[player]->getEffectActive(EFF_ENSEMBLE_FLUTE) + || stats[player]->getEffectActive(EFF_ENSEMBLE_LUTE) + || stats[player]->getEffectActive(EFF_ENSEMBLE_LYRE) + || stats[player]->getEffectActive(EFF_ENSEMBLE_DRUM) + || stats[player]->getEffectActive(EFF_ENSEMBLE_HORN) + || stats[player]->getEffectActive(EFF_GUARD_SPIRIT) + || stats[player]->getEffectActive(EFF_GUARD_BODY) + || stats[player]->getEffectActive(EFF_DIVINE_GUARD) + || stats[player]->getEffectActive(EFF_DELAY_PAIN) + || stats[player]->getEffectActive(EFF_ABSORB_MAGIC) + || stats[player]->getEffectActive(EFF_STATIC) + || stats[player]->getEffectActive(EFF_MAGICIANS_ARMOR) + || stats[player]->getEffectActive(EFF_SACRED_PATH) + || stats[player]->getEffectActive(EFF_MAGIC_WELL) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_JUSTICE) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_PEACE) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_PROVIDENCE) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_PURITY) + || stats[player]->getEffectActive(EFF_FOCI_LIGHT_SANCTUARY) + || stats[player]->getEffectActive(EFF_NIMBLENESS) + || stats[player]->getEffectActive(EFF_GREATER_MIGHT) + || stats[player]->getEffectActive(EFF_COUNSEL) + || stats[player]->getEffectActive(EFF_STURDINESS) + || stats[player]->getEffectActive(EFF_GROWTH) + || stats[player]->getEffectActive(EFF_SIGIL) + || stats[player]->getEffectActive(EFF_SANCTUARY) + || (cast_animation[player].overcharge > 0 || cast_animation[player].overcharge_init > 0) + || (players[player]->mechanics.gremlinBreakableCounter > 0 && stats[player]->type == GREMLIN) + || players[player]->mechanics.getWealthTier() > 0 ) { for ( auto img : frame->getImages() ) { - if ( !img->disabled && img->path.find("assistance.png") != std::string::npos ) + bool alignRight = *cvar_statusfx_align_text_right; + if ( !img->disabled ) { - if ( auto text = Text::get(std::to_string(stats[player]->MISC_FLAGS[STAT_FLAG_ASSISTANCE_PLAYER_PTS]).c_str(), - "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + if ( img->path.find("assistance.png") != std::string::npos ) { - text->drawColor(SDL_Rect{ 0,0,0,0 }, - SDL_Rect{ pos.x + img->pos.x + img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x, - pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, - 0, 0 }, - SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, - makeColor(255, 255, 255, 255)); + if ( auto text = Text::get(std::to_string(stats[player]->MISC_FLAGS[STAT_FLAG_ASSISTANCE_PLAYER_PTS]).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x, + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("ensemble_flute.png") != std::string::npos ) + { + alignRight = true; + std::string val = "I"; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_ENSEMBLE_FLUTE); + if ( effectStrength >= 1 ) + { + if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier4 ) + { + val = "IV"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier3 ) + { + val = "III"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier2 ) + { + val = "II"; + } + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("ensemble_lute.png") != std::string::npos ) + { + alignRight = true; + std::string val = "I"; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_ENSEMBLE_LUTE); + if ( effectStrength >= 1 ) + { + if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier4 ) + { + val = "IV"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier3 ) + { + val = "III"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier2 ) + { + val = "II"; + } + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("ensemble_lyre.png") != std::string::npos ) + { + alignRight = true; + std::string val = "I"; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_ENSEMBLE_LYRE); + if ( effectStrength >= 1 ) + { + if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier4 ) + { + val = "IV"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier3 ) + { + val = "III"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier2 ) + { + val = "II"; + } + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("ensemble_drum.png") != std::string::npos ) + { + alignRight = true; + std::string val = "I"; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_ENSEMBLE_DRUM); + if ( effectStrength >= 1 ) + { + if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier4 ) + { + val = "IV"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier3 ) + { + val = "III"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier2 ) + { + val = "II"; + } + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("ensemble_horn.png") != std::string::npos ) + { + alignRight = true; + std::string val = "I"; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_ENSEMBLE_HORN); + if ( effectStrength >= 1 ) + { + if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier4 ) + { + val = "IV"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier3 ) + { + val = "III"; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier2 ) + { + val = "II"; + } + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("battle_guard.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_GUARD_BODY); + if ( auto text = Text::get(std::to_string(effectStrength).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("missile_guard.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_DIVINE_GUARD); + if ( auto text = Text::get(std::to_string(effectStrength).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("spell_guard.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_GUARD_SPIRIT); + if ( auto text = Text::get(std::to_string(effectStrength).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("sacred_path.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_SACRED_PATH); + if ( auto text = Text::get(std::to_string(effectStrength).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + else if ( img->path.find("delay_pain.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_DELAY_PAIN); + if ( effectStrength > 1 ) + { + if ( auto text = Text::get(std::to_string(effectStrength - 1).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("asborb_magic.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_ABSORB_MAGIC); + if ( effectStrength > 1 ) + { + if ( auto text = Text::get(std::to_string(effectStrength - 1).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("static.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_STATIC); + if ( effectStrength >= 1 ) + { + if ( auto text = Text::get(std::to_string(effectStrength).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("magicians_armor.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_MAGICIANS_ARMOR); + if ( effectStrength >= 1 ) + { + if ( auto text = Text::get(std::to_string(effectStrength).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("magic_well.png") != std::string::npos ) + { + Uint8 effectStrength = stats[player]->getEffectActive(EFF_MAGIC_WELL); + if ( effectStrength > 1 ) + { + if ( auto text = Text::get(std::to_string(effectStrength - 1).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("symbol_justice.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_FOCI_LIGHT_JUSTICE); + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("symbol_peace.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_FOCI_LIGHT_PEACE); + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("symbol_providence.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_FOCI_LIGHT_PROVIDENCE); + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("symbol_purity.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_FOCI_LIGHT_PURITY); + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("symbol_sanctuary.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_FOCI_LIGHT_SANCTUARY); + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("divine_sanctuary.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_SANCTUARY) & 0xF; + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("sigil.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_SIGIL) & 0xF; + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("greater_might.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_GREATER_MIGHT) & 0xF; + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("counsel.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_COUNSEL) & 0xF; + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("nimbleness.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_NIMBLENESS) & 0xF; + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("sturdiness.png") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_STURDINESS) & 0xF; + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("growth_dryad") != std::string::npos + || img->path.find("growth_myconid") != std::string::npos ) + { + alignRight = true; + Uint8 effectStrength = stats[player]->getEffectActive(EFF_GROWTH); + if ( effectStrength >= 1 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "III"; + } + else if ( effectStrength >= 3 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("vandal.png") != std::string::npos ) + { + alignRight = true; + int effectStrength = players[player]->mechanics.getBreakableCounterTier(); + if ( effectStrength > 0 ) + { + std::string val = "I"; + if ( effectStrength >= 5 ) + { + val = "V"; + } + else if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("wealth") != std::string::npos ) + { + alignRight = true; + int effectStrength = players[player]->mechanics.getWealthTier(); + if ( effectStrength > 0 ) + { + std::string val = "I"; + if ( effectStrength >= 4 ) + { + val = "IV"; + } + else if ( effectStrength >= 3 ) + { + val = "III"; + } + else if ( effectStrength >= 2 ) + { + val = "II"; + } + if ( auto text = Text::get(val.c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } + } + else if ( img->path.find("overcharge.png") != std::string::npos ) + { + int charges = std::max(cast_animation[player].overcharge, cast_animation[player].overcharge_init); + if ( charges > 0 ) + { + if ( auto text = Text::get(std::to_string(charges).c_str(), + "fonts/pixel_maz_multiline.ttf#16#2", 0xFFFFFFFF, 0) ) + { + text->drawColor(SDL_Rect{ 0,0,0,0 }, + SDL_Rect{ pos.x + img->pos.x + (alignRight ? (img->pos.w - (int)text->getWidth()) : (img->pos.w / 2 - (int)text->getWidth() / 2 + *cvar_assist_icon_txt_x)), + pos.y + img->pos.y + img->pos.h / 2 - (int)text->getHeight() / 2 - 3 + *cvar_assist_icon_txt_y, + 0, 0 }, + SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, + makeColor(255, 255, 255, 255)); + } + } } - break; } } } @@ -6983,6 +7833,158 @@ bool StatusEffectQueue_t::doStatusEffectTooltip(StatusEffectQueueEntry_t& entry, } } } + else if ( effectID == EFF_SALAMANDER_HEART ) + { + variation = std::min(3, std::max(0, (int)entry.customVariable - 1)); + } + else if ( effectID == EFF_GROWTH ) + { + variation = std::min(2, std::max(0, (int)entry.customVariable - 2)); + + int tier = variation + 1; + + std::string newHeader = definition.getName(variation).c_str(); + uppercaseString(newHeader); + tooltipHeader->setText(newHeader.c_str()); + + std::string newDesc = ""; + if ( stats[player]->type == DRYAD ) + { + newDesc = definition.getDesc(1).c_str(); + + char buf[128] = ""; + snprintf(buf, sizeof(buf), definition.getDesc(6).c_str(), 10 * tier); // +PWR + newDesc += '\n'; + newDesc += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(7).c_str(), 10 * tier); // +MP RGN + newDesc += '\n'; + newDesc += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(8).c_str(), -5 * tier); // +Movespeed + newDesc += '\n'; + newDesc += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(11).c_str(), 5 * tier); // +Fire dmg + newDesc += '\n'; + newDesc += buf; + } + else if ( stats[player]->type == MYCONID ) + { + newDesc = definition.getDesc(0).c_str(); + newDesc += '\n'; + newDesc += definition.getDesc(2).c_str(); + if ( variation == 2 ) + { + newDesc += '\n'; + newDesc += definition.getDesc(3).c_str(); + } + char buf[128] = ""; + snprintf(buf, sizeof(buf), definition.getDesc(4).c_str(), tier); // +AC + newDesc += '\n'; + newDesc += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(5).c_str(), tier * 5); // +RES + newDesc += '\n'; + newDesc += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(9).c_str(), 20 + 15 * (tier - 1)); // +Poison dmg + newDesc += '\n'; + newDesc += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(10).c_str(), 100 * tier); // +Germinate dmg + newDesc += '\n'; + newDesc += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(8).c_str(), -10 * tier); // +Movespeed + newDesc += '\n'; + newDesc += buf; + } + + tooltipDesc->setText(newDesc.c_str()); + tooltipInnerWidth = definition.tooltipWidth; + } + else if ( effectID == EFF_ENSEMBLE_DRUM + || effectID == EFF_ENSEMBLE_FLUTE + || effectID == EFF_ENSEMBLE_LUTE + || effectID == EFF_ENSEMBLE_HORN + || effectID == EFF_ENSEMBLE_LYRE ) + { + int variation = 0; + if ( Uint8 effectStrength = stats[player]->getEffectActive(effectID) ) + { + if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier4 ) + { + variation = 3; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier3 ) + { + variation = 2; + } + else if ( effectStrength - 1 >= Stat::kEnsembleBreakPointTier2 ) + { + variation = 1; + } + } + + std::string newHeader = definition.getName(variation).c_str(); + uppercaseString(newHeader); + tooltipHeader->setText(newHeader.c_str()); + std::string newDesc = "";// definition.getDesc(variation).c_str(); + char buf[128] = ""; + if ( effectID == EFF_ENSEMBLE_DRUM ) + { + snprintf(buf, sizeof(buf), definition.getDesc(0).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_EFF_1)); + newDesc = buf; + newDesc += '\n'; + snprintf(buf, sizeof(buf), definition.getDesc(1).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_DRUM_TIER)); + newDesc += buf; + } + else if ( effectID == EFF_ENSEMBLE_FLUTE ) + { + snprintf(buf, sizeof(buf), definition.getDesc(0).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_EFF_1)); + newDesc = buf; + newDesc += '\n'; + snprintf(buf, sizeof(buf), definition.getDesc(1).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_FLUTE_TIER)); + newDesc += buf; + } + else if ( effectID == EFF_ENSEMBLE_LUTE ) + { + snprintf(buf, sizeof(buf), definition.getDesc(0).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_EFF_1)); + newDesc = buf; + newDesc += '\n'; + snprintf(buf, sizeof(buf), definition.getDesc(1).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LUTE_TIER)); + newDesc += buf; + } + else if ( effectID == EFF_ENSEMBLE_HORN ) + { + snprintf(buf, sizeof(buf), definition.getDesc(0).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_EFF_1)); + newDesc = buf; + newDesc += '\n'; + snprintf(buf, sizeof(buf), definition.getDesc(1).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_HORN_TIER)); + newDesc += buf; + } + else if ( effectID == EFF_ENSEMBLE_LYRE ) + { + snprintf(buf, sizeof(buf), definition.getDesc(0).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_EFF_1)); + newDesc = buf; + newDesc += '\n'; + snprintf(buf, sizeof(buf), definition.getDesc(1).c_str(), + (int)stats[player]->getEnsembleEffectBonus(Stat::ENSEMBLE_LYRE_TIER)); + newDesc += buf; + } + tooltipDesc->setText(newDesc.c_str()); + tooltipInnerWidth = definition.tooltipWidth; + } else if ( effectID == EFF_VAMPIRICAURA ) { bool sustained = false; @@ -7115,6 +8117,59 @@ bool StatusEffectQueue_t::doStatusEffectTooltip(StatusEffectQueueEntry_t& entry, tooltipDesc->setText(definition.getDesc(variation).c_str()); tooltipInnerWidth = definition.tooltipWidth; } + else if ( effectID == StatusEffectQueue_t::kEffectVandal ) + { + int tier = players[player]->mechanics.getBreakableCounterTier(); + variation = tier; + + std::string newHeader = definition.getName(variation).c_str(); + uppercaseString(newHeader); + tooltipHeader->setText(newHeader.c_str()); + + std::string descStr = definition.getDesc(0); + char buf[128]; + memset(buf, 0, sizeof(buf)); + int dmg = 1 + tier; + int speed = tier * 5; + int castspeed = tier * 5; + + snprintf(buf, sizeof(buf), definition.getDesc(1).c_str(), dmg); + if ( descStr != "" ) { descStr += '\n'; } + descStr += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(2).c_str(), speed); + if ( descStr != "" ) { descStr += '\n'; } + descStr += buf; + + snprintf(buf, sizeof(buf), definition.getDesc(3).c_str(), castspeed); + if ( descStr != "" ) { descStr += '\n'; } + descStr += buf; + + tooltipDesc->setText(descStr.c_str()); + tooltipInnerWidth = definition.tooltipWidth; + } + else if ( effectID == StatusEffectQueue_t::kEffectWealth ) + { + int tier = players[player]->mechanics.getWealthTier(); + variation = tier; + std::string newHeader = definition.getName(variation).c_str(); + uppercaseString(newHeader); + tooltipHeader->setText(newHeader.c_str()); + + + std::string descStr = ""; + char buf[128]; + memset(buf, 0, sizeof(buf)); + snprintf(buf, sizeof(buf), definition.getDesc(0).c_str(), tier * 10); + if ( descStr != "" ) { descStr += '\n'; } + descStr += buf; + + if ( descStr != "" ) { descStr += '\n'; } + descStr += definition.getDesc(1); + + tooltipDesc->setText(descStr.c_str()); + tooltipInnerWidth = definition.tooltipWidth; + } else if ( effectID == StatusEffectQueue_t::kEffectAssistance ) { std::string newHeader = definition.getName(-1).c_str(); @@ -7161,7 +8216,15 @@ bool StatusEffectQueue_t::doStatusEffectTooltip(StatusEffectQueueEntry_t& entry, && effectID != StatusEffectQueue_t::kEffectWantedInShop && effectID != StatusEffectQueue_t::kEffectBountyTarget && effectID != StatusEffectQueue_t::kEffectDisabledHPRegen - && effectID != StatusEffectQueue_t::kEffectAssistance ) + && effectID != StatusEffectQueue_t::kEffectAssistance + && effectID != StatusEffectQueue_t::kEffectVandal + && effectID != StatusEffectQueue_t::kEffectWealth + && !(effectID == EFF_ENSEMBLE_DRUM + || effectID == EFF_ENSEMBLE_FLUTE + || effectID == EFF_ENSEMBLE_LUTE + || effectID == EFF_ENSEMBLE_HORN + || effectID == EFF_ENSEMBLE_LYRE + || effectID == EFF_GROWTH) ) { std::string newHeader = definition.getName(variation).c_str(); uppercaseString(newHeader); @@ -7253,6 +8316,11 @@ const int StatusEffectQueue_t::kEffectBountyTarget = -23; const int StatusEffectQueue_t::kEffectInspiration = -24; const int StatusEffectQueue_t::kEffectRetaliation = -25; const int StatusEffectQueue_t::kEffectAssistance = -26; +const int StatusEffectQueue_t::kEffectStability = -27; +const int StatusEffectQueue_t::kEffectVandal = -28; +const int StatusEffectQueue_t::kEffectOvercharge = -29; +const int StatusEffectQueue_t::kEffectWealth = -30; +const int StatusEffectQueue_t::kEffectEnd = -31; const int StatusEffectQueue_t::kSpellEffectOffset = 10000; Frame* StatusEffectQueue_t::getStatusEffectFrame() @@ -7456,7 +8524,6 @@ void StatusEffectQueue_t::handleNavigation(std::mapgetAbsoluteSize(); - int mouseDetectionPadding = 2; SDL_Rect frameImgPos = frameImg->pos; bool oldDisabled = frameImg->disabled; @@ -7465,6 +8532,7 @@ void StatusEffectQueue_t::handleNavigation(std::mappos.w == 36 ? 0 : 2; size.x += frameImgPos.x - (mouseDetectionPadding); size.y += frameImgPos.y - (mouseDetectionPadding); size.w = frameImgPos.w + (mouseDetectionPadding * 2); @@ -7477,7 +8545,7 @@ void StatusEffectQueue_t::handleNavigation(std::mapcamera_virtualy1(); players[player]->hud.updateCursorAnimation(size.x - 1 + mouseDetectionPadding, size.y - 1 + mouseDetectionPadding, - frameImgPos.w, frameImgPos.h, inputs.getVirtualMouse(player)->draw_cursor); + frameImgPos.w + mouseDetectionPadding * 2, frameImgPos.h + mouseDetectionPadding * 2, inputs.getVirtualMouse(player)->draw_cursor); break; } @@ -7499,7 +8567,7 @@ void StatusEffectQueue_t::updateAllQueuedEffects() effectsEnabled = false; resetQueue(); } - if ( players[player]->ghost.isActive() && !kAllowGhostStatusEffects ) + if ( players[player]->ghost.isActive() && !kAllowGhostStatusEffects && !players[player]->entity ) { effectsEnabled = false; resetQueue(); @@ -7590,13 +8658,13 @@ void StatusEffectQueue_t::updateAllQueuedEffects() else { bool skipAnim = false; - bool effectActive = stats[player]->EFFECTS[i]; + bool effectActive = stats[player]->getEffectActive(i) > 0; if ( i == EFF_LEVITATING && !effectActive ) { - bool tmp = stats[player]->EFFECTS[EFF_FLUTTER]; - stats[player]->EFFECTS[EFF_FLUTTER] = false; + Uint8 tmp = stats[player]->getEffectActive(EFF_FLUTTER); + stats[player]->clearEffect(EFF_FLUTTER); effectActive = isLevitating(stats[player]); - stats[player]->EFFECTS[EFF_FLUTTER] = tmp; + stats[player]->setEffectValueUnsafe(EFF_FLUTTER, tmp); } else if ( i == EFF_MAGICREFLECT && !effectActive ) { @@ -7633,6 +8701,64 @@ void StatusEffectQueue_t::updateAllQueuedEffects() skipAnim = true; effectsToSkipAnim.insert(i); } + else if ( i == EFF_GROWTH ) + { + if ( !(stats[player]->type == MYCONID || stats[player]->type == DRYAD) + || stats[player]->helmet ) + { + effectActive = false; + } + else + { + if ( stats[player]->getEffectActive(i) <= 1 ) + { + effectActive = false; + } + else + { + for ( auto it = effectQueue.rbegin(); it != effectQueue.rend(); ++it ) + { + if ( (*it).effect == EFF_GROWTH ) + { + if ( (*it).customVariable != stats[player]->getEffectActive(i) ) + { + deleteEffect(EFF_GROWTH); + break; + } + } + } + } + } + } + else if ( i == EFF_SALAMANDER_HEART ) + { + if ( !(stats[player]->type == SALAMANDER) ) + { + effectActive = false; + } + else + { + for ( auto it = effectQueue.rbegin(); it != effectQueue.rend(); ++it ) + { + if ( (*it).effect == EFF_SALAMANDER_HEART ) + { + if ( (*it).customVariable != stats[player]->getEffectActive(i) ) + { + if ( stats[player]->getEffectActive(i) == 1 || stats[player]->getEffectActive(i) == 3 ) + { + // don't re-trigger + (*it).customVariable = stats[player]->getEffectActive(i); + } + else + { + deleteEffect(EFF_SALAMANDER_HEART); + } + break; + } + } + } + } + } else if ( i == EFF_BLIND ) { if ( stats[player]->mask && stats[player]->mask->type == TOOL_BLINDFOLD_TELEPATHY ) @@ -7641,6 +8767,15 @@ void StatusEffectQueue_t::updateAllQueuedEffects() effectsToSkipAnim.insert(i); } } + else if ( i == EFF_ENSEMBLE_DRUM + || i == EFF_ENSEMBLE_HORN + || i == EFF_ENSEMBLE_LUTE + || i == EFF_ENSEMBLE_FLUTE + || i == EFF_ENSEMBLE_LYRE ) + { + skipAnim = true; + effectsToSkipAnim.insert(i); + } else if ( i == EFF_DRUNK && stats[player]->type == GOATMAN ) { effectActive = false; @@ -7662,6 +8797,22 @@ void StatusEffectQueue_t::updateAllQueuedEffects() insertEffect(i, -1); } } + else if ( i == EFF_GROWTH ) + { + if ( insertEffect(i, -1) ) + { + effectQueue.back().customVariable = stats[player]->getEffectActive(EFF_GROWTH); + notificationQueue.back().customVariable = stats[player]->getEffectActive(EFF_GROWTH); + } + } + else if ( i == EFF_SALAMANDER_HEART ) + { + if ( insertEffect(i, -1) ) + { + effectQueue.back().customVariable = stats[player]->getEffectActive(EFF_SALAMANDER_HEART); + notificationQueue.back().customVariable = stats[player]->getEffectActive(EFF_SALAMANDER_HEART); + } + } else { insertEffect(i, -1); @@ -7695,7 +8846,7 @@ void StatusEffectQueue_t::updateAllQueuedEffects() bool inshop = false; std::map miscEffects; - for ( int i = kEffectBurning; i >= kEffectAssistance; --i ) + for ( int i = kEffectBurning; i >= kEffectEnd; --i ) { miscEffects[i] = false; } @@ -7720,11 +8871,11 @@ void StatusEffectQueue_t::updateAllQueuedEffects() { bool cursedItemIsBuff = shouldInvertEquipmentBeatitude(stats[player]); if ( ((stats[player]->mask && stats[player]->mask->type == TOOL_BLINDFOLD_FOCUS) - || (stats[player]->type == GOATMAN && stats[player]->EFFECTS[EFF_DRUNK])) ) + || (stats[player]->type == GOATMAN && stats[player]->getEffectActive(EFF_DRUNK))) ) { miscEffects[kEffectFreeAction] = true; } - if ( stats[player]->type == GOATMAN && stats[player]->EFFECTS[EFF_DRUNK] ) + if ( stats[player]->type == GOATMAN && stats[player]->getEffectActive(EFF_DRUNK) ) { miscEffects[kEffectDrunkGoatman] = true; } @@ -7758,6 +8909,35 @@ void StatusEffectQueue_t::updateAllQueuedEffects() { miscEffects[kEffectLesserWarning] = true; } + if ( stats[player]->shoes && stats[player]->shoes->type == CLEAT_BOOTS + && players[player]->entity && players[player]->entity->effectShapeshift == NOTHING ) + { + miscEffects[kEffectStability] = true; + } + if ( stats[player]->type == GREMLIN && players[player]->mechanics.gremlinBreakableCounter >= 0 ) + { + miscEffects[kEffectVandal] = true; + } + if ( players[player]->mechanics.getWealthTier() > 0 ) + { + miscEffects[kEffectWealth] = true; + + for ( auto it = effectQueue.rbegin(); it != effectQueue.rend(); ++it ) + { + if ( (*it).effect == kEffectWealth ) + { + if ( (*it).customVariable != players[player]->mechanics.getWealthTier() ) + { + deleteEffect(kEffectWealth); + break; + } + } + } + } + if ( cast_animation[player].overcharge_init > 0 || cast_animation[player].overcharge > 0 ) + { + miscEffects[kEffectOvercharge] = true; + } if ( stats[player]->breastplate && stats[player]->breastplate->type == VAMPIRE_DOUBLET ) { if ( (svFlags & SV_FLAG_HUNGER) ) @@ -7769,6 +8949,10 @@ void StatusEffectQueue_t::updateAllQueuedEffects() { miscEffects[kEffectResistBurning] = true; } + if ( stats[player]->amulet && stats[player]->amulet->type == AMULET_BURNINGRESIST ) + { + miscEffects[kEffectResistBurning] = true; + } if ( stats[player]->amulet && stats[player]->amulet->type == AMULET_POISONRESISTANCE ) { miscEffects[kEffectResistPoison] = true; @@ -7800,12 +8984,13 @@ void StatusEffectQueue_t::updateAllQueuedEffects() } if ( (stats[player]->ring && stats[player]->ring->type == RING_STRENGTH) || (stats[player]->gloves && stats[player]->gloves->type == GAUNTLETS_STRENGTH) - || stats[player]->EFFECTS[EFF_POTION_STR] ) + || stats[player]->getEffectActive(EFF_GREATER_MIGHT) + || stats[player]->getEffectActive(EFF_POTION_STR) ) { miscEffects[kEffectPush] = true; } if ( (stats[player]->shoes && stats[player]->shoes->type == IRON_BOOTS_WATERWALKING) - || skillCapstoneUnlocked(player, PRO_SWIMMING) ) + /*|| skillCapstoneUnlocked(player, PRO_LEGACY_SWIMMING)*/ ) { miscEffects[kEffectWaterWalking] = true; } @@ -7870,7 +9055,7 @@ void StatusEffectQueue_t::updateAllQueuedEffects() } } - for ( int i = kEffectBurning; i >= kEffectAssistance; --i ) + for ( int i = kEffectBurning; i >= kEffectEnd; --i ) { if ( miscEffects[i] == false ) { @@ -7881,7 +9066,7 @@ void StatusEffectQueue_t::updateAllQueuedEffects() } else { - if ( !players[player]->entity ) + if ( players[player] && !players[player]->entity ) { effectsToSkipAnim.insert(i); } @@ -7891,12 +9076,20 @@ void StatusEffectQueue_t::updateAllQueuedEffects() } if ( effectSet.find(i) == effectSet.end() ) { - insertEffect(i, -1); + bool inserted = insertEffect(i, -1); if ( i == kEffectSneak ) { effectsToSkipAnimThisFrame.push_back(i); } + else if ( i == kEffectWealth ) + { + if ( inserted ) + { + effectQueue.back().customVariable = players[player]->mechanics.getWealthTier(); + notificationQueue.back().customVariable = players[player]->mechanics.getWealthTier(); + } + } } } } @@ -7937,7 +9130,8 @@ void StatusEffectQueue_t::updateAllQueuedEffects() automatonHungerFrame->setDisabled(true); auto automatonFlameImg = automatonHungerFrame->findImage("flame"); - int iconSize = 32; + static ConsoleVariable cvar_statusfx_iconsize("/statusfx_iconsize", 36); + int iconSize = *cvar_statusfx_iconsize; int movex = splitscreen ? 4 : 0; if ( hungerIconActive ) { @@ -8129,6 +9323,22 @@ void StatusEffectQueue_t::updateAllQueuedEffects() { variation = 0; } + else if ( effectID == StatusEffectQueue_t::kEffectVandal ) + { + variation = 0; + } + else if ( effectID == EFF_GROWTH ) + { + variation = std::min(2, std::max(0, (int)notif.customVariable - 2)); + } + else if ( effectID == StatusEffectQueue_t::kEffectWealth ) + { + variation = 0; + } + else if ( effectID == EFF_SALAMANDER_HEART ) + { + variation = std::min(3, std::max(0, (int)notif.customVariable - 1)); + } else if ( effectID == EFF_VAMPIRICAURA ) { bool sustained = false; @@ -8300,7 +9510,7 @@ void StatusEffectQueue_t::updateAllQueuedEffects() if ( bFrameCapturesMouse && !tooltipShowing ) { SDL_Rect size = statusEffectFrame->getAbsoluteSize(); - int mouseDetectionPadding = 2; + int mouseDetectionPadding = frameImg->pos.w == 36 ? 0 : 2; size.x += frameImg->pos.x - (mouseDetectionPadding); size.y += frameImg->pos.y - (mouseDetectionPadding); size.w = frameImg->pos.w + (mouseDetectionPadding * 2); @@ -8318,8 +9528,8 @@ void StatusEffectQueue_t::updateAllQueuedEffects() size.x -= players[player]->camera_virtualx1(); size.y -= players[player]->camera_virtualy1(); - players[player]->hud.updateCursorAnimation(size.x - 1 + mouseDetectionPadding, size.y - 1 + mouseDetectionPadding, - frameImg->pos.w, frameImg->pos.h, inputs.getVirtualMouse(player)->draw_cursor); + players[player]->hud.updateCursorAnimation(size.x - 1, size.y - 1, + frameImg->pos.w + mouseDetectionPadding * 2, frameImg->pos.h + mouseDetectionPadding * 2, inputs.getVirtualMouse(player)->draw_cursor); } } } @@ -8589,6 +9799,11 @@ void StatusEffectQueue_t::updateEntryImage(StatusEffectQueueEntry_t& entry, Fram } img->path = StatusEffectDefinitions_t::getEffectImgPath(StatusEffectDefinitions_t::getEffect(entry.effect), variation); } + else if ( entry.effect == kEffectWealth ) + { + int variation = std::max(0, std::min(3, (int)entry.customVariable - 1)); + img->path = StatusEffectDefinitions_t::getEffectImgPath(StatusEffectDefinitions_t::getEffect(entry.effect), variation); + } else { if ( entry.effect >= kSpellEffectOffset ) @@ -8636,6 +9851,11 @@ void StatusEffectQueue_t::updateEntryImage(StatusEffectQueueEntry_t& entry, Fram img->path = StatusEffectDefinitions_t::getEffectImgPath(StatusEffectDefinitions_t::getEffect(effectID), variation); } } + else if ( effectID == EFF_SALAMANDER_HEART ) + { + variation = std::min(3, std::max(0, (int)entry.customVariable - 1)); + img->path = StatusEffectDefinitions_t::getEffectImgPath(StatusEffectDefinitions_t::getEffect(effectID), variation); + } else { img->path = StatusEffectDefinitions_t::getEffectImgPath(StatusEffectDefinitions_t::getEffect(effectID), variation); @@ -8648,6 +9868,14 @@ void StatusEffectQueue_t::updateEntryImage(StatusEffectQueueEntry_t& entry, Fram dest.x = entry.pos.x; dest.y = entry.pos.y; img->pos = dest; + if ( img->pos.w < 36 ) + { + img->pos.x += (36 - img->pos.w) / 2; + } + if ( img->pos.h < 36 ) + { + img->pos.y += (36 - img->pos.h) / 2; + } img->disabled = false; if ( img->path.size() > 1 && img->path[0] != '*' ) @@ -9159,6 +10387,28 @@ void createWorldTooltipPrompts(const int player) 0xFFFFFFFF, "images/system/white.png", "glyph img 4"); glyphAdditional3->disabled = true; + { + auto glyphSpellTarget = worldTooltipFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "images/system/white.png", "glyph img spell"); + glyphSpellTarget->disabled = true; + auto glyphSpellCancel = worldTooltipFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "images/system/white.png", "glyph img spell cancel"); + glyphSpellCancel->disabled = true; + auto iconSpellTarget = worldTooltipFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, + 0xFFFFFFFF, "images/system/white.png", "spell icon img"); + iconSpellTarget->disabled = true; + auto textSpellTarget = worldTooltipFrame->addField("prompt spell target text", 256); + textSpellTarget->setFont(promptFont); + textSpellTarget->setText(""); + textSpellTarget->setDisabled(true); + textSpellTarget->setSize(SDL_Rect{ 0, 0, 0, 0 }); + auto textSpellCancelTarget = worldTooltipFrame->addField("prompt spell cancel text", 256); + textSpellCancelTarget->setFont(promptFont); + textSpellCancelTarget->setText(""); + textSpellCancelTarget->setDisabled(true); + textSpellCancelTarget->setSize(SDL_Rect{ 0, 0, 0, 0 }); + } + auto cursor = worldTooltipFrame->addImage(SDL_Rect{ 0, 0, 0, 0 }, 0xFFFFFFFF, "images/system/white.png", "cursor img"); cursor->disabled = true; @@ -9255,6 +10505,17 @@ void Player::HUD_t::updateWorldTooltipPrompts() auto glyphCallout = worldTooltipFrame->findImage("glyph img 4"); glyphCallout->disabled = true; + auto textSpellTarget = worldTooltipFrame->findField("prompt spell target text"); + textSpellTarget->setDisabled(true); + auto textSpellCancelTarget = worldTooltipFrame->findField("prompt spell cancel text"); + textSpellCancelTarget->setDisabled(true); + auto glyphSpellTarget = worldTooltipFrame->findImage("glyph img spell"); + glyphSpellTarget->disabled = true; + auto iconSpellTarget = worldTooltipFrame->findImage("spell icon img"); + iconSpellTarget->disabled = true; + auto glyphSpellCancel = worldTooltipFrame->findImage("glyph img spell cancel"); + glyphSpellCancel->disabled = true; + SDL_Rect textPos{ 0, 0, 0, 0 }; const int skillIconToGlyphPadding = 4; const int nominalGlyphHeight = 26; @@ -9262,7 +10523,15 @@ void Player::HUD_t::updateWorldTooltipPrompts() bool usingTinkeringKit = false; bool useBracketsReticle = false; bool useSneakingReticle = false; - if ( player.entity && stats[player.playernum] ) { + + if ( player.ghost.isActive() ) + { + if ( player.ghost.my->skill[3] == 1 ) + { + useSneakingReticle = true; + } + } + else if ( player.entity && stats[player.playernum] ) { if ( stats[player.playernum]->defending ) { auto shield = stats[player.playernum]->shield; if ( shield && shield->type == TOOL_TINKERING_KIT ) { @@ -9275,14 +10544,15 @@ void Player::HUD_t::updateWorldTooltipPrompts() useSneakingReticle = true; } } - else if ( player.ghost.isActive() ) + + bool spellTarget = cast_animation[player.playernum].spellWaitingAttackInput(); + if ( spellTarget ) { - if ( player.ghost.my->skill[3] == 1 ) + if ( cast_animation[player.playernum].rangefinder == SpellRangefinderType::RANGEFINDER_NONE ) { - useSneakingReticle = true; + spellTarget = false; } } - bool followerInteract = followerMenu.selectMoveTo && (followerMenu.optionSelected == ALLY_CMD_MOVETO_SELECT || followerMenu.optionSelected == ALLY_CMD_ATTACK_SELECT); bool calloutInteract = calloutMenu.selectMoveTo && (calloutMenu.optionSelected == CalloutRadialMenu::CALLOUT_CMD_SELECT); @@ -9547,7 +10817,12 @@ void Player::HUD_t::updateWorldTooltipPrompts() if ( followerMenu.followerToCommand ) { int type = followerMenu.followerToCommand->getMonsterTypeFromSprite(); - if ( followerMenu.allowedInteractItems(type) + if ( followerMenu.attackCommandOnly(type) ) + { + text->setDisabled(false); + text->setText(Language::get(4042)); // "Attack..." + } + else if ( followerMenu.allowedInteractItems(type) || followerMenu.allowedInteractFood(type) || followerMenu.allowedInteractWorld(type) ) @@ -9599,6 +10874,292 @@ void Player::HUD_t::updateWorldTooltipPrompts() } } } + else if ( spellTarget ) + { + cursor->path = getCrosshairPath(); + Entity* target = uidToEntity(cast_animation[player.playernum].targetUid); + if ( target ) + { + cursor->path = "images/system/selectedcursor.png"; + } + if ( auto imgGet = Image::get(cursor->path.c_str()) ) + { + cursor->disabled = false; + promptPos.x -= (int)imgGet->getWidth() / 2; + promptPos.y -= (int)imgGet->getHeight() / 2; + SDL_Rect cursorPos{ 0, 0, (int)imgGet->getWidth(), (int)imgGet->getHeight() }; + cursor->pos = cursorPos; + cursor->color = makeColor(255, 255, 255, 255 * playerSettings[multiplayer ? 0 : player.playernum].shootmodeCrosshairOpacity / 100.f); + } + + textPos.x = cursor->pos.x + cursor->pos.w / 2; + textPos.y = cursor->pos.y + cursor->pos.h / 2; + if ( auto imgGet = Image::get("images/system/selectedcursor.png") ) + { + textPos.x -= (int)imgGet->getWidth() / 2; + textPos.y -= (int)imgGet->getHeight() / 2; + } + + textPos.x += 40; + textPos.y += 20; + const char* inputstr = "Attack"; + if ( inputs.hasController(player.playernum) ) + { + inputstr = "Cast Spell"; + } + auto glyphPathPressed = Input::inputs[player.playernum].getGlyphPathForBinding(inputstr, true); + auto glyphPathUnpressed = Input::inputs[player.playernum].getGlyphPathForBinding(inputstr, false); + if ( ticks % 50 < 25 ) + { + glyphSpellTarget->path = glyphPathPressed; + if ( auto imgGet = Image::get(glyphSpellTarget->path.c_str()) ) + { + glyphSpellTarget->disabled = false; + SDL_Rect glyphPos{ textPos.x, textPos.y, (int)imgGet->getWidth(), (int)imgGet->getHeight() }; + glyphSpellTarget->pos = glyphPos; + if ( auto imgGetUnpressed = Image::get(glyphPathUnpressed.c_str()) ) + { + const int unpressedHeight = imgGetUnpressed->getHeight(); + if ( unpressedHeight != glyphSpellTarget->pos.h ) + { + glyphSpellTarget->pos.y -= (glyphSpellTarget->pos.h - unpressedHeight); + } + + if ( unpressedHeight != nominalGlyphHeight ) + { + glyphSpellTarget->pos.y -= (unpressedHeight - nominalGlyphHeight) / 2; + } + } + textPos.x += glyphSpellTarget->pos.w; + } + } + else + { + glyphSpellTarget->path = glyphPathUnpressed; + if ( auto imgGet = Image::get(glyphSpellTarget->path.c_str()) ) + { + glyphSpellTarget->disabled = false; + SDL_Rect glyphPos{ textPos.x, textPos.y, (int)imgGet->getWidth(), (int)imgGet->getHeight() }; + glyphSpellTarget->pos = glyphPos; + textPos.x += glyphSpellTarget->pos.w; + + if ( glyphSpellTarget->pos.h != nominalGlyphHeight ) + { + glyphSpellTarget->pos.y -= (glyphSpellTarget->pos.h - nominalGlyphHeight) / 2; + } + } + } + textPos.x += skillIconToGlyphPadding; + + if ( true || target ) + { + char buf[128] = ""; + snprintf(buf, sizeof(buf), "#*images/ui/HUD/Casting%02d.png", ticks % 50 < 25 ? 1 : 2); + iconSpellTarget->path = buf; + + if ( auto imgGet = Image::get(iconSpellTarget->path.c_str()) ) + { + iconSpellTarget->disabled = false; + SDL_Rect iconPos{ textPos.x, textPos.y, (int)imgGet->getWidth(), (int)imgGet->getHeight() }; + iconSpellTarget->pos = iconPos; + textPos.x += iconSpellTarget->pos.w + skillIconToGlyphPadding; + } + } + + textSpellTarget->setDisabled(false); + /*if ( cast_animation[player.playernum].rangefinder == SpellRangefinderType::RANGEFINDER_NONE ) + { + textSpellTarget->setDisabled(true); + }*/ + if ( target ) + { + std::string interactText = Language::get(6500); + if ( target->behavior == &actMonster ) + { + if ( target->isInertMimic() ) + { + interactText += Language::get(675); + } + else + { + int monsterType = target->getMonsterTypeFromSprite(); + interactText += getMonsterLocalizedName((Monster)monsterType).c_str(); + } + } + else if ( target->isDamageableCollider() ) + { + interactText += Language::get(target->getColliderLangName()); + } + else if ( target->behavior == &actDoor ) + { + interactText += Language::get(674); + } + else if ( target->behavior == &actIronDoor ) + { + interactText += Language::get(6414); + } + else if ( target->behavior == &::actFurniture ) + { + switch ( target->furnitureType ) + { + case FURNITURE_CHAIR: + interactText += Language::get(677); + break; + case FURNITURE_TABLE: + interactText += Language::get(676); + break; + case FURNITURE_BED: + interactText += Language::get(2505); + break; + case FURNITURE_BUNKBED: + interactText += Language::get(2506); + break; + case FURNITURE_PODIUM: + interactText += Language::get(2507); + break; + default: + break; + } + } + else if ( target->behavior == &actChest ) + { + interactText += Language::get(675); + } + else if ( target->behavior == &actSpearTrap ) + { + interactText += Language::get(4350); + } + else if ( target->behavior == &actArrowTrap ) + { + interactText += Language::get(4351); + } + else if ( target->behavior == &actDoorFrame ) + { + interactText += Language::get(6693); + } + else if ( target->behavior == &actMagicTrap || target->behavior == &actMagicTrapCeiling ) + { + interactText += Language::get(4352); + } + else if ( target->behavior == &actPlayer || target->behavior == &actDeathGhost ) + { + int playernum = target->skill[2]; + if ( playernum >= 0 && playernum < MAXPLAYERS ) + { + char shortname[32]; + stringCopy(shortname, stats[playernum]->name, sizeof(shortname), 22); + std::string nameStr = shortname; + nameStr = messageSanitizePercentSign(nameStr, nullptr); + interactText += nameStr; + } + } + else + { + std::string txt = CalloutMenu[player.playernum].interactText; + auto prevCmd = CalloutMenu[player.playernum].optionSelected; + CalloutMenu[player.playernum].optionSelected = CalloutRadialMenu::CALLOUT_CMD_SELECT; + if ( CalloutMenu[player.playernum].allowedInteractEntity(*target) ) + { + std::string str = CalloutMenu[player.playernum].interactText; + if ( str.length() > strlen(Language::get(4347)) ) + { + str.erase(str.begin(), str.begin() + strlen(Language::get(4347))); + } + interactText += str; + } + strcpy(CalloutMenu[player.playernum].interactText, txt.c_str()); + CalloutMenu[player.playernum].optionSelected = prevCmd; + } + textSpellTarget->setText(interactText.c_str()); + } + else + { + textSpellTarget->setText(Language::get(6499)); + } + + textPos.w = textSpellTarget->getTextObject()->getWidth(); + textPos.h = Font::get(textSpellTarget->getFont())->height() + 8; + textPos.y -= 1; + textSpellTarget->setVJustify(Field::justify_t::CENTER); + textSpellTarget->setSize(textPos); + + if ( false ) + { + auto glyphPathUnpressed = Input::inputs[player.playernum].getGlyphPathForBinding("Cast Spell", false); + auto glyphPathPressed = Input::inputs[player.playernum].getGlyphPathForBinding("Cast Spell", true); + auto prevGlyphPos = glyphSpellTarget->pos; + if ( ticks % 50 < 25 ) + { + glyphSpellCancel->path = glyphPathPressed; + if ( auto imgGet = Image::get(glyphSpellCancel->path.c_str()) ) + { + glyphSpellCancel->disabled = false; + glyphSpellCancel->pos = SDL_Rect{ 0, 0, (int)imgGet->getWidth(), (int)imgGet->getHeight() }; + glyphSpellCancel->pos.y = prevGlyphPos.y + prevGlyphPos.h + 4; + glyphSpellCancel->pos.x = prevGlyphPos.x + prevGlyphPos.w / 2 - glyphSpellCancel->pos.w / 2; + if ( glyphSpellCancel->pos.x % 2 == 1 ) + { + ++glyphSpellCancel->pos.x; + } + if ( auto imgGetUnpressed = Image::get(glyphPathUnpressed.c_str()) ) + { + const int unpressedHeight = imgGetUnpressed->getHeight(); + if ( unpressedHeight != glyphSpellCancel->pos.h ) + { + glyphSpellCancel->pos.y -= (glyphSpellCancel->pos.h - unpressedHeight); + } + } + textPos.x += prevGlyphPos.w; + } + } + else + { + glyphSpellCancel->path = glyphPathUnpressed; + if ( auto imgGet = Image::get(glyphSpellCancel->path.c_str()) ) + { + glyphSpellCancel->disabled = false; + glyphSpellCancel->pos = SDL_Rect{ 0, 0, (int)imgGet->getWidth(), (int)imgGet->getHeight() }; + glyphSpellCancel->pos.y = prevGlyphPos.y + prevGlyphPos.h + 4; + glyphSpellCancel->pos.x = prevGlyphPos.x + prevGlyphPos.w / 2 - glyphSpellCancel->pos.w / 2; + if ( glyphSpellCancel->pos.x % 2 == 1 ) + { + ++glyphSpellCancel->pos.x; + } + } + } + + if ( !glyphSpellCancel->disabled ) + { + glyphSpellCancel->color = makeColor(255, 255, 255, 255); + textSpellCancelTarget->setText(Language::get(6501)); + textSpellCancelTarget->setColor(makeColor(255, 255, 255, 255)); + SDL_Rect textPos = textSpellCancelTarget->getSize(); + textPos.x = glyphSpellCancel->pos.x + glyphSpellCancel->pos.w + 4; + textPos.y = glyphSpellCancel->pos.y + 2; + textPos.h = textSpellTarget->getSize().h; + + if ( auto imgGet = Image::get(glyphPathPressed.c_str()) ) + { + if ( imgGet->getHeight() != glyphSpellCancel->pos.h ) + { + textPos.y += (glyphSpellCancel->pos.h - imgGet->getHeight()) / 2; + } + } + if ( glyphSpellCancel->pos.h != nominalGlyphHeight ) + { + textPos.y += (glyphSpellCancel->pos.h - nominalGlyphHeight) / 2; + } + textPos.w = textSpellCancelTarget->getTextObject()->getWidth(); + textSpellCancelTarget->setSize(textPos); + textSpellCancelTarget->setDisabled(false); + + promptPos.w = std::max(textPos.x + textPos.w, promptPos.w); + promptPos.h = std::max(textPos.y + textPos.h, promptPos.h); + promptPos.w = std::max(glyphSpellCancel->pos.x + glyphSpellCancel->pos.w, promptPos.w); + promptPos.h = std::max(glyphSpellCancel->pos.y + glyphSpellCancel->pos.h, promptPos.h); + } + } + } else { if ( !player.entity && !player.ghost.isActive() ) @@ -9821,9 +11382,9 @@ void Player::HUD_t::updateWorldTooltipPrompts() } else { - textPos.x = cursor->pos.x + 15; - textPos.y = cursor->pos.y + 11; - + textPos.x += 40; + textPos.y += 20; + text->setDisabled(false); text->setText(Language::get(3663)); } @@ -9831,6 +11392,21 @@ void Player::HUD_t::updateWorldTooltipPrompts() } } + if ( !textSpellTarget->isDisabled() ) + { + promptPos.w = std::max(textSpellTarget->getSize().x + textSpellTarget->getSize().w, promptPos.w); + promptPos.h = std::max(textSpellTarget->getSize().y + textSpellTarget->getSize().h, promptPos.h); + } + if ( !glyphSpellTarget->disabled ) + { + promptPos.w = std::max(glyphSpellTarget->pos.x + glyphSpellTarget->pos.w, promptPos.w); + promptPos.h = std::max(glyphSpellTarget->pos.y + glyphSpellTarget->pos.h, promptPos.h); + } + if ( !iconSpellTarget->disabled ) + { + promptPos.w = std::max(iconSpellTarget->pos.x + iconSpellTarget->pos.w, promptPos.w); + promptPos.h = std::max(iconSpellTarget->pos.y + iconSpellTarget->pos.h, promptPos.h); + } if ( !text->isDisabled() ) { textPos.w = text->getTextObject()->getWidth(); @@ -9839,8 +11415,8 @@ void Player::HUD_t::updateWorldTooltipPrompts() text->setVJustify(Field::justify_t::CENTER); text->setSize(textPos); - promptPos.w = text->getSize().x + text->getSize().w; - promptPos.h = text->getSize().y + text->getSize().h; + promptPos.w = std::max(text->getSize().x + text->getSize().w, promptPos.w); + promptPos.h = std::max(text->getSize().y + text->getSize().h, promptPos.h); } if ( !glyph->disabled ) { @@ -10528,7 +12104,7 @@ void Player::HUD_t::updateActionPrompts() promptText->getTextColor(), promptText->getOutlineColor()); textPos.w = textGetLongestLine->getWidth(); - textPos.h = promptText->getNumTextLines() * Font::get(promptText->getFont())->height(); + textPos.h = promptText->getNumTextLines() * Font::get(promptText->getFont())->height() + 4; textPos.x = promptPos.x + promptPos.w / 2 - textPos.w / 2; //textPos.y = promptPos.y + imgBacking->pos.y - textPos.h - 4; -- top aligned textPos.y = promptPos.y + imgBacking->pos.y + imgBacking->pos.h + 1; @@ -10570,39 +12146,86 @@ void Player::HUD_t::updateActionPrompts() imgBacking->path = actionPromptBackingIconPath00; } } + else if ( skillForPrompt == PRO_LEGACY_MAGIC || skillForPrompt == PRO_LEGACY_SPELLCASTING ) + { + imgBacking->path = actionPromptBackingIconPath00; + for ( auto& skill : player.skillSheet.skillSheetData.skillEntries ) + { + if ( skill.skillId == skillForPrompt ) + { + skillImg = skill.skillIconPath32px; + break; + } + } + } else if ( ghostPrompts ) { imgBacking->path = actionPromptBackingIconPath00; switch ( promptInfo.promptType ) { case ACTION_PROMPT_MAGIC: - skillImg = "*images/ui/HUD/HUD_Ghost_Haunt.png"; + if ( player.ghost.isSpiritGhost() ) + { + skillImg = "*images/ui/HUD/HUD_Ghost_Spirit_Return.png"; + } + else + { + skillImg = "*images/ui/HUD/HUD_Ghost_Haunt.png"; + } img->pos.x -= 2; img->pos.y -= 2; img->pos.w = 36; img->pos.h = 36; break; case ACTION_PROMPT_MAINHAND: - skillImg = "*images/ui/HUD/HUD_Ghost_Chill.png"; + if ( player.ghost.isSpiritGhost() ) + { + skillImg = "*images/ui/HUD/HUD_Ghost_Spirit_Attack.png"; + } + else + { + skillImg = "*images/ui/HUD/HUD_Ghost_Chill.png"; + } img->pos.x -= 2; img->pos.y -= 2; img->pos.w = 36; img->pos.h = 36; break; case ACTION_PROMPT_OFFHAND: - skillImg = "*images/ui/HUD/HUD_Ghost_Push.png"; + if ( player.ghost.isSpiritGhost() ) + { + skillImg = "*images/ui/HUD/HUD_Ghost_Spirit_Push.png"; + } + else + { + skillImg = "*images/ui/HUD/HUD_Ghost_Push.png"; + } img->pos.x -= 2; img->pos.y -= 2; img->pos.w = 36; img->pos.h = 36; break; case ACTION_PROMPT_SNEAK: - for ( auto& skill : player.skillSheet.skillSheetData.skillEntries ) + if ( player.ghost.isSpiritGhost() ) { - if ( skill.skillId == PRO_STEALTH ) + if ( player.ghost.my && player.ghost.my->skill[11] == 0 ) // low profile { - skillImg = skill.skillIconPath32px; - break; + skillImg = "*images/ui/HUD/HUD_Ghost_Spirit_LowProfile.png"; + } + else + { + skillImg = "*images/ui/HUD/HUD_Ghost_Spirit_HighProfile.png"; + } + } + else + { + for ( auto& skill : player.skillSheet.skillSheetData.skillEntries ) + { + if ( skill.skillId == PRO_STEALTH ) + { + skillImg = skill.skillIconPath32px; + break; + } } } break; @@ -10893,6 +12516,18 @@ void Player::HUD_t::processHUD() hudFrame->setOwner(player.playernum); hudFrame->setDrawCallback([](const Widget& widget, SDL_Rect rect) { HUDDrawGameEndHint(widget.getOwner(), rect); + + //if ( keystatus[SDLK_c] ) + //{ + // // debug stuff + // if ( auto tex = AOEIndicators_t::getTexture(AOEIndicators_t::uids - 1) ) + // { + // Image::draw(tex->texid, tex->w, tex->h, nullptr, SDL_Rect{ + // Frame::virtualScreenX / 2 - 128 * 2, Frame::virtualScreenY / 2 - 128 * 2, 128 * 4, 128 * 4 + // }, + // SDL_Rect{ 0, 0, Frame::virtualScreenX, Frame::virtualScreenY }, 0xffffffff); + // } + //} }); } @@ -14494,9 +16129,9 @@ void Player::GUIDropdown_t::process() { chest_inventory = &chestInv[player.playernum]; } - else if ( openedChest[player.playernum]->children.first && openedChest[player.playernum]->children.first->element ) + else if ( openedChest[player.playernum] ) { - chest_inventory = (list_t*)openedChest[player.playernum]->children.first->element; + chest_inventory = openedChest[player.playernum]->getChestInventoryList(); } } @@ -15461,13 +17096,14 @@ bool getAttackTooltipLines(int playernum, AttackHoverText_t& attackHoverTextInfo { if ( skill.skillId == attackHoverTextInfo.proficiency ) { - skillName = skill.name; + skillName = skill.getSkillName(); skillLVL = stats[playernum]->getModifiedProficiency(attackHoverTextInfo.proficiency); break; } } - if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_MELEE_WEAPON ) + if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_MELEE_WEAPON + || attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_RAPIER ) { switch ( lineNumber ) { @@ -15485,7 +17121,14 @@ bool getAttackTooltipLines(int playernum, AttackHoverText_t& attackHoverTextInfo attackHoverTextInfo.attackMinRange, attackHoverTextInfo.attackMaxRange); return true; case 3: - snprintf(titleBuf, 127, "%s", Player::CharacterSheet_t::getHoverTextString("attributes_atk_entry_attr_bonus_melee").c_str()); + if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_RAPIER ) + { + snprintf(titleBuf, 127, "%s", Player::CharacterSheet_t::getHoverTextString("attributes_atk_entry_attr_bonus_ranged").c_str()); + } + else + { + snprintf(titleBuf, 127, "%s", Player::CharacterSheet_t::getHoverTextString("attributes_atk_entry_attr_bonus_melee").c_str()); + } snprintf(valueBuf, 127, Player::CharacterSheet_t::getHoverTextString("attributes_atk_bonus_format").c_str(), attackHoverTextInfo.mainAttributeBonus); @@ -15637,6 +17280,7 @@ bool getAttackTooltipLines(int playernum, AttackHoverText_t& attackHoverTextInfo } } else if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN + || attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_MISC || attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_GEM ) { switch ( lineNumber ) @@ -15654,9 +17298,16 @@ bool getAttackTooltipLines(int playernum, AttackHoverText_t& attackHoverTextInfo return true; case 3: snprintf(titleBuf, 127, "%s", Player::CharacterSheet_t::getHoverTextString("attributes_atk_entry_attr_bonus_ranged").c_str()); - snprintf(valueBuf, 127, - Player::CharacterSheet_t::getHoverTextString("attributes_atk_bonus_format").c_str(), - attackHoverTextInfo.mainAttributeBonus); + if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_MISC ) + { + snprintf(valueBuf, 127, "-"); + } + else + { + snprintf(valueBuf, 127, + Player::CharacterSheet_t::getHoverTextString("attributes_atk_bonus_format").c_str(), + attackHoverTextInfo.mainAttributeBonus); + } return true; case 4: snprintf(titleBuf, 127, "%s", Player::CharacterSheet_t::getHoverTextString("attributes_atk_entry_weapon_bonus").c_str()); @@ -15666,9 +17317,16 @@ bool getAttackTooltipLines(int playernum, AttackHoverText_t& attackHoverTextInfo return true; case 5: snprintf(titleBuf, 127, "%s", Player::CharacterSheet_t::getHoverTextString("attributes_atk_entry_skill_bonus").c_str()); - snprintf(valueBuf, 127, - Player::CharacterSheet_t::getHoverTextString("attributes_atk_bonus_format").c_str(), - attackHoverTextInfo.proficiencyBonus); + if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_MISC ) + { + snprintf(valueBuf, 127, "-"); + } + else + { + snprintf(valueBuf, 127, + Player::CharacterSheet_t::getHoverTextString("attributes_atk_bonus_format").c_str(), + attackHoverTextInfo.proficiencyBonus); + } return true; case 6: snprintf(titleBuf, 127, "%s", Player::CharacterSheet_t::getHoverTextString("attributes_atk_entry_thrown_weapon_fully_charged").c_str()); @@ -15678,9 +17336,16 @@ bool getAttackTooltipLines(int playernum, AttackHoverText_t& attackHoverTextInfo return true; case 7: snprintf(titleBuf, 127, "%s", Player::CharacterSheet_t::getHoverTextString("attributes_atk_entry_thrown_weapon_base").c_str()); - snprintf(valueBuf, 127, - Player::CharacterSheet_t::getHoverTextString("attributes_atk_bonus_format").c_str(), - BASE_THROWN_DAMAGE); + if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_MISC ) + { + snprintf(valueBuf, 127, "-"); + } + else + { + snprintf(valueBuf, 127, + Player::CharacterSheet_t::getHoverTextString("attributes_atk_bonus_format").c_str(), + BASE_THROWN_DAMAGE); + } return true; default: return false; @@ -15783,7 +17448,7 @@ bool getAttackTooltipLines(int playernum, AttackHoverText_t& attackHoverTextInfo return false; } -real_t getDisplayedHPRegen(Entity* my, Stat& myStats, Uint32* outColor, char buf[32]) +real_t getDisplayedHPRegen(Entity* my, Stat& myStats, Uint32* outColor, char buf[32], bool excludeItemsEffectsBonus = false) { real_t regen = 0.0; if ( outColor ) @@ -15793,7 +17458,7 @@ real_t getDisplayedHPRegen(Entity* my, Stat& myStats, Uint32* outColor, char buf if ( myStats.HP > 0 ) { regen = (static_cast(Entity::getHealthRegenInterval(my, - myStats, true)) / TICKS_PER_SECOND); + myStats, true, excludeItemsEffectsBonus)) / TICKS_PER_SECOND); /*if ( myStats.type == SKELETON ) { if ( !(svFlags & SV_FLAG_HUNGER) ) @@ -15819,11 +17484,16 @@ real_t getDisplayedHPRegen(Entity* my, Stat& myStats, Uint32* outColor, char buf } } } - else if ( regen < HEAL_TIME / TICKS_PER_SECOND ) + else { if ( outColor ) { - *outColor = hudColors.characterSheetGreen; + real_t regenWithoutItems = (static_cast(Entity::getHealthRegenInterval(my, + myStats, true, true)) / TICKS_PER_SECOND); + if ( regen < (regenWithoutItems - 0.001) ) + { + *outColor = hudColors.characterSheetGreen; + } } } } @@ -15851,14 +17521,14 @@ real_t getDisplayedHPRegen(Entity* my, Stat& myStats, Uint32* outColor, char buf return regen * 100.0; } -real_t getDisplayedMPRegen(Entity* my, Stat& myStats, Uint32* outColor, char buf[32]) +real_t getDisplayedMPRegen(Entity* my, Stat& myStats, Uint32* outColor, char buf[32], bool excludeItemsEffectsBonus = false) { real_t regen = 0.0; bool isNegative = false; bool isInsectoid = false; if ( /*players[player.playernum]->entity*/ true ) { - regen = (static_cast(Entity::getManaRegenInterval(my, myStats, true)) / TICKS_PER_SECOND); + regen = (static_cast(Entity::getManaRegenInterval(my, myStats, true, excludeItemsEffectsBonus)) / TICKS_PER_SECOND); if ( myStats.type == AUTOMATON ) { if ( myStats.HUNGER <= 300 ) @@ -15923,11 +17593,18 @@ real_t getDisplayedMPRegen(Entity* my, Stat& myStats, Uint32* outColor, char buf } } } - else if ( regen < static_cast(getBaseManaRegen(my, myStats)) / TICKS_PER_SECOND ) + else { - if ( outColor ) + int baseRegen = static_cast(getBaseManaRegen(my, myStats)); + real_t regenPerMinute = 60 * TICKS_PER_SECOND / (real_t)(baseRegen); + const int regenTicks = TICKS_PER_SECOND * 60 / regenPerMinute; + real_t compareRegen = regenTicks / (real_t)TICKS_PER_SECOND; + if ( regen < (compareRegen - 0.001) ) { - *outColor = hudColors.characterSheetGreen; + if ( outColor ) + { + *outColor = hudColors.characterSheetGreen; + } } } } @@ -16023,6 +17700,8 @@ struct CharacterSheetTooltipCache_t std::string entry10 = ""; std::string entry11 = ""; std::string entry12 = ""; + std::string entry13 = ""; + std::string entry14 = ""; }; TextEntries_t textEntries[Player::CharacterSheet_t::SHEET_ENUM_END]; bool needsUpdate(const int player) @@ -16103,6 +17782,90 @@ struct CharacterSheetTooltipCache_t weapontype = getWeaponSkill(stats[player]->weapon); } }; + +void characterSheetTooltipSetZeroStat(Entity* entity, Stat* myStats, Sint32* LVL, Sint32 oldStats[NUMSTATS]) +{ + if ( !myStats ) { return; } + if ( LVL ) + { + *LVL = myStats->LVL; + myStats->LVL = 1; + } + if ( oldStats ) + { + for ( int i = 0; i < NUMSTATS; ++i ) + { + switch ( i ) + { + case STAT_STR: + oldStats[i] = myStats->STR; + myStats->STR += -statGetSTR(myStats, entity); + break; + case STAT_DEX: + oldStats[i] = myStats->DEX; + myStats->DEX += -statGetDEX(myStats, entity); + break; + case STAT_CON: + oldStats[i] = myStats->CON; + myStats->CON += -statGetCON(myStats, entity); + break; + case STAT_INT: + oldStats[i] = myStats->INT; + myStats->INT += -statGetINT(myStats, entity); + break; + case STAT_PER: + oldStats[i] = myStats->PER; + myStats->PER += -statGetPER(myStats, entity); + break; + case STAT_CHR: + oldStats[i] = myStats->CHR; + myStats->CHR += -statGetCHR(myStats, entity); + break; + default: + break; + } + } + } +} + +void characterSheetTooltipRestoreStats(Stat* myStats, Sint32* LVL, Sint32 oldStats[NUMSTATS]) +{ + if ( !myStats ) { return; } + if ( LVL ) + { + myStats->LVL = *LVL; + } + if ( oldStats ) + { + for ( int i = 0; i < NUMSTATS; ++i ) + { + switch ( i ) + { + case STAT_STR: + myStats->STR = oldStats[i]; + break; + case STAT_DEX: + myStats->DEX = oldStats[i]; + break; + case STAT_CON: + myStats->CON = oldStats[i]; + break; + case STAT_INT: + myStats->INT = oldStats[i]; + break; + case STAT_PER: + myStats->PER = oldStats[i]; + break; + case STAT_CHR: + myStats->CHR = oldStats[i]; + break; + default: + break; + } + } + } +} + bool blitCharacterSheetTooltipToSurf = false; CharacterSheetTooltipCache_t charsheetTooltipCache[MAXPLAYERS]; void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element, SDL_Rect pos, Player::PanelJustify_t tooltipJustify) @@ -16598,13 +18361,13 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element case SHEET_STR: { Sint32 STR = statGetSTR(stats[player.playernum], players[player.playernum]->entity); - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_atk_value_format").c_str(), STR); + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_atk_percent_format").c_str(), STR * 100 * Entity::PlayerAttackMeleeStatFactor); } break; case SHEET_DEX: { Sint32 DEX = statGetDEX(stats[player.playernum], players[player.playernum]->entity); - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_atk_value_format").c_str(), DEX); + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_atk_percent_format").c_str(), DEX * 100 * Entity::PlayerAttackRangedStatFactor); } break; case SHEET_CON: @@ -16616,14 +18379,23 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element case SHEET_INT: { //real_t val = getBonusFromCasterOfSpellElement(players[player.playernum]->entity, stats[player.playernum], nullptr, SPELL_NONE) * 100.0; - real_t bonus = getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum]); + /*if ( auto spell = player.magic.selectedSpell() ) + { + real_t bonus = getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum], spell->skillID); + real_t val = bonus * 100.0; + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_pwr_value_format").c_str(), val); + } + else + { + }*/ + real_t bonus = getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum], NUMPROFICIENCIES); real_t val = bonus * 100.0; snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_pwr_value_format").c_str(), val); } break; case SHEET_PER: { - real_t val = std::min(std::max(statGetPER(stats[player.playernum], players[player.playernum]->entity) / 2, 0), 50); + real_t val = std::min(std::max(statGetPER(stats[player.playernum], players[player.playernum]->entity), 0), 50); snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_pierce_value_format").c_str(), val); } break; @@ -16672,7 +18444,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element valueSizes[3] = std::make_pair(entryValue, backingFramePos); txtValueBackingFrame->setDisabled(false); } - if ( element == SHEET_STR || element == SHEET_DEX || element == SHEET_INT || element == SHEET_PER || element == SHEET_CHR ) + if ( element == SHEET_STR || element == SHEET_DEX || element == SHEET_INT || element == SHEET_PER || element == SHEET_CHR || element == SHEET_CON ) { // stat extra number display currentHeight += padyMid; @@ -16688,9 +18460,14 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element { snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_dex_thrown_atk_bonus").c_str()); } + else if ( element == SHEET_CON ) + { + snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_con_pwr_bonus").c_str()); + } else if ( element == SHEET_INT ) { - snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_int_mp_regen_bonus").c_str()); + //snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_int_mp_regen_bonus").c_str()); + snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_int_pwr_bonus2").c_str()); } else if ( element == SHEET_PER ) { @@ -16744,30 +18521,40 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element case SHEET_DEX: { Sint32 DEX = statGetDEX(stats[player.playernum], players[player.playernum]->entity); - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_atk_value_format").c_str(), DEX / 4); + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_atk_percent_format").c_str(), (DEX / 4) * 100 * Entity::PlayerAttackThrownStatFactor); } break; case SHEET_CON: + { + real_t bonus = getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum], PRO_THAUMATURGY); + bonus -= getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum], NUMPROFICIENCIES); + real_t val = bonus * 100.0; + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_pwr_value_format").c_str(), val); break; + } case SHEET_INT: { - Sint32 oldINT = stats[player.playernum]->INT; - stats[player.playernum]->INT += -statGetINT(stats[player.playernum], player.entity); - real_t regenWithoutINT = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); - stats[player.playernum]->INT = oldINT; - - real_t regenTotal = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); - real_t regenStatSkill = regenTotal - regenWithoutINT; - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_mp_regen_value_format").c_str(), regenStatSkill); - } + //Sint32 oldINT = stats[player.playernum]->INT; + //stats[player.playernum]->INT += -statGetINT(stats[player.playernum], player.entity); + //real_t regenWithoutINT = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); + //stats[player.playernum]->INT = oldINT; + // + //real_t regenTotal = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); + //real_t regenStatSkill = regenTotal - regenWithoutINT; + //snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_mp_regen_value_format").c_str(), regenStatSkill); + real_t bonus = getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum], PRO_SORCERY); + bonus -= getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum], NUMPROFICIENCIES); + real_t val = bonus * 100.0; + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_pwr_value_format").c_str(), val); break; + } case SHEET_PER: { const int PER = statGetPER(stats[player.playernum], players[player.playernum]->entity); const int range_bonus = std::min(std::max(0, PER / 5), 2); snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_light_value_format").c_str(), range_bonus); - } break; + } case SHEET_CHR: { real_t val = (50 + stats[player.playernum]->getModifiedProficiency(PRO_TRADING)) / 150.f; // sell value @@ -16784,8 +18571,8 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element stats[player.playernum]->CHR = stat; snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_selling_value_format").c_str(), (normalVal - zeroVal) * 100.0); - } break; + } default: break; } @@ -16813,7 +18600,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element valueSizes[4] = std::make_pair(entryValue, backingFramePos); txtValueBackingFrame->setDisabled(false); } - if ( element == SHEET_PER || element == SHEET_DEX ) + if ( element == SHEET_PER || element == SHEET_DEX || element == SHEET_CHR || element == SHEET_INT ) { // stat extra number display currentHeight += padyMid; @@ -16828,6 +18615,14 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element { snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_per_sneaking_bonus").c_str()); } + else if ( element == SHEET_INT ) + { + snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_int_res_bonus").c_str()); + } + else if ( element == SHEET_CHR ) + { + snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_chr_pwr_bonus").c_str()); + } entry->setText(buf); entry->setVJustify(Field::justify_t::TOP); @@ -16874,7 +18669,11 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element case SHEET_CON: break; case SHEET_INT: + { + Sint32 INT = std::max(0, std::min(90, statGetINT(stats[player.playernum], players[player.playernum]->entity))); + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_int_res_value_format").c_str(), INT); break; + } case SHEET_PER: { const int PER = statGetPER(stats[player.playernum], players[player.playernum]->entity); @@ -16884,7 +18683,13 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element } break; case SHEET_CHR: + { + real_t bonus = getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum], PRO_MYSTICISM); + bonus -= getSpellBonusFromCasterINT(players[player.playernum]->entity, stats[player.playernum], NUMPROFICIENCIES); + real_t val = bonus * 100.0; + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_pwr_value_format").c_str(), val); break; + } default: break; } @@ -16912,6 +18717,83 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element valueSizes[5] = std::make_pair(entryValue, backingFramePos); txtValueBackingFrame->setDisabled(false); } + if ( element == SHEET_CHR ) + { + // stat extra number display + currentHeight += padyMid; + auto entry = characterSheetTooltipTextFields[player.playernum][11]; assert(entry); + entry->setDisabled(false); + char buf[128] = ""; + if ( element == SHEET_CHR ) + { + snprintf(buf, sizeof(buf), "%s", getHoverTextString("stat_chr_restore_bonus").c_str()); + } + entry->setText(buf); + entry->setVJustify(Field::justify_t::TOP); + + SDL_Rect entryPos = entry->getSize(); + entryPos.x = padx + padxMid; + entryPos.y = currentHeight; + entryPos.w = txtPos.w - (padxMid * 2); + entry->setSize(entryPos); + if ( charsheetTooltipCache[player.playernum].textEntries[element].entry11 != entry->getText() ) + { + entry->reflowTextToFit(0); + charsheetTooltipCache[player.playernum].textEntries[element].entry11 = entry->getText(); + } + entryPos.h = actualFont->height(true) * entry->getNumTextLines() + extraTextHeightForLowerCharacters; + entry->setSize(entryPos); + entry->setColor(defaultColor); + currentHeight = std::max(entryPos.y + entryPos.h - extraTextHeightForLowerCharacters, 0); + tooltipPos.h = pady1 + currentHeight + pady2; + + auto entryValue = characterSheetTooltipTextFields[player.playernum][12]; assert(entry); + entryValue->setDisabled(false); + char valueBuf[128] = ""; + int value = 0; + switch ( element ) + { + case SHEET_STR: + break; + case SHEET_CON: + break; + case SHEET_INT: + break; + case SHEET_PER: + break; + case SHEET_CHR: + { + int val = Entity::getMPRestoreOnLevelUp(player.entity, stats[player.playernum], MP_MOD, true); + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("stat_chr_restore_format").c_str(), val); + break; + } + default: + break; + } + entryValue->setColor(hudColors.characterSheetNeutral); + if ( value < 0 ) + { + entryValue->setColor(hudColors.characterSheetRed); + } + else if ( value > 0 ) + { + entryValue->setColor(hudColors.characterSheetGreen); + } + entryValue->setText(valueBuf); + entryValue->setSize(entry->getSize()); + entryValue->setHJustify(Frame::justify_t::LEFT); + entryValue->setVJustify(Field::justify_t::TOP); + + auto txtValueBackingFrame = characterSheetTooltipTextBackingFrames[player.playernum][6]; + SDL_Rect backingFramePos = entryValue->getSize(); + auto txtValueGet = Text::get(entryValue->getText(), entryValue->getFont(), + entryValue->getTextColor(), entryValue->getOutlineColor()); + longestValue = std::max(longestValue, txtValueGet->getWidth()); + backingFramePos.x = backingFramePos.x + backingFramePos.w; + backingFramePos.h = actualFont->height(true) + extraTextHeightForLowerCharacters - 2; + valueSizes[6] = std::make_pair(entryValue, backingFramePos); + txtValueBackingFrame->setDisabled(false); + } for ( int index = 1; index <= NUM_CHARSHEET_TOOLTIP_BACKING_FRAMES; ++index ) { @@ -16954,7 +18836,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element currentHeight += padyMid; - auto entry = characterSheetTooltipTextFields[player.playernum][11]; assert(entry); + auto entry = characterSheetTooltipTextFields[player.playernum][13]; assert(entry); entry->setDisabled(false); char buf[512] = ""; @@ -16976,10 +18858,10 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element entryPos.y = currentHeight; entryPos.w = txtPos.w; entry->setSize(entryPos); - if ( charsheetTooltipCache[player.playernum].textEntries[element].entry11 != entry->getText() ) + if ( charsheetTooltipCache[player.playernum].textEntries[element].entry13 != entry->getText() ) { entry->reflowTextToFit(0); - charsheetTooltipCache[player.playernum].textEntries[element].entry11 = entry->getText(); + charsheetTooltipCache[player.playernum].textEntries[element].entry13 = entry->getText(); } entryPos.h = actualFont->height(true) * entry->getNumTextLines() + extraTextHeightForLowerCharacters; entry->setSize(entryPos); @@ -17068,7 +18950,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element { if ( skill.skillId == attackHoverTextInfo.proficiency ) { - skillName = skill.name; + skillName = skill.getSkillName(); break; } } @@ -17101,6 +18983,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element } } if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_MELEE_WEAPON + || attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_RAPIER || attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_WHIP ) { snprintf(descBuf, sizeof(descBuf), getHoverTextString("attributes_atk_melee_desc").c_str(), skillName.c_str()); @@ -17121,6 +19004,11 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element snprintf(descBuf, sizeof(descBuf), getHoverTextString("attributes_atk_thrown_desc").c_str(), skillName.c_str()); descText = descBuf; } + else if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_MISC ) + { + snprintf(descBuf, sizeof(descBuf), getHoverTextString("attributes_atk_thrown_desc").c_str(), skillName.c_str()); + descText = descBuf; + } else if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_GEM ) { snprintf(descBuf, sizeof(descBuf), getHoverTextString("attributes_atk_gem_desc").c_str(), skillName.c_str()); @@ -17163,6 +19051,19 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element break; case SHEET_POW: titleText = getHoverTextString("attributes_pwr_title"); + if ( auto spell = player.magic.selectedSpell() ) + { + for ( auto& skill : player.skillSheet.skillSheetData.skillEntries ) + { + if ( skill.skillId == spell->skillID ) + { + char buf[128]; + snprintf(buf, sizeof(buf), getHoverTextString("attributes_pwr_title_skill").c_str(), skill.getSkillName().c_str()); + titleText = buf; + break; + } + } + } descText = getHoverTextString("attributes_pwr_desc"); break; case SHEET_RES: @@ -17272,7 +19173,15 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_pwr_base").c_str()); std::string tag = "MAGIC_SPELLPOWER_TOTAL"; std::string formatValue = "%d"; - std::string pwrBonus = formatSkillSheetEffects(player.playernum, PRO_MAGIC, tag, formatValue); + std::string pwrBonus = ""; + if ( auto spell = player.magic.selectedSpell() ) + { + pwrBonus = formatSkillSheetEffects(player.playernum, spell->skillID, tag, formatValue); + } + else + { + pwrBonus = formatSkillSheetEffects(player.playernum, NUMPROFICIENCIES, tag, formatValue); + } Sint32 pwr = 100 + std::stoi(pwrBonus); snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_pwr_nobonus_format").c_str(), pwr); } @@ -17281,7 +19190,6 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element { snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_res_base").c_str()); real_t resistance = 100.0 * Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC); - resistance /= (Entity::getMagicResistance(stats[player.playernum]) + 1); resistance = 100.0 - resistance; snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_res_nobonus_format").c_str(), (int)resistance); } @@ -17395,6 +19303,18 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element glyphBacking->path = actionPromptBackingIconPath00; } } + else if ( attackHoverTextInfo.proficiency == PRO_LEGACY_MAGIC || attackHoverTextInfo.proficiency == PRO_LEGACY_SPELLCASTING ) + { + glyphBacking->path = actionPromptBackingIconPath00; + for ( auto& skill : player.skillSheet.skillSheetData.skillEntries ) + { + if ( skill.skillId == attackHoverTextInfo.proficiency ) + { + glyphIcon->path = skill.skillIconPath; + break; + } + } + } break; case SHEET_AC: glyphIcon->path = getHoverTextString("icon_ac_path"); @@ -17477,6 +19397,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element if ( element == SHEET_ATK ) { if ( attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN + || attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_MISC || attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_THROWN_GEM || attackHoverTextInfo.hoverType == AttackHoverText_t::ATK_HOVER_TYPE_PICKAXE ) { @@ -17571,7 +19492,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element { if ( skill.skillId == PRO_SHIELD ) { - skillName = skill.name; + skillName = skill.getSkillName(); skillLVL = stats[player.playernum]->getModifiedProficiency(skill.skillId); break; } @@ -17591,7 +19512,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_pwr_spellbook").c_str()); std::string tag = "MAGIC_SPELLPOWER_INT"; std::string formatValue = "%d"; - std::string pwrBonus = formatSkillSheetEffects(player.playernum, PRO_MAGIC, tag, formatValue); + std::string pwrBonus = formatSkillSheetEffects(player.playernum, NUMPROFICIENCIES, tag, formatValue); Sint32 pwr = std::stoi(pwrBonus) / 2; snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_pwr_bonus_format").c_str(), pwr); } @@ -17599,7 +19520,8 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element case SHEET_RES: { snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_res_sources").c_str()); - int sources = Entity::getMagicResistance(stats[player.playernum]); + int sources = 0; + Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC, nullptr, &sources); snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_res_sources_format").c_str(), sources); } break; @@ -17867,7 +19789,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element capitalizeString(race); snprintf(buf, sizeof(buf), getHoverTextString("attributes_rgn_base_value").c_str(), race.c_str()); - real_t regen = 100.0; + /*real_t regen = 100.0; if ( !(svFlags & SV_FLAG_HUNGER) ) { regen = 0.0; @@ -17875,8 +19797,21 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element if ( type == SKELETON ) { regen = 25.0; - } - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), regen); + }*/ + + Sint32 oldLVL = 0; + Sint32 oldStats[NUMSTATS]; + //characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], &oldLVL, oldStats); + //real_t baseRegen = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + //characterSheetTooltipRestoreStats(stats[player.playernum], &oldLVL, oldStats); + + characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], nullptr, oldStats); + real_t regenWithoutStats = getDisplayedHPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + characterSheetTooltipRestoreStats(stats[player.playernum], nullptr, oldStats); + + real_t displayedValue = regenWithoutStats; + + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), displayedValue); } break; case SHEET_RGN_MP: @@ -17884,7 +19819,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element if ( isAutomatonHTRegen ) { snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_rgn_ht_base_bonus").c_str()); - real_t baseHTModifier = 100.f; + real_t baseHTModifier = 100.0 / (MAGIC_REGEN_AUTOMATON_TIME / (real_t)MAGIC_REGEN_TIME); if ( stats[player.playernum]->HUNGER <= 300 ) { int baseTime = getBaseManaRegen(player.entity, *stats[player.playernum]); @@ -17948,12 +19883,24 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element capitalizeString(race); snprintf(buf, sizeof(buf), getHoverTextString("attributes_rgn_base_value").c_str(), race.c_str()); - real_t regen = 100.0; + /*real_t regen = 100.0; if ( type == SKELETON ) { regen = 25.0; - } - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), regen); + }*/ + + Sint32 oldLVL = 0; + Sint32 oldStats[NUMSTATS]; + //characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], &oldLVL, oldStats); + //real_t baseRegen = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + //characterSheetTooltipRestoreStats(stats[player.playernum], &oldLVL, oldStats); + + characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], nullptr, oldStats); + real_t regenWithoutStats = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + characterSheetTooltipRestoreStats(stats[player.playernum], nullptr, oldStats); + + real_t displayedValue = regenWithoutStats; + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), displayedValue); } } break; @@ -18090,19 +20037,46 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element { snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_pwr_entry_attr_bonus").c_str()); std::string tag = "MAGIC_SPELLPOWER_INT"; - std::string pwrINTBonus = formatSkillSheetEffects(player.playernum, PRO_MAGIC, tag, getHoverTextString("attributes_pwr_bonus_format")); + std::string pwrINTBonus = ""; + if ( auto spell = player.magic.selectedSpell() ) + { + pwrINTBonus = formatSkillSheetEffects(player.playernum, spell->skillID, tag, getHoverTextString("attributes_pwr_bonus_format")); + if ( spell->skillID == PRO_MYSTICISM ) + { + snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_pwr_entry_attr_bonus_mysticism").c_str()); + } + else if ( spell->skillID == PRO_THAUMATURGY ) + { + snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_pwr_entry_attr_bonus_thaumaturgy").c_str()); + } + } + else + { + pwrINTBonus = formatSkillSheetEffects(player.playernum, NUMPROFICIENCIES, tag, getHoverTextString("attributes_pwr_bonus_format")); + } snprintf(valueBuf, sizeof(valueBuf), "%s", pwrINTBonus.c_str()); } break; case SHEET_RES: { - snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_res_entry_items_bonus").c_str()); - Sint32 baseResist = 100 * damagetables[stats[player.playernum]->type][DAMAGE_TABLE_MAGIC]; - baseResist = 100 - baseResist; - real_t resistance = 100.0 * Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC); - resistance /= (Entity::getMagicResistance(stats[player.playernum]) + 1); - resistance = (100.0 - resistance); - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_res_bonus_format").c_str(), (int)resistance - baseResist); + snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_res_entry_int_bonus").c_str()); + + //Sint32 oldStats[NUMSTATS]; + //characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], nullptr, oldStats); + //real_t resistanceNoINT = 100.0 * Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC); + //characterSheetTooltipRestoreStats(stats[player.playernum], nullptr, oldStats); + + //real_t resistance = 100.0 * Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC); + + real_t damageMultiplier = damagetables[stats[player.playernum]->type][DAMAGE_TABLE_MAGIC]; + + real_t resistanceFromINT = std::max(0, std::min(90, statGetINT(stats[player.playernum], player.entity))); + if ( damageMultiplier < 1.0 ) + { + resistanceFromINT *= (1 - std::max(1.0 - damageMultiplier, 0.0)); + } + + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_res_bonus_format").c_str(), (int)resistanceFromINT); } break; case SHEET_RGN: @@ -18120,7 +20094,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element } } } - real_t baseRegen = 100.0; + /*/real_t baseRegen = 100.0; if ( !(svFlags & SV_FLAG_HUNGER) ) { baseRegen = 0.0; @@ -18128,11 +20102,15 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element if ( type == SKELETON ) { baseRegen = 25.0; - } + }*/ snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_rgn_entry_items_bonus").c_str()); - real_t regen = getDisplayedHPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), regen - baseRegen); + + real_t regenWithoutItems = getDisplayedHPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + real_t regenTotal = getDisplayedHPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); + + real_t regenItemsEffects = regenTotal - regenWithoutItems; + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), regenItemsEffects); } break; case SHEET_RGN_MP: @@ -18140,7 +20118,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element if ( isAutomatonHTRegen ) { snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_rgn_entry_items_bonus").c_str()); - real_t baseHTModifier = 100.f; + real_t baseHTModifier = 100.0 / (MAGIC_REGEN_AUTOMATON_TIME / (real_t)MAGIC_REGEN_TIME); if ( stats[player.playernum]->HUNGER <= 300 ) { int baseTime = getBaseManaRegen(player.entity, *stats[player.playernum]); @@ -18202,21 +20180,13 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element } } } - real_t baseRegen = 100.0; - if ( type == SKELETON ) - { - baseRegen = 25.0; - } snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_rgn_entry_items_bonus").c_str()); - Sint32 oldINT = stats[player.playernum]->INT; - stats[player.playernum]->INT += -statGetINT(stats[player.playernum], player.entity); - real_t regenWithoutINT = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); - stats[player.playernum]->INT = oldINT; + + real_t regenWithoutItems = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); real_t regenTotal = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); - real_t regenStatSkill = regenTotal - regenWithoutINT; - real_t regenItemsEffects = regenTotal - regenStatSkill - baseRegen; + real_t regenItemsEffects = regenTotal - regenWithoutItems; snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), regenItemsEffects); } } @@ -18318,7 +20288,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element if ( element == SHEET_ATK && getAttackTooltipLines(player.playernum, attackHoverTextInfo, 5, buf, valueBuf) || (element != SHEET_ATK - && element != SHEET_RES + /*&& element != SHEET_RES */ && !(element == SHEET_RGN && false/*&& !(svFlags & SV_FLAG_HUNGER) && stats[player.playernum]->type != SKELETON*/) && !(element == SHEET_RGN_MP && isInsectoidENRegen && !(svFlags & SV_FLAG_HUNGER)) ) @@ -18357,26 +20327,58 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element { snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_pwr_entry_items_bonus").c_str()); std::string tag = "MAGIC_SPELLPOWER_EQUIPMENT"; - std::string pwrINTBonus = formatSkillSheetEffects(player.playernum, PRO_MAGIC, tag, getHoverTextString("attributes_pwr_bonus_format")); + std::string pwrINTBonus = ""; + if ( auto spell = player.magic.selectedSpell() ) + { + pwrINTBonus = formatSkillSheetEffects(player.playernum, spell->skillID, tag, getHoverTextString("attributes_pwr_bonus_format")); + } + else + { + pwrINTBonus = formatSkillSheetEffects(player.playernum, NUMPROFICIENCIES, tag, getHoverTextString("attributes_pwr_bonus_format")); + } snprintf(valueBuf, sizeof(valueBuf), "%s", pwrINTBonus.c_str()); } break; case SHEET_RES: - break; - case SHEET_RGN: { - snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_rgn_plain_display").c_str()); - real_t regen = (static_cast(Entity::getHealthRegenInterval(player.entity, *stats[player.playernum], true)) / TICKS_PER_SECOND); - if ( regen <= 0.0 ) - { - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_hp_per_second_format_zero").c_str(), - (static_cast(HEAL_TIME) / TICKS_PER_SECOND)); - } - else + snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_res_entry_items_bonus").c_str()); + Sint32 baseResist = 100 * damagetables[stats[player.playernum]->type][DAMAGE_TABLE_MAGIC]; + baseResist = 100 - baseResist; + + //Sint32 oldStats[NUMSTATS]; + //characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], nullptr, oldStats); + //real_t resistanceNoINT = 100.0 * Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC); + //characterSheetTooltipRestoreStats(stats[player.playernum], nullptr, oldStats); + // + //real_t resistanceFromINT = (100.0 - resistance) - (100.0 - resistanceNoINT); + + real_t resistance = 100.0 * Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC); + real_t damageMultiplier = damagetables[stats[player.playernum]->type][DAMAGE_TABLE_MAGIC]; + + real_t resistanceFromINT = std::max(0, std::min(90, statGetINT(stats[player.playernum], player.entity))); + if ( damageMultiplier < 1.0 ) { - snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_hp_per_second_format").c_str(), - regen); + resistanceFromINT *= (1 - std::max(1.0 - damageMultiplier, 0.0)); } + + resistance = (100.0 - resistance) - resistanceFromINT - baseResist; + + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_res_bonus_format").c_str(), (int)resistance); + break; + } + case SHEET_RGN: + { + Sint32 oldStats[NUMSTATS]; + snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_rgn_hp_entry_statskill_bonus").c_str()); + + characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], nullptr, oldStats); + real_t regenWithoutStats = getDisplayedHPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + characterSheetTooltipRestoreStats(stats[player.playernum], nullptr, oldStats); + + real_t regenTotal = getDisplayedHPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + real_t regenStatSkill = regenTotal - regenWithoutStats; + + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), regenStatSkill); } break; case SHEET_RGN_MP: @@ -18421,20 +20423,26 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element } } } - real_t baseRegen = 100.0; + /*real_t baseRegen = 100.0; if ( type == SKELETON ) { baseRegen = 25.0; - } + }*/ + //real_t baseRegen = 100.0; + //Sint32 oldLVL = 0; + //characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], nullptr, oldStats); + //baseRegen = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); + //characterSheetTooltipRestoreStats(stats[player.playernum], nullptr, oldStats); + Sint32 oldStats[NUMSTATS]; snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_rgn_entry_statskill_bonus").c_str()); - Sint32 oldINT = stats[player.playernum]->INT; - stats[player.playernum]->INT += -statGetINT(stats[player.playernum], player.entity); - real_t regenWithoutINT = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); - stats[player.playernum]->INT = oldINT; - real_t regenTotal = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr); - real_t regenStatSkill = regenTotal - regenWithoutINT; + characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], nullptr, oldStats); + real_t regenWithoutStats = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + characterSheetTooltipRestoreStats(stats[player.playernum], nullptr, oldStats); + + real_t regenTotal = getDisplayedMPRegen(player.entity, *stats[player.playernum], nullptr, nullptr, true); + real_t regenStatSkill = regenTotal - regenWithoutStats; snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_bonus_format").c_str(), regenStatSkill); } @@ -18540,7 +20548,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element entryValue->setHJustify(Frame::justify_t::LEFT); entryValue->setVJustify(Field::justify_t::TOP); - if ( element == SHEET_RGN || (element == SHEET_RGN_MP && (isAutomatonHTRegen || isInsectoidENRegen)) ) + if ( /*element == SHEET_RGN ||*/ (element == SHEET_RGN_MP && (isAutomatonHTRegen || isInsectoidENRegen)) ) { // special rule here to ignore size of this long line auto txtValueBackingFrame = characterSheetTooltipTextBackingFrames[player.playernum][currentTextBackingFrameIndex]; @@ -18575,7 +20583,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element } if ( element == SHEET_ATK && getAttackTooltipLines(player.playernum, attackHoverTextInfo, 6, buf, valueBuf) - || (element == SHEET_RGN_MP && !isInsectoidENRegen) || element == SHEET_WGT ) + || (element == SHEET_RGN_MP && !isInsectoidENRegen) || element == SHEET_WGT || element == SHEET_RGN ) { // extra number display - line 6 hasEntryInfoLines = true; @@ -18593,6 +20601,22 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element break; case SHEET_RES: break; + case SHEET_RGN: + { + snprintf(buf, sizeof(buf), "%s", getHoverTextString("attributes_rgn_plain_display").c_str()); + real_t regen = (static_cast(Entity::getHealthRegenInterval(player.entity, *stats[player.playernum], true)) / TICKS_PER_SECOND); + if ( regen <= 0.0 ) + { + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_hp_per_second_format_zero").c_str(), + (static_cast(HEAL_TIME) / TICKS_PER_SECOND)); + } + else + { + snprintf(valueBuf, sizeof(valueBuf), getHoverTextString("attributes_rgn_hp_per_second_format").c_str(), + regen); + } + } + break; case SHEET_RGN_MP: { if ( isAutomatonHTRegen ) @@ -18725,7 +20749,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element entryValue->setHJustify(Frame::justify_t::LEFT); entryValue->setVJustify(Field::justify_t::TOP); - if ( element == SHEET_RGN_MP ) + if ( element == SHEET_RGN_MP || element == SHEET_RGN ) { // special rule here to ignore size of this long line auto txtValueBackingFrame = characterSheetTooltipTextBackingFrames[player.playernum][currentTextBackingFrameIndex]; @@ -18976,7 +21000,7 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element std::string descriptionText = mapDisplayNamesDescriptions[map.name].second.c_str(); std::string mapDetailsText = ""; - auto mapDetails = Player::Minimap_t::mapDetails; + auto& mapDetails = Player::Minimap_t::mapDetails; for ( auto& detail : mapDetails ) { if ( mapDetailsText != "" ) @@ -19143,7 +21167,6 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element } } - char titleBuf[64]; std::string titleText = getHoverTextString("race_title_normal"); if ( players[player.playernum]->entity ) { @@ -19300,7 +21323,6 @@ void Player::CharacterSheet_t::updateCharacterSheetTooltip(SheetElements element } } - char titleBuf[64]; if ( player.entity && player.entity->effectShapeshift != 0 ) { txt->setText(getHoverTextString("class_title_shapeshift").c_str()); @@ -19537,6 +21559,10 @@ void Player::CharacterSheet_t::updateCharacterInfo() { className->setTextColor(hudColors.characterDLC2ClassText); } + else if ( client_classes[player.playernum] >= CLASS_BARD && client_classes[player.playernum] <= CLASS_PALADIN ) + { + className->setTextColor(hudColors.characterDLC3ClassText); + } else { className->setTextColor(hudColors.characterBaseClassText); @@ -19659,6 +21685,14 @@ void Player::CharacterSheet_t::updateCharacterInfo() { sexImg->path = "*#images/ui/CharSheet/HUD_CharSheet_Sex_AutomatonM_02.png"; } + else if ( type == DRYAD ) + { + sexImg->path = "*#images/ui/CharSheet/HUD_CharSheet_Height_T_00.png"; + } + else if ( type == MYCONID ) + { + sexImg->path = "*#images/ui/CharSheet/HUD_CharSheet_Height_S_00.png"; + } else { sexImg->path = "*#images/ui/CharSheet/HUD_CharSheet_Sex_M_02.png"; @@ -19672,6 +21706,14 @@ void Player::CharacterSheet_t::updateCharacterInfo() { sexImg->path = "*#images/ui/CharSheet/HUD_CharSheet_Sex_AutomatonF_02.png"; } + else if ( type == DRYAD ) + { + sexImg->path = "*#images/ui/CharSheet/HUD_CharSheet_Height_S_00.png"; + } + else if ( type == MYCONID ) + { + sexImg->path = "*#images/ui/CharSheet/HUD_CharSheet_Height_T_00.png"; + } else { sexImg->path = "*#images/ui/CharSheet/HUD_CharSheet_Sex_F_02.png"; @@ -20240,7 +22282,15 @@ void Player::CharacterSheet_t::updateAttributes() if ( auto field = attributesInnerFrame->findField("pwr text stat") ) { - real_t spellPower = (getBonusFromCasterOfSpellElement(player.entity, stats[player.playernum], nullptr, SPELL_NONE) * 100.0) + 100.0; + real_t spellPower = 0.0; + if ( auto spell = player.magic.selectedSpell() ) + { + spellPower = (getBonusFromCasterOfSpellElement(player.entity, stats[player.playernum], nullptr, spell->ID, spell->skillID) * 100.0) + 100.0; + } + else + { + spellPower = (getBonusFromCasterOfSpellElement(player.entity, stats[player.playernum], nullptr, SPELL_NONE, NUMPROFICIENCIES) * 100.0) + 100.0; + } snprintf(buf, sizeof(buf), "%.f%%", spellPower); if ( strcmp(buf, field->getText()) ) { @@ -20265,8 +22315,15 @@ void Player::CharacterSheet_t::updateAttributes() if ( auto field = attributesInnerFrame->findField("res text stat") ) { real_t resistance = 100.0 * Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC); - resistance /= (Entity::getMagicResistance(stats[player.playernum]) + 1); + + Sint32 oldStats[NUMSTATS]; + characterSheetTooltipSetZeroStat(player.entity, stats[player.playernum], nullptr, oldStats); + real_t resistanceNoINT = 100.0 * Entity::getDamageTableMultiplier(player.entity, *stats[player.playernum], DAMAGE_TABLE_MAGIC); + characterSheetTooltipRestoreStats(stats[player.playernum], nullptr, oldStats); + resistance = -(resistance - 100.0); + resistanceNoINT = -(resistanceNoINT - 100.0); + snprintf(buf, sizeof(buf), "%d%%", (int)resistance); if ( strcmp(buf, field->getText()) ) { @@ -20274,11 +22331,11 @@ void Player::CharacterSheet_t::updateAttributes() charsheetTooltipCache[player.playernum].manualUpdate = true; } field->setColor(hudColors.characterSheetNeutral); - if ( resistance > 0.01 ) + if ( (int)resistance > 0 && (int)resistanceNoINT > 0 ) { field->setColor(hudColors.characterSheetGreen); } - else if ( resistance < -0.01 ) + else if ( (int)resistance < 0 ) { field->setColor(hudColors.characterSheetRed); } @@ -20448,6 +22505,13 @@ void Player::Inventory_t::Appraisal_t::updateAppraisalAnim() itemNotifyAnimState = 0; } } + + spellLearnAnim = 1.0; + int interval = 2 * TICKS_PER_SECOND; + if ( ticks % (2 * interval) <= interval ) + { + spellLearnAnim = 1.0 - 0.25 * std::max(0.0, sin((ticks % interval) * PI / (real_t)interval)); + } } void drawClockwiseSquareMesh(const char* texture, float lerp, SDL_Rect rect, Uint32 color) { @@ -20480,9 +22544,19 @@ void drawUnidentifiedItemEffectHotbarCallback(const Widget& widget, SDL_Rect rec opacity *= parent->getOpacity() / 100.0; } const auto& appraisal = players[player]->inventoryUI.appraisal; - drawClockwiseSquareMesh("images/ui/HUD/hotbar/Appraisal_Icon_OutlineHotbar.png", - (appraisal.timermax - appraisal.timer) / (float)appraisal.timermax, - drawRect, makeColor(255, 255, 255, opacity)); + + if ( appraisal.current_item == appraisal.manual_appraised_item ) + { + drawClockwiseSquareMesh("images/ui/HUD/hotbar/Appraisal_Icon_OutlineHotbar_Manual.png", + (appraisal.timermax - appraisal.timer) / (float)appraisal.timermax, + drawRect, makeColor(255, 255, 255, opacity)); + } + else + { + drawClockwiseSquareMesh("images/ui/HUD/hotbar/Appraisal_Icon_OutlineHotbar.png", + (appraisal.timermax - appraisal.timer) / (float)appraisal.timermax, + drawRect, makeColor(255, 255, 255, opacity)); + } } auto drawMesh = [](real_t x, real_t y, real_t size, SDL_Rect rect, Uint32 color) { @@ -20537,9 +22611,19 @@ void drawUnidentifiedItemEffectCallback(const Widget& widget, SDL_Rect rect) { opacity *= parent->getOpacity() / 100.0; } - drawClockwiseSquareMesh("images/ui/Inventory/Appraisal_Icon_Outline.png", - (appraisal.timermax - appraisal.timer) / (float)appraisal.timermax, - drawRect, makeColor(255, 255, 255, opacity)); + + if ( appraisal.current_item == appraisal.manual_appraised_item ) + { + drawClockwiseSquareMesh("images/ui/Inventory/Appraisal_Icon_Outline_Manual.png", + (appraisal.timermax - appraisal.timer) / (float)appraisal.timermax, + drawRect, makeColor(255, 255, 255, opacity)); + } + else + { + drawClockwiseSquareMesh("images/ui/Inventory/Appraisal_Icon_Outline.png", + (appraisal.timermax - appraisal.timer) / (float)appraisal.timermax, + drawRect, makeColor(255, 255, 255, opacity)); + } } auto drawMesh = [](real_t x, real_t y, real_t size, SDL_Rect rect, Uint32 color) { @@ -20943,6 +23027,10 @@ void updateSlotFrameFromItem(Frame* slotFrame, void* itemPtr, bool forceUnusable if ( iconLabelImg->path != "" ) { iconLabelImg->disabled = (!item->identified || hiddenItemInGUI); + if ( item->type == SPELL_ITEM && !(slotType && *slotType == GAMEUI_FRAMEDATA_SPELL_LEARNABLE) ) + { + iconLabelImg->disabled = true; + } } iconLabelImg->color = spriteImage->color; if ( auto iconLabelBgImg = spriteImageFrame->getImages()[SLOTFRAME_ITEMSPRITE_LABELBG_IMG]/*spriteImageFrame->findImage("icon label bg img")*/ ) @@ -20953,6 +23041,14 @@ void updateSlotFrameFromItem(Frame* slotFrame, void* itemPtr, bool forceUnusable iconLabelBgImg->pos.y = 1; iconLabelBgImg->disabled = iconLabelImg->disabled || disableBackgrounds; iconLabelBgImg->color = makeColor(255, 255, 255, 255); + if ( item->type == SPELL_ITEM ) + { + iconLabelBgImg->disabled = true; + SDL_Color color; + getColor(iconLabelImg->color, &color.r, &color.g, &color.b, &color.a); + color.a *= (players[player]->inventoryUI.appraisal.spellLearnAnim); + iconLabelImg->color = makeColor(color.r, color.g, color.b, color.a); + } } } if ( slotFrame->getUserData() ) @@ -20971,15 +23067,35 @@ void updateSlotFrameFromItem(Frame* slotFrame, void* itemPtr, bool forceUnusable { qtyFrame->setDisabled(true); bool drawQty = (item->count > 1) ? true : false; + Uint32 qtyColor = 0xFFFFFFFF; if ( !drawQty && GenericGUI[player].isNodeTinkeringCraftableItem(item->node) ) { drawQty = true; } else if ( slotType && *slotType == GAMEUI_FRAMEDATA_ALCHEMY_RECIPE_ENTRY ) + { + if ( item == &GenericGUI[player].alchemyGUI.torchCount ) + { + if ( item->count >= 0 ) + { + drawQty = true; + if ( item->count < 4 ) + { + qtyColor = hudColors.characterSheetRed; + } + } + } + else + { + drawQty = true; + } + } + else if ( slotType && *slotType == GAMEUI_FRAMEDATA_ALCHEMY_MISSING_QTY ) { drawQty = true; + qtyColor = hudColors.characterSheetRed; } - Uint32 qtyColor = 0xFFFFFFFF; + bool stackable = false; Item*& selectedItem = inputs.getUIInteraction(player)->selectedItem; if ( selectedItem && !isHotbarIcon && !alchemyResultIcon @@ -21032,6 +23148,33 @@ void updateSlotFrameFromItem(Frame* slotFrame, void* itemPtr, bool forceUnusable qtyText->setColor(qtyColor); } } + else + { + if ( item->type == SPELL_ITEM + && (item->appearance == SPELL_LEAD_BOLT || item->appearance == SPELL_MERCURY_BOLT + || item->appearance == SPELL_FORGE_METAL_SCRAP || item->appearance == SPELL_FORGE_MAGIC_SCRAP) ) + { + if ( spell_t* spell = getSpellFromItem(player, item, true) ) + { + qtyFrame->setDisabled(false); + if ( auto qtyText = qtyFrame->getFields()[SLOTFRAME_QTY_TEXT]/*qtyFrame->findField("quantity text")*/ ) + { + char qtybuf[32] = ""; + int cost = getGoldCostOfSpell(spell, player); + if ( cost > stats[player]->GOLD ) + { + qtyColor = hudColors.characterSheetRed; + } + snprintf(qtybuf, sizeof(qtybuf), "%d", cost); + if ( strcmp(qtyText->getText(), qtybuf) ) + { + qtyText->setText(qtybuf); + } + qtyText->setColor(qtyColor); + } + } + } + } } if ( auto beatitudeFrame = frames[SLOTFRAME_BEATITUDE_FRAME]/*slotFrame->findFrame("beatitude status frame")*/ ) @@ -21712,6 +23855,21 @@ void createInventoryTooltipFrame(const int player, tooltipTextField->setHJustify(Field::justify_t::LEFT); tooltipTextField->setVJustify(Field::justify_t::CENTER); tooltipTextField->setColor(makeColor( 188, 154, 114, 255)); + + auto spellImg = valueFrame->addImage(SDL_Rect{ 0, 0, 16, 16 }, + 0xFFFFFFFF, + "*#images/ui/Inventory/tooltips/HUD_Tooltip_Icon_WGT_00.png", + "inventory mouse tooltip spell skill image"); + spellImg->disabled = true; + + tooltipTextField = valueFrame->addField("inventory mouse tooltip spell skill value", 64); + tooltipTextField->setText("Nothing"); + tooltipTextField->setSize(SDL_Rect{ 0, 0, 0, 0 }); + tooltipTextField->setFont(bodyFont.c_str()); + tooltipTextField->setHJustify(Field::justify_t::LEFT); + tooltipTextField->setVJustify(Field::justify_t::CENTER); + tooltipTextField->setColor(makeColor(188, 154, 114, 255)); + tooltipTextField->setDisabled(true); } if ( auto promptFrame = tooltipFrame->addFrame("inventory mouse tooltip prompt frame") ) { @@ -22983,6 +25141,10 @@ void drawObjectPreview(std::string modelsPath, Entity* object, SDL_Rect pos, rea { entity = newEntity(989, 0, &limb.children, nullptr); } + else if ( modelsPath == "arcane_boulder" ) + { + entity = newEntity(990, 0, &limb.children, nullptr); + } else { entity = newEntity(245, 0, &limb.children, nullptr); @@ -23203,6 +25365,19 @@ void drawCharacterPreview(const int player, SDL_Rect pos, int fov, real_t offset //TempTexture* minimapTexture = new TempTexture(); auto playerEntity = Player::getPlayerInteractEntity(player); + if ( playerEntity && playerEntity->behavior == &actDeathGhost ) + { + if ( playerEntity->skill[10] != 0 ) // cosmetic ghost + { + if ( auto node = list_Node(&playerEntity->children, 2) ) + { + if ( Entity* entity = (Entity*)node->element ) + { + playerEntity = entity; + } + } + } + } if ( playerEntity ) { @@ -23214,6 +25389,22 @@ void drawCharacterPreview(const int player, SDL_Rect pos, int fov, real_t offset view.y = playerEntity->y / 16.0 + (.92 * sin(offsetyaw + (*cvar_char_portrait_static_angle ? playerEntity->yaw : 0))); view.z = playerEntity->z * 2; + if ( playerEntity->behavior == &actDeathGhostLimb ) + { + if ( auto node = list_Node(&playerEntity->children, 2) ) + { + if ( Entity* entity = (Entity*)node->element ) + { + view.z = entity->z * 2; + } + } + } + else if ( playerEntity->behavior == &actPlayer ) + { + real_t nominalHeight = 0.0; + view.z = std::min(nominalHeight, view.z); + } + view.ang = (offsetyaw - PI + (*cvar_char_portrait_static_angle ? playerEntity->yaw : 0)); //5 * PI / 4; view.vang = PI / 20; @@ -23256,6 +25447,14 @@ void drawCharacterPreview(const int player, SDL_Rect pos, int fov, real_t offset continue; } } + else if ( playerEntity->behavior == &actDeathGhostLimb ) + { + if ( c < 2 ) + { + c++; + continue; + } + } Entity* entity = (Entity*)node->element; if ( !entity->flags[INVISIBLE] || (entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER]) ) { @@ -23263,7 +25462,11 @@ void drawCharacterPreview(const int player, SDL_Rect pos, int fov, real_t offset if (!dark) { entity->flags[BRIGHT] = true; } int oldDither = entity->dithering[&view].value; - if ( entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER] ) + if ( entity->ditheringOverride >= 0 ) + { + entity->dithering[&view].value = entity->ditheringOverride; + } + else if ( entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER] ) { entity->dithering[&view].value = ditherVal; //entity->flags[BRIGHT] = false; @@ -23274,27 +25477,30 @@ void drawCharacterPreview(const int player, SDL_Rect pos, int fov, real_t offset } c++; } - for ( node_t* node = map.entities->first; node != NULL; node = node->next ) + if ( playerEntity->behavior == &actPlayer ) { - Entity* entity = (Entity*)node->element; - if ( (Sint32)entity->getUID() == -4 ) // torch sprites + for ( node_t* node = map.entities->first; node != NULL; node = node->next ) { - if ( (entity->skill[1] - 1) != player ) + Entity* entity = (Entity*)node->element; + if ( (Sint32)entity->getUID() == -4 ) // torch sprites { - continue; - } - bool b = entity->flags[BRIGHT]; - if (!dark) { entity->flags[BRIGHT] = true; } + if ( (entity->skill[1] - 1) != player ) + { + continue; + } + bool b = entity->flags[BRIGHT]; + if (!dark) { entity->flags[BRIGHT] = true; } - int oldDither = entity->dithering[&view].value; - if ( entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER] ) - { - entity->dithering[&view].value = ditherVal; - //entity->flags[BRIGHT] = false; + int oldDither = entity->dithering[&view].value; + if ( entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER] ) + { + entity->dithering[&view].value = ditherVal; + //entity->flags[BRIGHT] = false; + } + glDrawSprite(&view, entity, REALCOLORS); + entity->flags[BRIGHT] = b; + entity->dithering[&view].value = oldDither; } - glDrawSprite(&view, entity, REALCOLORS); - entity->flags[BRIGHT] = b; - entity->dithering[&view].value = oldDither; } } } @@ -23308,7 +25514,7 @@ void drawCharacterPreview(const int player, SDL_Rect pos, int fov, real_t offset if ( (entity->behavior == &actPlayerLimb && entity->skill[2] == player && (!entity->flags[INVISIBLE] || (entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER]))) || (Sint32)entity->getUID() == -4 ) { - if ( (Sint32)entity->getUID() == -4 ) // torch sprites + if ( (Sint32)entity->getUID() == -4 && playerEntity->behavior == &actPlayer ) // torch sprites { if ( (entity->skill[1] - 1) != player ) { @@ -23334,7 +25540,11 @@ void drawCharacterPreview(const int player, SDL_Rect pos, int fov, real_t offset if (!dark) { entity->flags[BRIGHT] = true; } int oldDither = entity->dithering[&view].value; - if ( entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER] ) + if ( entity->ditheringOverride >= 0 ) + { + entity->dithering[&view].value = entity->ditheringOverride; + } + else if ( entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER] ) { entity->dithering[&view].value = ditherVal; //entity->flags[BRIGHT] = false; @@ -23347,7 +25557,7 @@ void drawCharacterPreview(const int player, SDL_Rect pos, int fov, real_t offset } } - else if ( playerEntity->behavior == &actDeathGhost ) + else if ( playerEntity->behavior == &actDeathGhost || playerEntity->behavior == &actDeathGhostLimb ) { if ( entity->behavior == &actDeathGhostLimb && entity->skill[2] == player && (!entity->flags[INVISIBLE] || (entity->flags[INVISIBLE] && entity->flags[INVISIBLE_DITHER])) ) @@ -23419,9 +25629,15 @@ void Player::SkillSheet_t::loadSkillSheetJSON() { allEntries.push_back(SkillSheetData_t::SkillEntry_t()); auto& entry = allEntries[allEntries.size() - 1]; + entry.setSkillName(""); + entry.setSkillShortName(""); if ( (*itr).HasMember("name") ) { - entry.name = (*itr)["name"].GetString(); + entry.setSkillName((*itr)["name"].GetString()); + } + if ( (*itr).HasMember("shortname") ) + { + entry.setSkillShortName((*itr)["shortname"].GetString()); } if ( (*itr).HasMember("id") ) { @@ -23467,6 +25683,10 @@ void Player::SkillSheet_t::loadSkillSheetJSON() auto& effect = entry.effects[entry.effects.size() - 1]; effect.tag = (*eff_itr)["tag"].GetString(); effect.title = (*eff_itr)["title"].GetString(); + if ( (*eff_itr).HasMember("title_short") ) + { + effect.titleShort = (*eff_itr)["title_short"].GetString(); + } effect.rawValue = (*eff_itr)["value"].GetString(); effect.valueCustomWidthOffset = 0; if ( (*eff_itr).HasMember("custom_value_width_offset") ) @@ -24928,6 +27148,14 @@ void loadHUDSettingsJSON() d["colors"]["charsheet_dlc2_text"]["b"].GetInt(), d["colors"]["charsheet_dlc2_text"]["a"].GetInt()); } + if ( d["colors"].HasMember("charsheet_dlc3_text") ) + { + hudColors.characterDLC3ClassText = makeColor( + d["colors"]["charsheet_dlc3_text"]["r"].GetInt(), + d["colors"]["charsheet_dlc3_text"]["g"].GetInt(), + d["colors"]["charsheet_dlc3_text"]["b"].GetInt(), + d["colors"]["charsheet_dlc3_text"]["a"].GetInt()); + } } if ( d.HasMember("dropdowns") ) { @@ -25102,7 +27330,7 @@ void createPlayerSpellList(const int player) snprintf(slotname, sizeof(slotname), "spell %d %d", x, y); auto slotFrame = spellSlotsFrame->addFrame(slotname); - players[player]->inventoryUI.spellSlotFrames[x + y * 100] = slotFrame; + players[player]->inventoryUI.spellSlotFrames[x + y * 1000] = slotFrame; SDL_Rect slotPos{ currentSlotPos.x, currentSlotPos.y, inventorySlotSize, inventorySlotSize }; slotFrame->setSize(slotPos); @@ -25158,6 +27386,26 @@ void createPlayerSpellList(const int player) titleText->setSize(SDL_Rect{ 56, 12, 96, 24 }); titleText->setColor(makeColor(236, 175, 28, 255)); + auto filterTooltipFrame = bgFrame->addFrame("filter frame"); + filterTooltipFrame->setSize(SDL_Rect{ 0, bg->pos.h - 50, bg->pos.w, 50 }); + filterTooltipFrame->setHollow(true); + filterTooltipFrame->setInheritParentFrameOpacity(false); + + auto filterText = filterTooltipFrame->addField("filter txt", 64); + filterText->setFont(smallfont_outline); + filterText->setText(Language::get(6842)); + filterText->setHJustify(Field::justify_t::CENTER); + filterText->setVJustify(Field::justify_t::TOP); + filterText->setSize(SDL_Rect{ 0, 3, filterTooltipFrame->getSize().w, 48}); + filterText->setColor(makeColor(236, 175, 28, 255)); + filterText->setDisabled(true); + + auto filterTooltipImg = filterTooltipFrame->addImage( + SDL_Rect{ filterTooltipFrame->getSize().w / 2 - 154 / 2, filterTooltipFrame->getSize().h - 50, 154, 50 }, + makeColor(255, 255, 255, 128), + "*#images/ui/Inventory/HUD_Magic_Filter_Tooltip.png", "spell filter tooltip img"); + filterTooltipImg->disabled = true; + auto closeBtn = bgFrame->addButton("close spell button"); SDL_Rect closeBtnPos; closeBtnPos.x = 180; @@ -25194,18 +27442,64 @@ void createPlayerSpellList(const int player) } }); - auto skillBg = bgFrame->addImage(SDL_Rect{ 6, 6, 32, 32 }, + /*auto skillBg = bgFrame->addImage(SDL_Rect{ 14, 6, 32, 32 }, makeColor(255, 255, 255, 255), - "*#images/ui/Inventory/HUD_Magic_Casting_BG_01.png", "spell skill bg"); + "*#images/ui/Inventory/HUD_Magic_Casting_BG_01.png", "spell skill bg");*/ + + auto filterBtn = bgFrame->addButton("spell filter button"); + SDL_Rect filterBtnPos; + filterBtnPos.x = 14; + filterBtnPos.y = 6; + filterBtnPos.w = 32; + filterBtnPos.h = 32; + filterBtn->setSize(filterBtnPos); + filterBtn->setColor(makeColor(255, 255, 255, 255)); + filterBtn->setHighlightColor(makeColor(255, 255, 255, 255)); + filterBtn->setTextHighlightColor(makeColor(201, 162, 100, 255)); + filterBtn->setText(""); + filterBtn->setFont(font); + filterBtn->setHideGlyphs(true); + filterBtn->setHideKeyboardGlyphs(true); + filterBtn->setHideSelectors(true); + filterBtn->setMenuConfirmControlType(0); + filterBtn->setBackground("*#images/ui/Inventory/HUD_Magic_Casting_BG_01.png"); + filterBtn->setBackgroundHighlighted("*#images/ui/Inventory/HUD_Magic_Casting_BG_High_01.png"); + filterBtn->setBackgroundActivated("*#images/ui/Inventory/HUD_Magic_Casting_BG_Press_01.png"); + filterBtn->setCallback([](Button& button) { + auto& val = players[button.getOwner()]->inventoryUI.spellPanel.spellFilterBySkill; + if ( val == 0 ) { val = PRO_SORCERY; } + else if ( val == PRO_SORCERY ) { val = PRO_MYSTICISM; } + else if ( val == PRO_MYSTICISM ) { val = PRO_THAUMATURGY; } + else + { + val = 0; + } + Player::soundActivate(); + }); + filterBtn->setTickCallback([](Widget& widget) { + if ( widget.isSelected() ) + { + if ( !inputs.getVirtualMouse(widget.getOwner())->draw_cursor ) + { + widget.deselect(); + } + } + }); - auto skillIcon = bgFrame->addImage(SDL_Rect{ skillBg->pos.x + 4, skillBg->pos.y + 4, 24, 24 }, + auto skillIcon = bgFrame->addImage(SDL_Rect{ filterBtnPos.x + 4, filterBtnPos.y + 4, 24, 24 }, makeColor(255, 255, 255, 255), "", "spell skill icon"); + skillIcon->ontop = true; auto closeGlyph = bgFrame->addImage(SDL_Rect{ 0, 0, 24, 24 }, makeColor(255, 255, 255, 255), "", "close spell glyph"); closeGlyph->disabled = true; + + auto filterGlyph = bgFrame->addImage(SDL_Rect{ 0, 0, 24, 24 }, + makeColor(255, 255, 255, 255), + "", "filter spell glyph"); + filterGlyph->disabled = true; } } @@ -25243,9 +27537,9 @@ bool takeAllChestGUIAction(const int player) { chest_inventory = &chestInv[player]; } - else if ( openedChest[player]->children.first && openedChest[player]->children.first->element ) + else if ( openedChest[player] ) { - chest_inventory = (list_t*)openedChest[player]->children.first->element; + chest_inventory = openedChest[player]->getChestInventoryList(); } if ( !chest_inventory ) { @@ -26352,6 +28646,13 @@ void createPlayerInventory(const int player) GenericGUI[player].assistShrineGUI.assistShrineFrame->setInheritParentFrameOpacity(false); GenericGUI[player].assistShrineGUI.assistShrineFrame->setDisabled(true); + GenericGUI[player].mailboxGUI.mailFrame = frame->addFrame("mail"); + GenericGUI[player].mailboxGUI.mailFrame->setHollow(true); + GenericGUI[player].mailboxGUI.mailFrame->setBorder(0); + GenericGUI[player].mailboxGUI.mailFrame->setOwner(player); + GenericGUI[player].mailboxGUI.mailFrame->setInheritParentFrameOpacity(false); + GenericGUI[player].mailboxGUI.mailFrame->setDisabled(true); + auto oldCursorFrame = frame->addFrame("inventory old item cursor"); oldCursorFrame->setSize(SDL_Rect{ 0, 0, inventorySlotSize + 16, inventorySlotSize + 16 }); oldCursorFrame->setDisabled(true); @@ -26464,9 +28765,9 @@ void Player::Inventory_t::updateItemContextMenu() { chest_inventory = &chestInv[player.playernum]; } - else if ( openedChest[player.playernum]->children.first && openedChest[player.playernum]->children.first->element ) + else if ( openedChest[player.playernum] ) { - chest_inventory = (list_t*)openedChest[player.playernum]->children.first->element; + chest_inventory = openedChest[player.playernum]->getChestInventoryList(); } if ( chest_inventory ) { @@ -26969,7 +29270,19 @@ void Player::Inventory_t::activateItemContextMenuOption(Item* item, ItemContextM } if ( prompt == PROMPT_APPRAISE ) { + int prevAppraisedManual = players[player]->inventoryUI.appraisal.manual_appraised_item; players[player]->inventoryUI.appraisal.appraiseItem(item); + if ( players[player]->inventoryUI.appraisal.current_item == item->uid ) + { + if ( prevAppraisedManual == item->uid ) + { + players[player]->inventoryUI.appraisal.manual_appraised_item = 0; + } + else + { + players[player]->inventoryUI.appraisal.manual_appraised_item = item->uid; + } + } return; } else if ( prompt == PROMPT_DROP ) @@ -27351,7 +29664,9 @@ void Player::Inventory_t::activateItemContextMenuOption(Item* item, ItemContextM || prompt == PROMPT_INTERACT_SPELLBOOK_HOTBAR || prompt == PROMPT_INSPECT || prompt == PROMPT_INSPECT_ALTERNATE - || prompt == PROMPT_TINKER ) + || prompt == PROMPT_TINKER + || prompt == PROMPT_COOK + || prompt == PROMPT_SCEPTER_CHARGE ) { if ( item->type == TOOL_PLAYER_LOOT_BAG ) { @@ -27364,6 +29679,31 @@ void Player::Inventory_t::activateItemContextMenuOption(Item* item, ItemContextM useItem(item, player); } } + else if ( item->type == MAGICSTAFF_SCEPTER ) + { + if ( !disableItemUsage ) + { + if ( item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX >= MAGICSTAFF_SCEPTER_CHARGE_MAX - 1 ) + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(6838), item->getName()); // fully charged! + playSoundPlayer(player, 90, 64); + } + else if ( true /*item->status > BROKEN*/ ) // allow broken tinker kit + { + GenericGUI[player].openGUI(GUI_TYPE_ITEMFX, item, item->beatitude, item->type, 0); + } + else + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(1092), item->getName()); // this is useless! + playSoundPlayer(player, 90, 64); + } + } + else + { + messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3432)); // unable to use in current form message. + playSoundPlayer(player, 90, 64); + } + } else if ( item->type == TOOL_ALEMBIC ) { // not experimenting @@ -27389,6 +29729,26 @@ void Player::Inventory_t::activateItemContextMenuOption(Item* item, ItemContextM playSoundPlayer(player, 90, 64); } } + else if ( item->type == TOOL_FRYING_PAN ) + { + if ( !disableItemUsage ) + { + if ( item->status > BROKEN ) + { + GenericGUI[player].openGUI(GUI_TYPE_ALCHEMY, true, item); + } + else + { + messagePlayer(player, MESSAGE_EQUIPMENT, Language::get(1092), item->getName()); // this is useless! + playSoundPlayer(player, 90, 64); + } + } + else + { + messagePlayer(player, MESSAGE_INVENTORY | MESSAGE_HINT | MESSAGE_EQUIPMENT, Language::get(3432)); // unable to use in current form message. + playSoundPlayer(player, 90, 64); + } + } else if ( item->type == TOOL_TINKERING_KIT ) { if ( !disableItemUsage ) @@ -27413,7 +29773,7 @@ void Player::Inventory_t::activateItemContextMenuOption(Item* item, ItemContextM { if ( !disableItemUsage && prompt == PROMPT_INTERACT_SPELLBOOK_HOTBAR ) { - if ( itemCategory(item) == SPELLBOOK ) + if ( itemCategory(item) == SPELLBOOK || itemCategory(item) == TOME_SPELL ) { players[player]->magic.spellbookUidFromHotbarSlot = item->uid; } @@ -27972,6 +30332,21 @@ void Player::Inventory_t::updateCursor() cursor.queuedModule = Player::GUI_t::MODULE_NONE; } } + else if ( cursor.queuedModule == Player::GUI_t::MODULE_MAILBOX ) + { + auto& mailboxGUI = GenericGUI[player.playernum].mailboxGUI; + if ( !mailboxGUI.mailGUIHasBeenCreated() + || mailboxGUI.mailFrame->isDisabled() ) + { + // cancel + cursor.queuedModule = Player::GUI_t::MODULE_NONE; + } + else if ( mailboxGUI.isInteractable ) + { + moveMouse = true; + cursor.queuedModule = Player::GUI_t::MODULE_NONE; + } + } else if ( cursor.queuedModule == Player::GUI_t::MODULE_ASSISTSHRINE ) { auto& assistShrineGUI = GenericGUI[player.playernum].assistShrineGUI; @@ -29225,6 +31600,10 @@ void Player::HUD_t::updateXPBar() { textClass->setColor(hudColors.characterDLC2ClassText); } + else if ( client_classes[player.playernum] >= CLASS_BARD && client_classes[player.playernum] <= CLASS_PALADIN ) + { + textClass->setColor(hudColors.characterDLC3ClassText); + } else { textClass->setColor(hudColors.characterBaseClassText); @@ -29344,20 +31723,40 @@ struct enemybarMapLowDurationTick_k { } }; }; -struct enemybarEffectMapFx2_lowDuration_k { +struct enemybarEffectMapFx5_lowDuration_k { std::map m; }; +struct enemybarEffectMapFx4_lowDuration_k { + std::map m; +}; +struct enemybarEffectMapFx3_lowDuration_k { + std::map m; +}; +struct enemybarEffectMapFx2_lowDuration_k { + std::map m; +}; struct enemybarEffectMapFx1_lowDuration_k { std::map m; }; -struct enemybarEffectMapFx2_k { +struct enemybarEffectMapFx5_k { std::map m; }; +struct enemybarEffectMapFx4_k { + std::map m; +}; +struct enemybarEffectMapFx3_k { + std::map m; +}; +struct enemybarEffectMapFx2_k { + std::map m; +}; struct enemybarEffectMapFx1_k { std::map m; }; enemybarEffectMapFx1_k enemyBarEffectMap; -SDL_Surface* enemyBarEffectMapExists(Uint32 fx1, Uint32 fx2, Uint32 fx_lowDuration1, Uint32 fx_lowDuration2, bool lowDurationTicks) +SDL_Surface* enemyBarEffectMapExists(Uint32 fx1, Uint32 fx2, Uint32 fx3, Uint32 fx4, Uint32 fx5, + Uint32 fx_lowDuration1, Uint32 fx_lowDuration2, Uint32 fx_lowDuration3, Uint32 fx_lowDuration4, Uint32 fx_lowDuration5, + bool lowDurationTicks) { if ( enemyBarEffectMap.m.find(fx1) != enemyBarEffectMap.m.end() ) { @@ -29365,15 +31764,39 @@ SDL_Surface* enemyBarEffectMapExists(Uint32 fx1, Uint32 fx2, Uint32 fx_lowDurati if ( m1.m.find(fx2) != m1.m.end() ) { auto& m2 = m1.m[fx2]; - if ( m2.m.find(fx_lowDuration1) != m2.m.end() ) + if ( m2.m.find(fx3) != m2.m.end() ) { - auto& m3 = m2.m[fx_lowDuration1]; - if ( m3.m.find(fx_lowDuration2) != m3.m.end() ) + auto& m3 = m2.m[fx3]; + if ( m3.m.find(fx4) != m3.m.end() ) { - auto& m4 = m3.m[fx_lowDuration2]; - if ( m4.m.find(lowDurationTicks) != m4.m.end() ) + auto& m4 = m3.m[fx4]; + if ( m4.m.find(fx5) != m4.m.end() ) { - return m4.m[lowDurationTicks]; + auto& m5 = m4.m[fx5]; + if ( m5.m.find(fx_lowDuration1) != m5.m.end() ) + { + auto& m6 = m5.m[fx_lowDuration1]; + if ( m6.m.find(fx_lowDuration2) != m6.m.end() ) + { + auto& m7 = m6.m[fx_lowDuration2]; + if ( m7.m.find(fx_lowDuration3) != m7.m.end() ) + { + auto& m8 = m7.m[fx_lowDuration3]; + if ( m8.m.find(fx_lowDuration4) != m8.m.end() ) + { + auto& m9 = m8.m[fx_lowDuration4]; + if ( m9.m.find(fx_lowDuration5) != m9.m.end() ) + { + auto& m10 = m9.m[fx_lowDuration5]; + if ( m10.m.find(lowDurationTicks) != m10.m.end() ) + { + return m10.m[lowDurationTicks]; + } + } + } + } + } + } } } } @@ -29381,16 +31804,20 @@ SDL_Surface* enemyBarEffectMapExists(Uint32 fx1, Uint32 fx2, Uint32 fx_lowDurati } return nullptr; } -void enemyBarEffectMapInsert(Uint32 fx1, Uint32 fx2, Uint32 fx_lowDuration1, Uint32 fx_lowDuration2, bool lowDurationTicks, +void enemyBarEffectMapInsert(Uint32 fx1, Uint32 fx2, Uint32 fx3, Uint32 fx4, Uint32 fx5, + Uint32 fx_lowDuration1, Uint32 fx_lowDuration2, Uint32 fx_lowDuration3, Uint32 fx_lowDuration4, Uint32 fx_lowDuration5, + bool lowDurationTicks, SDL_Surface* surf) { - enemyBarEffectMap.m[fx1].m[fx2].m[fx_lowDuration1].m[fx_lowDuration2].m[lowDurationTicks] = surf; + enemyBarEffectMap.m[fx1].m[fx2].m[fx3].m[fx4].m[fx5].m[fx_lowDuration1] + .m[fx_lowDuration2].m[fx_lowDuration3].m[fx_lowDuration4].m[fx_lowDuration5] + .m[lowDurationTicks] = surf; } // to nest deep maps and suppress visual studio warnings -struct enemybarMapFx2_lowDuration_k { +struct enemybarMapFx5_lowDuration_k { std::map m; - ~enemybarMapFx2_lowDuration_k() + ~enemybarMapFx5_lowDuration_k() { if ( EnemyHPDamageBarHandler::bEnemyBarSimpleBlit ) { @@ -29405,12 +31832,30 @@ struct enemybarMapFx2_lowDuration_k { } }; }; +struct enemybarMapFx4_lowDuration_k { + std::map m; +}; +struct enemybarMapFx3_lowDuration_k { + std::map m; +}; +struct enemybarMapFx2_lowDuration_k { + std::map m; +}; struct enemybarMapFx1_lowDuration_k { std::map m; }; -struct enemybarMapFx2_k { +struct enemybarMapFx5_k { std::map m; }; +struct enemybarMapFx4_k { + std::map m; +}; +struct enemybarMapFx3_k { + std::map m; +}; +struct enemybarMapFx2_k { + std::map m; +}; struct enemybarMapFx1_k { std::map m; }; @@ -29426,7 +31871,8 @@ struct enemybarMapName_k { enemybarMapName_k enemyBarMap; SDL_Surface* enemyBarMapExists(std::string name, int baseWidth, int baseHeight, int progressWidth, int damageWidth, - Uint32 fx1, Uint32 fx2, Uint32 fx_lowDuration1, Uint32 fx_lowDuration2) + Uint32 fx1, Uint32 fx2, Uint32 fx3, Uint32 fx4, Uint32 fx5, + Uint32 fx_lowDuration1, Uint32 fx_lowDuration2, Uint32 fx_lowDuration3, Uint32 fx_lowDuration4, Uint32 fx_lowDuration5) { if ( enemyBarMap.m.find(name) != enemyBarMap.m.end() ) { @@ -29448,12 +31894,36 @@ SDL_Surface* enemyBarMapExists(std::string name, int baseWidth, int baseHeight, if ( m4.m.find(fx2) != m4.m.end() ) { auto& m5 = m4.m[fx2]; - if ( m5.m.find(fx_lowDuration1) != m5.m.end() ) + if ( m5.m.find(fx3) != m5.m.end() ) { - auto& m6 = m5.m[fx_lowDuration1]; - if ( m6.m.find(fx_lowDuration2) != m6.m.end() ) + auto& m6 = m5.m[fx3]; + if ( m6.m.find(fx4) != m6.m.end() ) { - return m6.m[fx_lowDuration2]; + auto& m7 = m6.m[fx4]; + if ( m7.m.find(fx5) != m7.m.end() ) + { + auto& m8 = m7.m[fx5]; + if ( m8.m.find(fx_lowDuration1) != m8.m.end() ) + { + auto& m9 = m8.m[fx_lowDuration1]; + if ( m9.m.find(fx_lowDuration2) != m9.m.end() ) + { + auto& m10 = m9.m[fx_lowDuration2]; + if ( m10.m.find(fx_lowDuration3) != m10.m.end() ) + { + auto& m11 = m10.m[fx_lowDuration3]; + if ( m11.m.find(fx_lowDuration4) != m11.m.end() ) + { + auto& m12 = m11.m[fx_lowDuration4]; + if ( m12.m.find(fx_lowDuration5) != m12.m.end() ) + { + return m12.m[fx_lowDuration5]; + } + } + } + } + } + } } } } @@ -29464,8 +31934,8 @@ SDL_Surface* enemyBarMapExists(std::string name, int baseWidth, int baseHeight, return nullptr; } static void enemyBarMapInsert(std::string name, int baseWidth, int baseHeight, int progressWidth, int damageWidth, - Uint32 statusfx1, Uint32 statusfx2, - Uint32 statusfx_lowDuration1, Uint32 statusfx_lowDuration2, + Uint32 statusfx1, Uint32 statusfx2, Uint32 statusfx3, Uint32 statusfx4, Uint32 statusfx5, + Uint32 statusfx_lowDuration1, Uint32 statusfx_lowDuration2, Uint32 statusfx_lowDuration3, Uint32 statusfx_lowDuration4, Uint32 statusfx_lowDuration5, SDL_Surface* surf) { Uint32 totalSizeKey = baseWidth & 0xFFFF; @@ -29473,7 +31943,9 @@ static void enemyBarMapInsert(std::string name, int baseWidth, int baseHeight, totalSizeKey |= (((ticks % 25) >= 12) << 24) & 0xFF000000; Uint32 progressDamageKey = progressWidth & 0xFFFF; progressDamageKey |= ((damageWidth & 0xFFFF) << 16); - enemyBarMap.m[name].m[totalSizeKey].m[progressDamageKey].m[statusfx1].m[statusfx2].m[statusfx_lowDuration1].m[statusfx_lowDuration2] = surf; + enemyBarMap.m[name].m[totalSizeKey].m[progressDamageKey] + .m[statusfx1].m[statusfx2].m[statusfx3].m[statusfx4].m[statusfx5] + .m[statusfx_lowDuration1].m[statusfx_lowDuration2].m[statusfx_lowDuration3].m[statusfx_lowDuration4].m[statusfx_lowDuration5] = surf; } SDL_Surface* EnemyHPDamageBarHandler::EnemyHPDetails::blitEnemyBar(const int player, SDL_Surface* statusEffectSprite) @@ -29522,8 +31994,14 @@ SDL_Surface* EnemyHPDamageBarHandler::EnemyHPDetails::blitEnemyBar(const int pla dmgProgress->pos.w, hpProgress->pos.w, enemy_statusEffects1, enemy_statusEffects2, + enemy_statusEffects3, + enemy_statusEffects4, + enemy_statusEffects5, enemy_statusEffectsLowDuration1, - enemy_statusEffectsLowDuration2); + enemy_statusEffectsLowDuration2, + enemy_statusEffectsLowDuration3, + enemy_statusEffectsLowDuration4, + enemy_statusEffectsLowDuration5); if ( !hashSurf ) { //messagePlayer(0, MESSAGE_DEBUG, "Hash for enemy bar not found!"); @@ -29617,8 +32095,14 @@ SDL_Surface* EnemyHPDamageBarHandler::EnemyHPDetails::blitEnemyBar(const int pla dmgProgress->pos.w, hpProgress->pos.w, enemy_statusEffects1, enemy_statusEffects2, + enemy_statusEffects3, + enemy_statusEffects4, + enemy_statusEffects5, enemy_statusEffectsLowDuration1, enemy_statusEffectsLowDuration2, + enemy_statusEffectsLowDuration3, + enemy_statusEffectsLowDuration4, + enemy_statusEffectsLowDuration5, sprite); } return sprite; @@ -29636,7 +32120,11 @@ SDL_Surface* EnemyHPDamageBarHandler::EnemyHPDetails::blitEnemyBarStatusEffects( { return nullptr; } - if ( enemy_statusEffects1 == 0 && enemy_statusEffects2 == 0 ) + if ( enemy_statusEffects1 == 0 + && enemy_statusEffects2 == 0 + && enemy_statusEffects3 == 0 + && enemy_statusEffects4 == 0 + && enemy_statusEffects5 == 0 ) { return nullptr; } @@ -29652,8 +32140,14 @@ SDL_Surface* EnemyHPDamageBarHandler::EnemyHPDetails::blitEnemyBarStatusEffects( hashSurf = enemyBarEffectMapExists( enemy_statusEffects1, enemy_statusEffects2, + enemy_statusEffects3, + enemy_statusEffects4, + enemy_statusEffects5, enemy_statusEffectsLowDuration1, enemy_statusEffectsLowDuration2, + enemy_statusEffectsLowDuration3, + enemy_statusEffectsLowDuration4, + enemy_statusEffectsLowDuration5, (ticks % 25) >= 12); if ( !hashSurf ) { @@ -29798,6 +32292,190 @@ SDL_Surface* EnemyHPDamageBarHandler::EnemyHPDetails::blitEnemyBarStatusEffects( } } } + if ( enemy_statusEffects3 != 0 ) + { + for ( int i = 0; i < 32; ++i ) + { + if ( (enemy_statusEffects3 & (1 << i)) != 0 ) + { + int effectID = i + 64; + if ( StatusEffectQueue_t::StatusEffectDefinitions_t::effectDefinitionExists(effectID) ) + { + int variation = -1; + SDL_Surface* srcSurf = nullptr; + if ( i == EFF_SHAPESHIFT ) + { + if ( entity && entity->behavior == &actPlayer ) + { + switch ( entity->effectShapeshift ) + { + case RAT: + variation = 0; + break; + case SPIDER: + variation = 1; + break; + case TROLL: + variation = 2; + break; + case CREATURE_IMP: + variation = 3; + break; + default: + break; + } + } + } + auto& definition = StatusEffectQueue_t::StatusEffectDefinitions_t::getEffect(effectID); + if ( !definition.neverDisplay ) + { + std::string imgPath; + if ( i == EFF_SHAPESHIFT && variation == -1 ) + { + imgPath = ""; + } + else + { + imgPath = StatusEffectQueue_t::StatusEffectDefinitions_t::getEffectImgPath(definition, variation); + } + if ( imgPath != "" ) + { + srcSurf = const_cast(Image::get(imgPath.c_str())->getSurf()); + + bool blinking = false; + if ( (enemy_statusEffectsLowDuration3 & (1 << i)) != 0 ) + { + blinking = true; + } + statusEffectIcons.push_back(std::make_pair(srcSurf, blinking)); + } + } + } + } + } + } + if ( enemy_statusEffects4 != 0 ) + { + for ( int i = 0; i < 32; ++i ) + { + if ( (enemy_statusEffects4 & (1 << i)) != 0 ) + { + int effectID = i + 96; + if ( StatusEffectQueue_t::StatusEffectDefinitions_t::effectDefinitionExists(effectID) ) + { + int variation = -1; + SDL_Surface* srcSurf = nullptr; + if ( i == EFF_SHAPESHIFT ) + { + if ( entity && entity->behavior == &actPlayer ) + { + switch ( entity->effectShapeshift ) + { + case RAT: + variation = 0; + break; + case SPIDER: + variation = 1; + break; + case TROLL: + variation = 2; + break; + case CREATURE_IMP: + variation = 3; + break; + default: + break; + } + } + } + auto& definition = StatusEffectQueue_t::StatusEffectDefinitions_t::getEffect(effectID); + if ( !definition.neverDisplay ) + { + std::string imgPath; + if ( i == EFF_SHAPESHIFT && variation == -1 ) + { + imgPath = ""; + } + else + { + imgPath = StatusEffectQueue_t::StatusEffectDefinitions_t::getEffectImgPath(definition, variation); + } + if ( imgPath != "" ) + { + srcSurf = const_cast(Image::get(imgPath.c_str())->getSurf()); + + bool blinking = false; + if ( (enemy_statusEffectsLowDuration4 & (1 << i)) != 0 ) + { + blinking = true; + } + statusEffectIcons.push_back(std::make_pair(srcSurf, blinking)); + } + } + } + } + } + } + if ( enemy_statusEffects5 != 0 ) + { + for ( int i = 0; i < 32; ++i ) + { + if ( (enemy_statusEffects5 & (1 << i)) != 0 ) + { + int effectID = i + 128; + if ( StatusEffectQueue_t::StatusEffectDefinitions_t::effectDefinitionExists(effectID) ) + { + int variation = -1; + SDL_Surface* srcSurf = nullptr; + if ( effectID == EFF_SALAMANDER_HEART ) + { + variation = -1; + if ( entity ) + { + if ( (entity->sprite == 2018 || entity->sprite == 2019) + || (entity->sprite == 1540 || entity->sprite == 1541) ) + { + variation = 2; + } + else if ( (entity->sprite == 2016 || entity->sprite == 2017) + || (entity->sprite == 1538 || entity->sprite == 1539) ) + { + variation = 0; + } + } + } + auto& definition = StatusEffectQueue_t::StatusEffectDefinitions_t::getEffect(effectID); + if ( !definition.neverDisplay ) + { + std::string imgPath; + if ( effectID == EFF_SHAPESHIFT && variation == -1 ) + { + imgPath = ""; + } + else if ( effectID == EFF_SALAMANDER_HEART && variation == -1 ) + { + imgPath = ""; + } + else + { + imgPath = StatusEffectQueue_t::StatusEffectDefinitions_t::getEffectImgPath(definition, variation); + } + if ( imgPath != "" ) + { + srcSurf = const_cast(Image::get(imgPath.c_str())->getSurf()); + + bool blinking = false; + if ( (enemy_statusEffectsLowDuration5 & (1 << i)) != 0 ) + { + blinking = true; + } + statusEffectIcons.push_back(std::make_pair(srcSurf, blinking)); + } + } + } + } + } + } //const int numIcons = statusEffectIcons.size(); //const int iconTotalWidth = iconWidth + 2; @@ -29844,8 +32522,14 @@ SDL_Surface* EnemyHPDamageBarHandler::EnemyHPDetails::blitEnemyBarStatusEffects( enemyBarEffectMapInsert( enemy_statusEffects1, enemy_statusEffects2, + enemy_statusEffects3, + enemy_statusEffects4, + enemy_statusEffects5, enemy_statusEffectsLowDuration1, enemy_statusEffectsLowDuration2, + enemy_statusEffectsLowDuration3, + enemy_statusEffectsLowDuration4, + enemy_statusEffectsLowDuration5, (ticks % 25) >= 12, sprite); } @@ -31119,7 +33803,7 @@ void Player::HUD_t::updateHPBar() } } - hpFrame->setDisabled(player.ghost.isActive()); + hpFrame->setDisabled(player.ghost.isActive() && !player.entity); } void Player::HUD_t::updateMPBar() @@ -31157,6 +33841,7 @@ void Player::HUD_t::updateMPBar() mpForegroundFrame->setSize(_pos); } + std::vector allMPBarImages; auto mpBg = mpFrame->findImage("mp img base"); auto mpEndcap = mpForegroundFrame->findImage("mp img endcap"); auto mpProgressBot = mpForegroundFrame->findImage("mp img progress bot"); @@ -31206,6 +33891,7 @@ void Player::HUD_t::updateMPBar() MPBar.animateSetpoint = stats[player.playernum]->MP; + bool forceLoop = false; bool flashAnimationPreviouslyPlaying = MPBar.flashTicks > 0; if ( MPBar.animateSetpoint < MPBar.animatePreviousSetpoint ) // insta-change as losing health { @@ -31219,6 +33905,24 @@ void Player::HUD_t::updateMPBar() MPBar.flashType = FLASH_ON_DAMAGE; } + if ( &MPBarPaths_t::getMPBar(player.playernum) == &MPBarPaths_t::automatonHTBars ) + { + if ( stats[player.playernum]->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + forceLoop = true; + // flash for taking damage + if ( MPBar.flashTicks <= 0 ) + { + MPBar.animateTicks = ticks; + + // flash for taking damage + MPBar.flashTicks = ticks; + MPBar.flashProcessedOnTick = 0; + MPBar.flashType = FLASH_ON_DAMAGE; + } + } + } + if ( MPBar.maxValue > stats[player.playernum]->MAXMP ) { mpFadedValue = MPBar.animateSetpoint; // resetting game etc, stop fade animation sticking out of frame @@ -31255,7 +33959,7 @@ void Player::HUD_t::updateMPBar() mpFadedValue = mpForegroundValue; MPBar.animateTicks = ticks; } - else if ( mpFadedValue > MPBar.animateSetpoint ) + else if ( mpFadedValue > MPBar.animateSetpoint || forceLoop ) { if ( ticks - MPBar.animateTicks > 30 /*|| stats[player.playernum]->MP <= 0*/ ) // fall after x ticks { @@ -31351,7 +34055,11 @@ void Player::HUD_t::updateMPBar() mpProgressEndCap->path = MPBarPaths_t::get(player.playernum, "mp img progress endcap"); auto mpProgressEndCapFlash = mpForegroundFrame->findImage("mp img progress endcap flash"); mpProgressEndCapFlash->disabled = true; - const int framesPerAnimation = (MPBar.flashType == FLASH_ON_DAMAGE ? 1 : 2)/* * *cvar_hpanimdebug*/; + int framesPerAnimation = (MPBar.flashType == FLASH_ON_DAMAGE ? 1 : 2)/* * *cvar_hpanimdebug*/; + if ( forceLoop ) + { + framesPerAnimation = 4; + } const int numAnimationFrames = (MPBar.flashType == FLASH_ON_DAMAGE ? 30 : 2)/* * *cvar_hpanimdebug*/; if ( MPBar.flashTicks > 0 ) { @@ -31400,6 +34108,10 @@ void Player::HUD_t::updateMPBar() if ( MPBar.flashAnimState <= 9 ) { + if ( forceLoop && MPBar.flashAnimState == 9 ) + { + MPBar.flashAnimState = 1; + } if ( MPBar.flashAnimState == 7 ) { // we need the MP bar to flash long enough for long spellcast times @@ -31520,6 +34232,73 @@ void Player::HUD_t::updateMPBar() } } + //allMPBarImages.push_back(mpBg); + //allMPBarImages.push_back(mpEndcap); + allMPBarImages.push_back(mpProgressBot); + allMPBarImages.push_back(mpProgress); + allMPBarImages.push_back(mpProgressEndCap); + allMPBarImages.push_back(mpFadedBase); + allMPBarImages.push_back(mpFaded); + allMPBarImages.push_back(mpFadedEndCap); + allMPBarImages.push_back(mpBase); + allMPBarImages.push_back(mpProgressEndCapFlash); + static ConsoleVariable cvar_mp_color_ht("/mp_color_ht", { 0.85, 0.85, 1.0, 1.0 }); + static ConsoleVariable cvar_mp_color_ht_high("/mp_color_ht_high", { 1.0, 1.0, 1.0, 1.0 }); + static ConsoleVariable cvar_mp_color_default("/mp_color_default", { 1.0, 1.0, 1.0, 1.0 }); + Vector4 mpColor = *cvar_mp_color_default; + if ( &MPBarPaths_t::getMPBar(player.playernum) == &MPBarPaths_t::automatonHTBars ) + { + mpColor = *cvar_mp_color_ht; + if ( stats[player.playernum]->getEffectActive(EFF_SALAMANDER_HEART) == 2 ) + { + mpColor = *cvar_mp_color_ht_high; + } + } + + /*if ( keystatus[SDLK_KP_7] ) + { + keystatus[SDLK_KP_7] = 0; + cvar_mp_color->x = std::min(1.f, cvar_mp_color->x + 0.01f); + } + if ( keystatus[SDLK_KP_4] ) + { + keystatus[SDLK_KP_4] = 0; + cvar_mp_color->x = std::max(0.f, cvar_mp_color->x - 0.01f); + } + if ( keystatus[SDLK_KP_8] ) + { + keystatus[SDLK_KP_8] = 0; + cvar_mp_color->y = std::min(1.f, cvar_mp_color->y + 0.01f); + } + if ( keystatus[SDLK_KP_5] ) + { + keystatus[SDLK_KP_5] = 0; + cvar_mp_color->y = std::max(0.f, cvar_mp_color->y - 0.01f); + } + if ( keystatus[SDLK_KP_9] ) + { + keystatus[SDLK_KP_9] = 0; + cvar_mp_color->z = std::min(1.f, cvar_mp_color->z + 0.01f); + } + if ( keystatus[SDLK_KP_6] ) + { + keystatus[SDLK_KP_6] = 0; + cvar_mp_color->z = std::max(0.f, cvar_mp_color->z - 0.01f); + }*/ + for ( auto img : allMPBarImages ) + { + if ( img ) + { + Uint8 r, g, b, a; + getColor(img->color, &r, &g, &b, &a); + r = 255 * mpColor.x; + g = 255 * mpColor.y; + b = 255 * mpColor.z; + //a *= 255 * cvar_mp_color->w; + img->color = makeColor(r, g, b, a); + } + } + if ( player.magic.noManaProcessedOnTick != 0 ) { if ( ticks != player.magic.noManaProcessedOnTick ) @@ -31534,7 +34313,7 @@ void Player::HUD_t::updateMPBar() } } - mpFrame->setDisabled(player.ghost.isActive()); + mpFrame->setDisabled(player.ghost.isActive() && !player.entity); } bool hotbar_slot_t::matchesExactLastItem(int player, Item* item) @@ -31906,7 +34685,7 @@ void Player::Hotbar_t::updateHotbar() if ( controller ) { glyph->disabled = slot->isDisabled(); - if ( !player.shootmode || !player.entity ) + if ( !player.shootmode || !player.entity || player.ghost.isActive() ) { glyph->disabled = true; } @@ -32116,6 +34895,13 @@ void Player::Hotbar_t::updateHotbar() } } + Item* hotbarItem = uidToItem(hotbar[num].item); + slotItem->setUserData(nullptr); + if ( hotbarItem && hotbarItem->type == SPELL_ITEM ) + { + slotItem->setUserData(&GAMEUI_FRAMEDATA_SPELL_LEARNABLE); + } + if ( current_hotbar == num ) { bool showHighlightedSlot = true; @@ -32145,7 +34931,12 @@ void Player::Hotbar_t::updateHotbar() highlightSlot->setSize(pos); // this follows the slots around highlightSlotImg->disabled = false; - updateSlotFrameFromItem(highlightSlotItem, uidToItem(hotbar[num].item)); + highlightSlotItem->setUserData(nullptr); + if ( hotbarItem && hotbarItem->type == SPELL_ITEM ) + { + highlightSlotItem->setUserData(&GAMEUI_FRAMEDATA_SPELL_LEARNABLE); + } + updateSlotFrameFromItem(highlightSlotItem, hotbarItem); if ( player.inventoryUI.frame ) { @@ -32203,18 +34994,18 @@ void Player::Hotbar_t::updateHotbar() { highlightSlotImg->disabled = true; highlightSlotItem->setDisabled(true); - updateSlotFrameFromItem(slotItem, uidToItem(hotbar[num].item)); + updateSlotFrameFromItem(slotItem, hotbarItem); } } } else { - updateSlotFrameFromItem(slotItem, uidToItem(hotbar[num].item)); + updateSlotFrameFromItem(slotItem, hotbarItem); } } else { - updateSlotFrameFromItem(slotItem, uidToItem(hotbar[num].item)); + updateSlotFrameFromItem(slotItem, hotbarItem); } } } @@ -32464,7 +35255,7 @@ void Player::SkillSheet_t::createSkillSheet() profName->setTextColor(skillSheetData.defaultTextColor); if ( i < skillSheetData.skillEntries.size() ) { - profName->setText(skillSheetData.skillEntries[i].name.c_str()); + profName->setText(skillSheetData.skillEntries[i].getSkillName(true).c_str()); } else { @@ -32515,7 +35306,7 @@ void Player::SkillSheet_t::createSkillSheet() profName->setTextColor(skillSheetData.defaultTextColor); if ( i < skillSheetData.skillEntries.size() ) { - profName->setText(skillSheetData.skillEntries[i].name.c_str()); + profName->setText(skillSheetData.skillEntries[i].getSkillName(true).c_str()); } else { @@ -32618,7 +35409,7 @@ void Player::SkillSheet_t::createSkillSheet() auto scrollAreaFrame = scrollAreaOuterFrame->addFrame("skill scroll area"); scrollAreaFrame->setHollow(true); skillSheetEntryFrames[player.playernum].scrollArea = scrollAreaFrame; - scrollAreaFrame->setSize(SDL_Rect{ 0, 0, scrollAreaOuterFrame->getSize().w, 1000 }); + scrollAreaFrame->setSize(SDL_Rect{ 0, 0, scrollAreaOuterFrame->getSize().w, 4000 }); SDL_Rect txtPos{ 0, 0, scrollAreaFrame->getSize().w, scrollAreaFrame->getSize().h }; { @@ -32703,7 +35494,6 @@ void Player::SkillSheet_t::createSkillSheet() char effectFrameName[64] = ""; char effectFieldName[64] = ""; char effectFieldVal[64] = ""; - char effectBgName[64]; int effectXOffset = 72; // tmp paramters - configured in skillsheet json int effectBackgroundXOffset = 8; int effectBackgroundWidth = 80; @@ -32766,7 +35556,7 @@ void Player::SkillSheet_t::createSkillSheet() auto effectValFrame = effectFrame->addFrame("effect val frame"); effectValFrame->setHollow(true); effectValFrame->setSize(SDL_Rect{ valueX, 0, effectXOffset, effectFrame->getSize().h - 4 }); - auto effectVal = effectValFrame->addField("effect val", 1024); + auto effectVal = effectValFrame->addField("effect val", 2048); effectVal->setFont(descFont); effectVal->setSize(SDL_Rect{ 0, 0, 1000, effectValFrame->getSize().h }); // large 1000px to handle large text length marquee effectVal->setVJustify(Field::justify_t::CENTER); @@ -32969,7 +35759,7 @@ void Player::SkillSheet_t::openSkillSheet() std::string formatSkillSheetEffects(int playernum, int proficiency, std::string& tag, std::string& rawValue) { - char buf[1024] = ""; + char buf[2048] = ""; if ( !players[playernum] ) { return ""; } Entity* player = players[playernum]->entity; real_t val = 0.0; @@ -33089,7 +35879,7 @@ std::string formatSkillSheetEffects(int playernum, int proficiency, std::string& } else if ( tag == "RANGED_PIERCE_CHANCE" ) { - val = std::min(std::max(statGetPER(stats[playernum], player) / 2, 0), 50); + val = std::min(std::max(statGetPER(stats[playernum], player), 0), 50); snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); } return buf; @@ -33530,37 +36320,37 @@ std::string formatSkillSheetEffects(int playernum, int proficiency, std::string& } return buf; } - else if ( proficiency == PRO_SWIMMING ) - { - if ( tag == "SWIM_SPEED_TOTAL" ) - { - val = (((stats[playernum]->getModifiedProficiency(proficiency) / 100.f) * 50.f) + 50); // water movement speed - if ( stats[playernum]->type == SKELETON ) - { - val *= .5; - } - snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); - } - else if ( tag == "SWIM_SPEED_BASE" ) - { - val = -50.0; // water movement speed - if ( stats[playernum]->type == SKELETON ) - { - val -= 25.0; - } - snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); - } - else if ( tag == "SWIM_SPEED_BONUS" ) - { - val = (((stats[playernum]->getModifiedProficiency(proficiency) / 100.f) * 50.f)); // water movement speed - if ( stats[playernum]->type == SKELETON ) - { - val *= .5; - } - snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); - } - return buf; - } + //else if ( proficiency == PRO_LEGACY_SWIMMING ) + //{ + // if ( tag == "SWIM_SPEED_TOTAL" ) + // { + // val = (((stats[playernum]->getModifiedProficiency(proficiency) / 100.f) * 50.f) + 50); // water movement speed + // if ( stats[playernum]->type == SKELETON ) + // { + // val *= .5; + // } + // snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); + // } + // else if ( tag == "SWIM_SPEED_BASE" ) + // { + // val = -50.0; // water movement speed + // if ( stats[playernum]->type == SKELETON ) + // { + // val -= 25.0; + // } + // snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); + // } + // else if ( tag == "SWIM_SPEED_BONUS" ) + // { + // val = (((stats[playernum]->getModifiedProficiency(proficiency) / 100.f) * 50.f)); // water movement speed + // if ( stats[playernum]->type == SKELETON ) + // { + // val *= .5; + // } + // snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); + // } + // return buf; + //} else if ( proficiency == PRO_LEADERSHIP ) { if ( tag == "LEADER_MAX_FOLLOWERS" ) @@ -33690,31 +36480,129 @@ std::string formatSkillSheetEffects(int playernum, int proficiency, std::string& } else if ( tag == "APPRAISE_MAX_GOLD_VALUE" ) { - if ( skillCapstoneUnlocked(playernum, proficiency) ) - { - snprintf(buf, sizeof(buf), "%s", Language::get(4065)); // "any" - } - else + //if ( skillCapstoneUnlocked(playernum, proficiency) ) + //{ + // snprintf(buf, sizeof(buf), "%s", Language::get(6965)); // "any" + //} + //else { - val = 10 * (stats[playernum]->getModifiedProficiency(proficiency) + (statGetPER(stats[playernum], player) * 5)); // max gold value can appraise - if ( val < 0.0 ) + int skillLVL = (stats[playernum]->getModifiedProficiency(proficiency) + (statGetPER(stats[playernum], player) * Player::Inventory_t::Appraisal_t::perStatMult)); // max gold value can appraise + if ( skillLVL < 0 ) { snprintf(buf, sizeof(buf), "??? Gold"); } - else if ( val < 0.1 ) + else { - val = 9; - snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); + for ( auto& table : Player::Inventory_t::Appraisal_t::appraisal_tables ) + { + if ( skillLVL >= table.skillLVL ) + { + val = table.goldValueLimit; + break; + } + } + if ( val > 99999 ) + { + snprintf(buf, sizeof(buf), "%s", Language::get(4065)); // "any" + } + else + { + snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); + } + } + } + } + else if ( tag == "APPRAISE_GOLD_FAST" ) + { + //if ( skillCapstoneUnlocked(playernum, proficiency) ) + //{ + // snprintf(buf, sizeof(buf), "%s", Language::get(4065)); // "any" + //} + //else + { + int skillLVL = (stats[playernum]->getModifiedProficiency(proficiency) + (statGetPER(stats[playernum], player) * Player::Inventory_t::Appraisal_t::perStatMult)); // max gold value can appraise + if ( skillLVL < 0 ) + { + snprintf(buf, sizeof(buf), "??? Gold"); } else { + for ( auto& table : Player::Inventory_t::Appraisal_t::appraisal_tables ) + { + if ( skillLVL >= table.skillLVL ) + { + val = table.fastTimeGold; + break; + } + } snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); } } } + else if ( tag == "APPRAISE_SPEEDUP" ) + { + int skillLVL = (stats[playernum]->getModifiedProficiency(proficiency) + (statGetPER(stats[playernum], player) * Player::Inventory_t::Appraisal_t::perStatMult)); + /*if ( skillCapstoneUnlocked(playernum, proficiency) ) + { + val = 100.0; + } + else */ + if ( skillLVL >= 50 ) + { + real_t capstone = skillCapstoneUnlocked(playernum, proficiency) ? 0.75 : 1.0; + real_t ratio = (1.0 - capstone * std::max(0.2, 0.5 + (100 - skillLVL) / 100.0)) * 100.0; + val = ratio; + val = std::min(100.0, val); + } + else + { + val = 0.0; + } + snprintf(buf, sizeof(buf), rawValue.c_str(), val); + } + else if ( tag == "APPRAISE_SPEEDUP_CONSUMABLES" ) + { + int skillLVL = (stats[playernum]->getModifiedProficiency(proficiency) + (statGetPER(stats[playernum], player) * Player::Inventory_t::Appraisal_t::perStatMult)); + /*if ( skillCapstoneUnlocked(playernum, proficiency) ) + { + val = 100.0; + } + else */ + if ( skillLVL >= 0 ) + { + real_t capstone = skillCapstoneUnlocked(playernum, proficiency) ? 0.75 : 1.0; + real_t ratio = (1.0 - capstone * std::max(0.2, 1.0 + (-skillLVL) / 100.0)) * 100.0; + val = ratio; + val = std::min(100.0, val); + } + else + { + val = 0.0; + } + snprintf(buf, sizeof(buf), rawValue.c_str(), val); + } + else if ( tag == "APPRAISE_FLOOR_BONUS" ) + { + int skillLVL = std::min(100, std::max(0, (stats[playernum]->getModifiedProficiency(proficiency) + (statGetPER(stats[playernum], player) * Player::Inventory_t::Appraisal_t::perStatMult)))); + /*if ( skillCapstoneUnlocked(playernum, proficiency) ) + { + val = 100.0; + } + else */ + if ( skillLVL >= 0 ) + { + real_t appraisalTimerReduce = (1.0 - (0.75 - (0.25 * skillLVL) / 100.0)) * 100.0; + val = appraisalTimerReduce; + } + else + { + val = 0.0; + } + snprintf(buf, sizeof(buf), rawValue.c_str(), val); + } else if ( tag == "APPRAISE_WORTHLESS_GLASS" ) { - if ( (stats[playernum]->getModifiedProficiency(proficiency) + (statGetPER(stats[playernum], player) * 5)) >= 100 ) + if ( (stats[playernum]->getModifiedProficiency(proficiency) + (statGetPER(stats[playernum], player) * Player::Inventory_t::Appraisal_t::perStatMult)) >= 40 ) { snprintf(buf, sizeof(buf), rawValue.c_str(), Language::get(1314)); // yes } @@ -33923,120 +36811,14 @@ std::string formatSkillSheetEffects(int playernum, int proficiency, std::string& } return buf; } - else if ( proficiency == PRO_SPELLCASTING ) + /*else if ( proficiency == PRO_LEGACY_SPELLCASTING ) { - if ( tag == "CASTING_MP_REGEN" ) - { - if ( (stats[playernum])->playerRace == RACE_INSECTOID && (stats[playernum])->stat_appearance == 0 ) - { - return Language::get(4066); - } - else if ( (stats[playernum])->type == AUTOMATON ) - { - return Language::get(4067); - } - else - { - val = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); - snprintf(buf, sizeof(buf), rawValue.c_str(), val); - } - } - else if ( tag == "CASTING_MP_REGEN_SKILL_MULTIPLIER" ) - { - if ( (stats[playernum])->playerRace == RACE_INSECTOID && (stats[playernum])->stat_appearance == 0 ) - { - return Language::get(4066); - } - else if ( (stats[playernum])->type == AUTOMATON ) - { - return Language::get(4067); - } - else - { - val = 0.0; - int skill = stats[playernum]->getModifiedProficiency(proficiency); - int multiplier = (skill / 20) + 1; - val = multiplier; - //real_t normalValue = player->getBaseManaRegen(*(stats[playernum])) / (TICKS_PER_SECOND * 1.f); - //stats[playernum]->getModifiedProficiency(proficiency) = 0; - //real_t zeroValue = player->getBaseManaRegen(*(stats[playernum])) / (TICKS_PER_SECOND * 1.f); - //stats[playernum]->getModifiedProficiency(proficiency) = skill; - // - //val = (100 * zeroValue / normalValue) - 100; - snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); - } - } - else if ( tag == "CASTING_MP_REGEN_SKILL_BONUS" ) - { - val = 0.0; - int skill = stats[playernum]->getProficiency(proficiency); - real_t normalValue = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); - stats[playernum]->setProficiencyUnsafe(proficiency, -999); - real_t zeroValue = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); - stats[playernum]->setProficiency(proficiency, skill); - - val = (100 * zeroValue / normalValue) - 100; - snprintf(buf, sizeof(buf), rawValue.c_str(), val); - } - else if ( tag == "CASTING_MP_REGEN_BONUS_INT" ) - { - val = 0.0; - int stat = stats[playernum]->INT; - real_t normalValue = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); - stats[playernum]->INT = 0; - real_t zeroValue = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); - stats[playernum]->INT = stat; - - val = (100 * zeroValue / normalValue) - 100; - snprintf(buf, sizeof(buf), rawValue.c_str(), val); - } - else if ( tag == "CASTING_BEGINNER" ) - { - if ( isSpellcasterBeginner(playernum, player) ) - { - snprintf(buf, sizeof(buf), rawValue.c_str(), Language::get(1314)); // yes - } - else - { - snprintf(buf, sizeof(buf), rawValue.c_str(), Language::get(1315)); // no - } - } - else if ( tag == "CASTING_SPELLBOOK_FUMBLE" ) - { - int skillLVL = std::min(std::max(0, stats[playernum]->getModifiedProficiency(proficiency) + statGetINT(stats[playernum], player)), 100); - skillLVL /= 20; - std::string tierName = Language::get(4061); - tierName += " "; - if ( skillLVL <= 0 ) - { - tierName += "I"; - } - else if ( skillLVL == 1 ) - { - tierName += "II"; - } - else if ( skillLVL == 2 ) - { - tierName += "III"; - } - else if ( skillLVL == 3 ) - { - tierName += "IV"; - } - else if ( skillLVL == 4 ) - { - tierName += "V"; - } - else if ( skillLVL >= 5 ) - { - tierName += "VI"; - } - snprintf(buf, sizeof(buf), rawValue.c_str(), tierName.c_str()); - } return buf; - } - else if ( proficiency == PRO_MAGIC ) + }*/ + else if ( proficiency == PRO_SORCERY || proficiency == PRO_MYSTICISM || proficiency == PRO_THAUMATURGY || proficiency == NUMPROFICIENCIES ) { + static ConsoleVariable cvar_skillsheet_magic_namelen("/skillsheet_magic_namelen", 14); + snprintf(buf, sizeof(buf), "%d", 0); if ( tag == "MAGIC_CURRENT_TIER" ) { std::string tierName = Language::get(4061); @@ -34078,23 +36860,115 @@ std::string formatSkillSheetEffects(int playernum, int proficiency, std::string& } else if ( tag == "MAGIC_SPELLPOWER_TOTAL" ) { - val = (getBonusFromCasterOfSpellElement(player, stats[playernum], nullptr, SPELL_NONE) * 100.0); + if ( auto spell = players[playernum]->magic.selectedSpell() ) + { + val = (getBonusFromCasterOfSpellElement(player, stats[playernum], nullptr, spell->ID, proficiency) * 100.0); + } + else + { + val = (getBonusFromCasterOfSpellElement(player, stats[playernum], nullptr, SPELL_NONE, proficiency) * 100.0); + } snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); } else if ( tag == "MAGIC_SPELLPOWER_INT" ) { //val = (getBonusFromCasterOfSpellElement(player, stats[playernum], nullptr, SPELL_NONE) * 100.0); - real_t bonus = getSpellBonusFromCasterINT(players[playernum]->entity, stats[playernum]); + real_t bonus = getSpellBonusFromCasterINT(players[playernum]->entity, stats[playernum], proficiency); val = bonus * 100.0; snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); } else if ( tag == "MAGIC_SPELLPOWER_EQUIPMENT" ) { - val = (getBonusFromCasterOfSpellElement(player, stats[playernum], nullptr, SPELL_NONE) * 100.0); - real_t bonus = getSpellBonusFromCasterINT(players[playernum]->entity, stats[playernum]); + if ( auto spell = players[playernum]->magic.selectedSpell() ) + { + val = (getBonusFromCasterOfSpellElement(player, stats[playernum], nullptr, spell->ID, proficiency) * 100.0); + } + else + { + val = (getBonusFromCasterOfSpellElement(player, stats[playernum], nullptr, SPELL_NONE, proficiency) * 100.0); + } + real_t bonus = getSpellBonusFromCasterINT(players[playernum]->entity, stats[playernum], proficiency); val -= bonus * 100.0; snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); } + else if ( tag == "MAGIC_MEMORIZED_LEVEL_SPELLS" ) + { + int skillLVL = stats[playernum]->getProficiency(proficiency); + std::string magics = ""; + std::set inserted; + for ( node_t* node = players[playernum]->magic.spellList.first; node; node = node->next ) + { + if ( skillLVL >= SKILL_LEVEL_LEGENDARY ) + { + break; + } + if ( node && node->element ) + { + if ( spell_t* spell = (spell_t*)node->element ) + { + if ( spell->skillID != proficiency ) + { + continue; + } + if ( spell->ID == SPELL_GHOST_BOLT + || spell->ID == SPELL_SLIME_ACID + || spell->ID == SPELL_SLIME_FIRE + || spell->ID == SPELL_SLIME_WATER + || spell->ID == SPELL_SLIME_TAR + || spell->ID == SPELL_SLIME_METAL + || spell->hide_from_ui == true ) + { + if ( spellIsNaturallyLearnedByRaceOrClass(players[playernum]->entity, + *stats[playernum], spell->ID, playernum) ) + { + // show these spells + } + else + { + continue; + } + } + if ( skillLVL < std::min(SKILL_LEVEL_LEGENDARY, (spell->difficulty + 20)) ) + { + if ( inserted.find(spell->ID) != inserted.end() ) + { + continue; + } + inserted.insert(spell->ID); + if ( strcmp(spell->getSpellName(), "") ) + { + if ( magics != "" ) + { + magics += '\n'; + } + magics += "\x1E "; + if ( players[playernum]->bUseCompactGUIHeight() || players[playernum]->bUseCompactGUIWidth() ) + { + std::string name = spell->getSpellName(); + if ( name.size() > *cvar_skillsheet_magic_namelen ) + { + name = name.substr(0, *cvar_skillsheet_magic_namelen - 1); + name += ".."; + magics += name; + } + else + { + magics += name; + } + } + else + { + magics += spell->getSpellName(); + } + } + } + } + } + } + + if ( magics == "" ) { magics = "-"; } + snprintf(buf, sizeof(buf), rawValue.c_str(), magics.c_str()); + } else if ( tag == "MAGIC_CURRENT_TIER_SPELLS" ) { int skillLVL = std::min(stats[playernum]->getModifiedProficiency(proficiency) + statGetINT(stats[playernum], player), 100); @@ -34104,47 +36978,203 @@ std::string formatSkillSheetEffects(int playernum, int proficiency, std::string& } std::string magics = ""; std::set inserted; - for ( auto it = allGameSpells.begin(); it != allGameSpells.end(); ++it ) + for ( int i = SPELL_NONE + 1; i < NUM_SPELLS; ++i ) { - auto spellEntry = *it; - if ( !spellEntry ) - { - continue; - } - if ( spellEntry->ID == SPELL_WEAKNESS - || spellEntry->ID == SPELL_GHOST_BOLT - || spellEntry->ID == SPELL_SLIME_ACID - || spellEntry->ID == SPELL_SLIME_FIRE - || spellEntry->ID == SPELL_SLIME_WATER - || spellEntry->ID == SPELL_SLIME_TAR - || spellEntry->ID == SPELL_SLIME_METAL ) + auto find = allGameSpells.find(i); + if ( find != allGameSpells.end() ) { - continue; - } - if ( spellEntry && spellEntry->difficulty == (skillLVL * 20) ) - { - if ( inserted.find(spellEntry->ID) != inserted.end() ) + auto spellEntry = find->second; + if ( !spellEntry ) { continue; } - inserted.insert(spellEntry->ID); - if ( magics != "" ) + if ( spellEntry->skillID != proficiency ) { - magics += '\n'; + continue; + } + if ( spellEntry->ID == SPELL_GHOST_BOLT + || spellEntry->ID == SPELL_SLIME_ACID + || spellEntry->ID == SPELL_SLIME_FIRE + || spellEntry->ID == SPELL_SLIME_WATER + || spellEntry->ID == SPELL_SLIME_TAR + || spellEntry->ID == SPELL_SLIME_METAL + || spellEntry->hide_from_ui == true ) + { + if ( spellIsNaturallyLearnedByRaceOrClass(players[playernum]->entity, + *stats[playernum], spellEntry->ID, playernum) ) + { + // show these spells + } + else + { + continue; + } + } + if ( spellEntry && spellEntry->difficulty == (skillLVL * 20) ) + { + if ( inserted.find(spellEntry->ID) != inserted.end() ) + { + continue; + } + inserted.insert(spellEntry->ID); + if ( magics != "" ) + { + magics += '\n'; + } + magics += "\x1E "; + if ( players[playernum]->bUseCompactGUIHeight() || players[playernum]->bUseCompactGUIWidth() ) + { + std::string name = spellEntry->getSpellName(); + if ( name.size() > *cvar_skillsheet_magic_namelen ) + { + name = name.substr(0, *cvar_skillsheet_magic_namelen - 1); + name += ".."; + magics += name; + } + else + { + magics += name; + } + } + else + { + magics += spellEntry->getSpellName(); + } } - magics += "\x1E "; - magics += spellEntry->getSpellName(); } } if ( magics == "" ) { magics = "-"; } snprintf(buf, sizeof(buf), rawValue.c_str(), magics.c_str()); } + if ( tag == "CASTING_MP_REGEN" ) + { + if ( (stats[playernum])->playerRace == RACE_INSECTOID && (stats[playernum])->stat_appearance == 0 ) + { + return Language::get(4066); + } + else if ( (stats[playernum])->type == AUTOMATON ) + { + return Language::get(4067); + } + else + { + val = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); + snprintf(buf, sizeof(buf), rawValue.c_str(), val); + } + } + else if ( tag == "CASTING_MP_REGEN_SKILL_MULTIPLIER" ) + { + if ( (stats[playernum])->playerRace == RACE_INSECTOID && (stats[playernum])->stat_appearance == 0 ) + { + return Language::get(4066); + } + else if ( (stats[playernum])->type == AUTOMATON ) + { + return Language::get(4067); + } + else + { + val = 0.0; + int skill = stats[playernum]->getModifiedProficiency(proficiency); + int multiplier = (skill / 20) + 1; + val = multiplier; + //real_t normalValue = player->getBaseManaRegen(*(stats[playernum])) / (TICKS_PER_SECOND * 1.f); + //stats[playernum]->getModifiedProficiency(proficiency) = 0; + //real_t zeroValue = player->getBaseManaRegen(*(stats[playernum])) / (TICKS_PER_SECOND * 1.f); + //stats[playernum]->getModifiedProficiency(proficiency) = skill; + // + //val = (100 * zeroValue / normalValue) - 100; + snprintf(buf, sizeof(buf), rawValue.c_str(), (int)val); + } + } + else if ( tag == "CASTING_MP_REGEN_SKILL_BONUS" ) + { + val = 0.0; + int skill = stats[playernum]->getProficiency(proficiency); + real_t normalValue = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); + stats[playernum]->setProficiencyUnsafe(proficiency, -999); + real_t zeroValue = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); + stats[playernum]->setProficiency(proficiency, skill); + + val = (100 * zeroValue / normalValue) - 100; + snprintf(buf, sizeof(buf), rawValue.c_str(), val); + } + else if ( tag == "CASTING_MP_REGEN_BONUS_INT" ) + { + val = 0.0; + int stat = stats[playernum]->INT; + real_t normalValue = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); + stats[playernum]->INT = 0; + real_t zeroValue = getBaseManaRegen(player, *(stats[playernum])) / (TICKS_PER_SECOND * 1.f); + stats[playernum]->INT = stat; + + val = (100 * zeroValue / normalValue) - 100; + snprintf(buf, sizeof(buf), rawValue.c_str(), val); + } + else if ( tag == "CASTING_BEGINNER" ) + { + //if ( isSpellcasterBeginner(playernum, player, proficiency) ) + int skillLVL = std::min(std::max(0, stats[playernum]->getModifiedProficiency(proficiency) + statGetINT(stats[playernum], player)), 100); + if ( skillLVL < SKILL_LEVEL_BASIC ) + { + snprintf(buf, sizeof(buf), rawValue.c_str(), Language::get(1314)); // yes + } + else + { + snprintf(buf, sizeof(buf), rawValue.c_str(), Language::get(1315)); // no + } + } + else if ( tag == "CASTING_SPELLBOOK_FUMBLE" ) + { + int skillLVL = std::min(std::max(0, stats[playernum]->getModifiedProficiency(proficiency) + statGetINT(stats[playernum], player)), 100); + skillLVL /= 20; + std::string tierName = Language::get(4061); + tierName += " "; + if ( skillLVL <= 0 ) + { + tierName += "I"; + } + else if ( skillLVL == 1 ) + { + tierName += "II"; + } + else if ( skillLVL == 2 ) + { + tierName += "III"; + } + else if ( skillLVL == 3 ) + { + tierName += "IV"; + } + else if ( skillLVL == 4 ) + { + tierName += "V"; + } + else if ( skillLVL >= 5 ) + { + tierName += "VI"; + } + snprintf(buf, sizeof(buf), rawValue.c_str(), tierName.c_str()); + } return buf; } return ""; } +std::string Player::SkillSheet_t::getSkillNameFromID(int skillID, bool shortName) +{ + for ( auto& entry : Player::SkillSheet_t::skillSheetData.skillEntries ) + { + if ( entry.skillId == skillID ) + { + return entry.getSkillName(shortName); + } + } + + return ""; +} + void Player::SkillSheet_t::selectSkill(int skill) { selectedSkill = skill; @@ -34697,14 +37727,14 @@ void Player::SkillSheet_t::processSkillSheet() std::string rightBinary = "00000000"; for ( auto& skillEntry : skillSheetData.skillEntries ) { - if ( index >= 8 ) + if ( index >= 8 && index < 16 ) { if ( stats[player.playernum]->getModifiedProficiency(skillEntry.skillId) >= SKILL_LEVEL_LEGENDARY ) { leftBinary[7 - (index - 8)] = '1'; } } - else + else if ( index < 8 ) { if ( stats[player.playernum]->getModifiedProficiency(skillEntry.skillId) >= SKILL_LEVEL_LEGENDARY ) { @@ -35279,7 +38309,7 @@ void Player::SkillSheet_t::processSkillSheet() //DebugTimers.addTimePoint("skill", "post resize"); - if ( selectedSkill >= 0 && selectedSkill < skillSheetData.skillEntries.size() ) + if ( selectedSkill >= 0 && selectedSkill < skillSheetData.skillEntries.size() && selectedSkill < NUMPROFICIENCIES ) { int proficiency = skillSheetData.skillEntries[selectedSkill].skillId; int proficiencyValue = stats[player.playernum]->getModifiedProficiency(proficiency); @@ -35357,7 +38387,7 @@ void Player::SkillSheet_t::processSkillSheet() if ( !bSkillSheetEntryLoaded ) { auto skillTitleTxt = innerFrame->findField("skill title txt"); - skillTitleTxt->setText(skillSheetData.skillEntries[selectedSkill].name.c_str()); + skillTitleTxt->setText(skillSheetData.skillEntries[selectedSkill].getSkillName().c_str()); auto statTypeTxt = scrollArea->findField("stat type txt"); auto statIcon = scrollArea->findImage("stat icon"); @@ -35453,6 +38483,8 @@ void Player::SkillSheet_t::processSkillSheet() auto effectFrameBgImgTmp = scrollArea->findImage("effect frame bg tmp"); effectFrameBgImgTmp->disabled = false; + static ConsoleVariable cvar_skillsheet_skip_spell_list("/skillsheet_skip_spell_list", true); + for ( int eff = 0; eff < 10; ++eff ) { auto effectFrame = skillSheetEntryFrames[player.playernum].effectFrames[eff]; @@ -35470,11 +38502,18 @@ void Player::SkillSheet_t::processSkillSheet() { effectFramePos.y += moveEffectsOffsetY; } - effectFrame->setSize(effectFramePos); - - effectFrame->setDisabled(false); auto& effect_t = skillSheetData.skillEntries[selectedSkill].effects[eff]; + if ( proficiency == PRO_SORCERY || proficiency == PRO_THAUMATURGY || proficiency == PRO_MYSTICISM ) + { + if ( *cvar_skillsheet_skip_spell_list && effect_t.tag == "MAGIC_CURRENT_TIER_SPELLS" ) + { + continue; + } + } + + effectFrame->setSize(effectFramePos); + effectFrame->setDisabled(false); bool bEffUpdated = false; if ( (effect_t.bAllowRealtimeUpdate && (ticks % (std::max(TICKS_PER_SECOND, MAXPLAYERS * 10))) == (player.playernum * 10)) @@ -35522,7 +38561,15 @@ void Player::SkillSheet_t::processSkillSheet() auto effectTxtFrame = effectFrame->findFrame("effect txt frame"); auto effectTxt = effectTxtFrame->findField("effect txt"); auto effectBgImgFrame = effectFrame->findFrame("effect val bg frame"); + effectTxt->setText(effect_t.title.c_str()); + if ( player.bUseCompactGUIHeight() || player.bUseCompactGUIWidth() ) + { + if ( effect_t.titleShort != "" ) + { + effectTxt->setText(effect_t.titleShort.c_str()); + } + } { // adjust position to match width of container @@ -36390,9 +39437,9 @@ void Player::Inventory_t::SpellPanel_t::updateSpellPanel() sliderCapBot->pos.y = sliderPos.y + sliderPos.h - sliderCapBot->pos.h; } - auto skillBg = baseFrame->findImage("spell skill bg"); + /*auto skillBg = baseFrame->findImage("spell skill bg"); skillBg->pos.x = 14; - skillBg->pos.y = 6; + skillBg->pos.y = 6;*/ int lowestItemY = getNumSpellsToDisplayVertical() - 1; for ( node_t* node = stats[player.playernum]->inventory.first; node != NULL; node = node->next ) @@ -36470,24 +39517,99 @@ void Player::Inventory_t::SpellPanel_t::updateSpellPanel() } } + auto filterGlyph = baseFrame->findImage("filter spell glyph"); + filterGlyph->disabled = true; + auto filterBtn = baseFrame->findButton("spell filter button"); + filterBtn->setDisabled(true); + SDL_Rect filterBtnPos = filterBtn->getSize(); + filterBtnPos.x = 14; + filterBtnPos.y = 6; + filterBtn->setSize(filterBtnPos); + if ( inputs.getVirtualMouse(player.playernum)->draw_cursor ) + { + filterBtn->setDisabled(!isInteractable); + if ( isInteractable ) + { + buttonSpellUpdateSelectorOnHighlight(player.playernum, filterBtn); + } + } + else if ( filterBtn->isSelected() ) + { + filterBtn->deselect(); + } + if ( filterBtn->isDisabled() && usingGamepad ) + { + SDL_Rect filterBtnPos = filterBtn->getSize(); + filterBtnPos.x -= 8; + filterBtn->setSize(filterBtnPos); + + filterGlyph->path = Input::inputs[player.playernum].getGlyphPathForBinding("MenuAlt2"); + if ( auto imgGet = Image::get(filterGlyph->path.c_str()) ) + { + filterGlyph->pos.w = imgGet->getWidth(); + filterGlyph->pos.h = imgGet->getHeight(); + filterGlyph->disabled = false; + } + filterGlyph->pos.x = filterBtn->getSize().x + filterBtn->getSize().w; + if ( filterGlyph->pos.x % 2 == 1 ) + { + ++filterGlyph->pos.x; + } + filterGlyph->pos.y = filterBtn->getSize().y + filterBtn->getSize().h / 2 - filterGlyph->pos.h / 2; + if ( filterGlyph->pos.y % 2 == 1 ) + { + ++filterGlyph->pos.y; + } + } + auto skillIcon = baseFrame->findImage("spell skill icon"); - skillIcon->pos.x = skillBg->pos.x + 4; - skillIcon->pos.y = skillBg->pos.y + 4; - for ( auto& skillEntry : Player::SkillSheet_t::skillSheetData.skillEntries ) + auto filterFrame = baseFrame->findFrame("filter frame"); + auto filterText = filterFrame->findField("filter txt"); + filterText->setDisabled(true); + auto filterTooltipImg = filterFrame->findImage("spell filter tooltip img"); + filterTooltipImg->disabled = true; + if ( spellFilterBySkill > 0 ) { - if ( skillEntry.skillId == PRO_MAGIC ) + skillIcon->pos.x = filterBtn->getSize().x + 4; + skillIcon->pos.y = filterBtn->getSize().y + 4; + skillIcon->pos.w = 24; + skillIcon->pos.h = 24; + for ( auto& skillEntry : Player::SkillSheet_t::skillSheetData.skillEntries ) { - if ( skillCapstoneUnlocked(player.playernum, PRO_MAGIC) ) - { - skillIcon->path = skillEntry.skillIconPathLegend.c_str(); - } - else + if ( skillEntry.skillId == spellFilterBySkill ) { - skillIcon->path = skillEntry.skillIconPath.c_str(); + if ( skillCapstoneUnlocked(player.playernum, skillEntry.skillId) ) + { + skillIcon->path = skillEntry.skillIconPathLegend.c_str(); + } + else + { + skillIcon->path = skillEntry.skillIconPath.c_str(); + } + char buf[128]; + snprintf(buf, sizeof(buf), Language::get(6842), skillEntry.getSkillName().c_str()); + filterText->setText(buf); + filterText->setDisabled(false); + filterTooltipImg->disabled = false; + filterFrame->setDisabled(false); + real_t opacity = filterFrame->getOpacity() / 100.0; + real_t opacityChange = .05 * getFPSScale(144.0); + opacity = std::min(opacity + opacityChange, 1.0); + filterFrame->setOpacity(opacity * 100.0); + break; } - break; } } + else + { + skillIcon->pos.x = filterBtn->getSize().x; + skillIcon->pos.y = filterBtn->getSize().y; + skillIcon->path = "#*images/ui/Inventory/HUD_Magic_Filter_All.png"; + skillIcon->pos.w = 32; + skillIcon->pos.h = 32; + filterFrame->setDisabled(true); + filterFrame->setOpacity(0.0); + } if ( bOpen && isInteractable ) { @@ -36512,20 +39634,23 @@ void Player::Inventory_t::SpellPanel_t::updateSpellPanel() } } - if ( input.consumeBinaryToggle("MenuScrollDown") ) + if ( abs(scrollSetpoint - scrollAnimateX) < 0.00001 ) { - scrollSetpoint = std::max(scrollSetpoint + player.inventoryUI.getSlotSize(), 0); - if ( player.inventoryUI.cursor.queuedModule == Player::GUI_t::MODULE_SPELLS ) + if ( input.binaryToggle("MenuScrollDown") ) { - player.inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_NONE; + scrollSetpoint = std::max(scrollSetpoint + player.inventoryUI.getSlotSize(), 0); + if ( player.inventoryUI.cursor.queuedModule == Player::GUI_t::MODULE_SPELLS ) + { + player.inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_NONE; + } } - } - else if ( input.consumeBinaryToggle("MenuScrollUp") ) - { - scrollSetpoint = std::max(scrollSetpoint - player.inventoryUI.getSlotSize(), 0); - if ( player.inventoryUI.cursor.queuedModule == Player::GUI_t::MODULE_SPELLS ) + else if ( input.binaryToggle("MenuScrollUp") ) { - player.inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_NONE; + scrollSetpoint = std::max(scrollSetpoint - player.inventoryUI.getSlotSize(), 0); + if ( player.inventoryUI.cursor.queuedModule == Player::GUI_t::MODULE_SPELLS ) + { + player.inventoryUI.cursor.queuedModule = Player::GUI_t::MODULE_NONE; + } } } } @@ -36544,7 +39669,7 @@ void Player::Inventory_t::SpellPanel_t::updateSpellPanel() // slightly faster on gamepad static ConsoleVariable cvar_spell_slider_speed("/spell_slider_speed", 1.f); - const real_t factor = (3.0 * (*cvar_spell_slider_speed + (usingGamepad ? -.25f : 0.f))); + const real_t factor = (3.0 * (*cvar_spell_slider_speed + (usingGamepad ? -.5f : 0.f))); if ( scrollSetpoint - scrollAnimateX > 0.0 ) { setpointDiff = fpsScale * std::max(3.0, (scrollSetpoint - scrollAnimateX)) / factor; @@ -36580,6 +39705,14 @@ void Player::Inventory_t::SpellPanel_t::updateSpellPanel() player.inventoryUI.cycleInventoryTab(); player.inventoryUI.spellPanel.closeSpellPanel(); } + if ( Input::inputs[player.playernum].binaryToggle("MenuAlt2") ) + { + Input::inputs[player.playernum].consumeBinaryToggle("MenuAlt2"); + if ( filterBtn ) + { + filterBtn->getCallback()(*filterBtn); + } + } } } @@ -36686,7 +39819,7 @@ void Player::Inventory_t::SpellPanel_t::scrollToSlot(int x, int y, bool instantl } } -void Player::Inventory_t::ChestGUI_t::openChest() +void Player::Inventory_t::ChestGUI_t::openChest(bool _voidChest) { if ( player.inventoryUI.chestFrame ) { @@ -36709,6 +39842,7 @@ void Player::Inventory_t::ChestGUI_t::openChest() player.hud.compactLayoutMode = Player::HUD_t::COMPACT_LAYOUT_INVENTORY; player.inventory_mode = INVENTORY_MODE_ITEM; bOpen = true; + voidChest = _voidChest; } if ( inputs.getUIInteraction(player.playernum)->selectedItem ) { @@ -36750,6 +39884,7 @@ void Player::Inventory_t::ChestGUI_t::closeChest() scrollInertia = 0.0; scrollAnimateX = scrollSetpoint; bOpen = false; + voidChest = false; bFirstTimeSnapCursor = false; if ( inputs.getUIInteraction(player.playernum)->selectedItemFromChest > 0 ) { @@ -36802,9 +39937,9 @@ const bool Player::Inventory_t::isItemFromChest(Item* item) const { chest_inventory = &chestInv[player.playernum]; } - else if ( openedChest[player.playernum]->children.first && openedChest[player.playernum]->children.first->element ) + else if ( openedChest[player.playernum] ) { - chest_inventory = (list_t*)openedChest[player.playernum]->children.first->element; + chest_inventory = openedChest[player.playernum]->getChestInventoryList(); } if ( item->node && item->node->list == chest_inventory ) @@ -36855,6 +39990,16 @@ void Player::Inventory_t::ChestGUI_t::updateChest() auto chestFramePos = chestFrame->getSize(); auto baseBackgroundImg = baseFrame->findImage("chest base img"); auto lidBackgroundImg = baseFrame->findImage("chest lid img"); + if ( voidChest ) + { + baseBackgroundImg->path = "*#images/ui/Inventory/chests/Chest_Main_Void_00.png"; + lidBackgroundImg->path = "*#images/ui/Inventory/chests/Chest_Top_Void_00.png"; + } + else + { + baseBackgroundImg->path = "*#images/ui/Inventory/chests/Chest_Main_00.png"; + lidBackgroundImg->path = "*#images/ui/Inventory/chests/Chest_Top_00.png"; + } int playercount = 0; for ( int c = 0; c < MAXPLAYERS; ++c ) { @@ -37951,7 +41096,8 @@ bool Player::WorldUI_t::WorldTooltipItem_t::isItemSameAsCurrent(Item* item) && item->beatitude == beatitude && item->count == count && item->appearance == appearance - && item->identified == identified ) + && item->identified == identifiedItem + && hasAppraiseCapstone == stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) >= SKILL_LEVEL_LEGENDARY ) { return true; } @@ -37976,7 +41122,8 @@ SDL_Surface* Player::WorldUI_t::WorldTooltipItem_t::blitItemWorldTooltip(Item* i beatitude = item->beatitude; count = item->count; appearance = item->appearance; - identified = item->identified; + identifiedItem = item->identified; + hasAppraiseCapstone = stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) >= SKILL_LEVEL_LEGENDARY; SDL_Rect tooltip; char buf[1024] = ""; @@ -38039,6 +41186,11 @@ SDL_Surface* Player::WorldUI_t::WorldTooltipItem_t::blitItemWorldTooltip(Item* i snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), item->getName(), item->appearance % ENCHANTED_FEATHER_MAX_DURABILITY, item->beatitude); } + else if ( item->type == MAGICSTAFF_SCEPTER && item->identified ) + { + snprintf(buf, sizeof(buf), "%s %s (%d%%) (%+d)", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), + item->getName(), item->appearance % MAGICSTAFF_SCEPTER_CHARGE_MAX, item->beatitude); + } else if ( itemCategory(item) == BOOK ) { snprintf(buf, sizeof(buf), "%s %s", ItemTooltips.getItemStatusAdjective(item->type, item->status).c_str(), @@ -38367,13 +41519,31 @@ SDL_Surface* Player::WorldUI_t::WorldTooltipItem_t::blitItemWorldTooltip(Item* i SDL_BlitScaled(srcSurf, nullptr, itemWorldTooltipSurface, &goldPos); char goldBuf[32]; - if ( !item->identified && itemCategory(item) == GEM ) + if ( !item->identified && stats[player.playernum]->getModifiedProficiency(PRO_APPRAISAL) < SKILL_LEVEL_LEGENDARY ) { - snprintf(goldBuf, sizeof(goldBuf), "%d", items[GEM_GLASS].value); + if ( itemCategory(item) == GEM && item->type != GEM_ROCK ) + { + snprintf(goldBuf, sizeof(goldBuf), "%s", "???"); + } + else + { + if ( item->getGoldValue() < 100 ) + { + snprintf(goldBuf, sizeof(goldBuf), "%s", "?"); + } + else if ( item->getGoldValue() < 1000 ) + { + snprintf(goldBuf, sizeof(goldBuf), "%s", "??"); + } + else + { + snprintf(goldBuf, sizeof(goldBuf), "%s", "???"); + } + } } else { - snprintf(goldBuf, sizeof(goldBuf), "%d", items[item->type].value); + snprintf(goldBuf, sizeof(goldBuf), "%d", item->getGoldValue()); } if ( SDL_Surface* textSurf = const_cast(Text::get(goldBuf, font->getName(), hudColors.characterSheetNeutral, 0)->getSurf()) ) @@ -38437,6 +41607,33 @@ void Player::WorldUI_t::WorldTooltipDialogue_t::update() } } +void Player::WorldUI_t::WorldTooltipDialogue_t::Dialogue_t::updateWorldCoordinates() +{ + auto& setting = WorldDialogueSettings_t::settings[dialogueType]; + Entity* parentEnt = uidToEntity(parent); + if ( parentEnt && setting.followEntity ) + { + if ( TimerExperiments::bUseTimerInterpolation && parentEnt->bUseRenderInterpolation ) + { + x = parentEnt->lerpRenderState.x.position * 16.0; + y = parentEnt->lerpRenderState.y.position * 16.0; + z = parentEnt->lerpRenderState.z.position + setting.offsetZ; + } + else + { + x = parentEnt->x; + y = parentEnt->y; + z = parentEnt->z + setting.offsetZ; + } + } + else if ( parentEnt ) + { + x = parentEnt->x; + y = parentEnt->y; + z = parentEnt->z + setting.offsetZ; + } +} + void Player::WorldUI_t::WorldTooltipDialogue_t::Dialogue_t::update() { if ( !init ) @@ -38469,21 +41666,7 @@ void Player::WorldUI_t::WorldTooltipDialogue_t::Dialogue_t::update() auto& setting = WorldDialogueSettings_t::settings[dialogueType]; - if ( parentEnt && setting.followEntity ) - { - /*if ( parentEnt->bUseRenderInterpolation ) - { - x = parentEnt->lerpRenderState.x.position * 16.0; - y = parentEnt->lerpRenderState.y.position * 16.0; - z = parentEnt->lerpRenderState.z.position + setting.offsetZ; - } - else*/ - { - x = parentEnt->x; - y = parentEnt->y; - z = parentEnt->z + setting.offsetZ; - } - } + updateWorldCoordinates(); real_t dx, dy; auto& camera = cameras[player]; @@ -38636,18 +41819,8 @@ void Player::WorldUI_t::WorldTooltipDialogue_t::createDialogueTooltip(Uint32 uid auto& setting = WorldDialogueSettings_t::settings[d->dialogueType]; - /*if ( parentEnt->bUseRenderInterpolation ) - { - d->x = parentEnt->lerpRenderState.x.position * 16.0; - d->y = parentEnt->lerpRenderState.y.position * 16.0; - d->z = parentEnt->lerpRenderState.z.position + setting.offsetZ; - } - else*/ - { - d->x = parentEnt->x; - d->y = parentEnt->y; - d->z = parentEnt->z + setting.offsetZ; - } + d->updateWorldCoordinates(); + d->animZ = 1.5; d->drawScale = 0.1 + setting.scaleMod; @@ -39447,7 +42620,7 @@ bool SkillUpAnimation_t::soundIndexUsedForNotification(const int index) } else { - for ( auto skill : Player::SkillSheet_t::skillSheetData.skillEntries ) + for ( auto& skill : Player::SkillSheet_t::skillSheetData.skillEntries ) { if ( index == skill.skillSfx ) { @@ -40298,8 +43471,9 @@ size_t SkillUpAnimation_t::getSkillUpIndexToDisplay() case PRO_UNARMED: priority.push(std::make_pair(10, index)); break; - case PRO_MAGIC: - case PRO_SPELLCASTING: + case PRO_SORCERY: + case PRO_MYSTICISM: + case PRO_THAUMATURGY: priority.push(std::make_pair(5, index)); break; case PRO_STEALTH: @@ -40311,9 +43485,6 @@ size_t SkillUpAnimation_t::getSkillUpIndexToDisplay() case PRO_ALCHEMY: priority.push(std::make_pair(2, index)); break; - case PRO_SWIMMING: - priority.push(std::make_pair(1, index)); - break; case PRO_APPRAISAL: default: priority.push(std::make_pair(0, index)); @@ -40389,9 +43560,10 @@ void SkillUpAnimation_t::addSkillUp(const int _numSkill, const int _currentSkill case PRO_STEALTH: ticksToLive = 4 * TICKS_PER_SECOND; break; - case PRO_MAGIC: - case PRO_SPELLCASTING: - ticksToLive = 2 * TICKS_PER_SECOND; + case PRO_SORCERY: + case PRO_MYSTICISM: + case PRO_THAUMATURGY: + ticksToLive = 3 * TICKS_PER_SECOND; break; case PRO_LOCKPICKING: case PRO_TRADING: @@ -40399,9 +43571,6 @@ void SkillUpAnimation_t::addSkillUp(const int _numSkill, const int _currentSkill case PRO_ALCHEMY: ticksToLive = 3 * TICKS_PER_SECOND; break; - case PRO_SWIMMING: - ticksToLive = 2 * TICKS_PER_SECOND; - break; case PRO_APPRAISAL: default: ticksToLive = 2 * TICKS_PER_SECOND; @@ -40794,7 +43963,7 @@ void updateSkillUpFrame(const int player) else { char buf[128]; - snprintf(buf, sizeof(buf), Language::get(4327), Player::SkillSheet_t::skillSheetData.skillEntries[skillsheetIndex].name.c_str()); + snprintf(buf, sizeof(buf), Language::get(4327), Player::SkillSheet_t::skillSheetData.skillEntries[skillsheetIndex].getSkillName().c_str()); skillNameTxt->setText(buf); } if ( auto textGet = skillNameTxt->getTextObject() ) @@ -40838,7 +44007,7 @@ void updateSkillUpFrame(const int player) } else { - for ( auto skill : Player::SkillSheet_t::skillSheetData.skillEntries ) + for ( auto& skill : Player::SkillSheet_t::skillSheetData.skillEntries ) { if ( skill.skillId == skillUp.whichSkill ) { diff --git a/src/ui/GameUI.hpp b/src/ui/GameUI.hpp index 9bad11401..7dc455a0d 100644 --- a/src/ui/GameUI.hpp +++ b/src/ui/GameUI.hpp @@ -67,6 +67,7 @@ struct CustomColors_t Uint32 characterBaseClassText = 0xFFFFFFFF; Uint32 characterDLC1ClassText = 0xFFFFFFFF; Uint32 characterDLC2ClassText = 0xFFFFFFFF; + Uint32 characterDLC3ClassText = makeColorRGB(0, 255, 0); }; extern CustomColors_t hudColors; @@ -76,6 +77,8 @@ extern int GAMEUI_FRAMEDATA_ALCHEMY_RECIPE_SLOT; // displaying in main alchemy g extern int GAMEUI_FRAMEDATA_ALCHEMY_RECIPE_ENTRY; // the recipe icon extern int GAMEUI_FRAMEDATA_WORLDTOOLTIP_ITEM; extern int GAMEUI_FRAMEDATA_SHOP_ITEM; +extern int GAMEUI_FRAMEDATA_ALCHEMY_MISSING_QTY; // alchemy ingredient missing quantity +extern int GAMEUI_FRAMEDATA_SPELL_LEARNABLE; // if spell provides skill xp // if true, use the new user interface extern bool newui; @@ -178,6 +181,11 @@ struct StatusEffectQueue_t static const int kEffectInspiration; static const int kEffectRetaliation; static const int kEffectAssistance; + static const int kEffectStability; + static const int kEffectVandal; + static const int kEffectOvercharge; + static const int kEffectWealth; + static const int kEffectEnd; Frame* statusEffectFrame = nullptr; Frame* statusEffectTooltipFrame = nullptr; @@ -328,6 +336,7 @@ void openMapWindow(int player); void openLogWindow(int player); void capitalizeString(std::string& str); +void lowercaseString(std::string& str); void uppercaseString(std::string& str); void camelCaseString(std::string& str); bool stringStartsWithVowel(std::string& str); diff --git a/src/ui/LoadingScreen.cpp b/src/ui/LoadingScreen.cpp index fe83c3bf9..5b5613c5f 100644 --- a/src/ui/LoadingScreen.cpp +++ b/src/ui/LoadingScreen.cpp @@ -122,11 +122,11 @@ static void baseCreateLoadingScreen(real_t progress, const char* background_imag } // Loading... text - auto label = loading_frame->addField("loading_label", 128); - label->setSize(fullscreen); - label->setJustify(Field::justify_t::CENTER); - label->setFont("fonts/pixel_maz.ttf#64#2"); - label->setText(Language::get(709)); + //auto label = loading_frame->addField("loading_label", 128); + //label->setSize(fullscreen); + //label->setJustify(Field::justify_t::CENTER); + //label->setFont("fonts/pixel_maz.ttf#64#2"); + //label->setText(Language::get(709)); } void createLoadingScreen(real_t progress) { diff --git a/src/ui/MainMenu.cpp b/src/ui/MainMenu.cpp index 35f96eb00..a097e6dbd 100644 --- a/src/ui/MainMenu.cpp +++ b/src/ui/MainMenu.cpp @@ -1,3 +1,4 @@ +#include "../main.hpp" #include "MainMenu.hpp" #include "Frame.hpp" #include "Image.hpp" @@ -557,6 +558,7 @@ namespace MainMenu { // Controls options struct Controls { bool numkeys_in_inventory_enabled = true; + bool numkeys_change_hotbar_slot_enabled = true; bool mkb_world_tooltips_enabled = true; bool gamepad_facehotbar = true; float mouse_sensitivity = 32.f; @@ -566,6 +568,8 @@ namespace MainMenu { float turn_sensitivity_y = 50.f; bool gamepad_camera_invert_x = false; bool gamepad_camera_invert_y = false; + float gamepad_deadzone_left = 25.f; + float gamepad_deadzone_right = 25.f; float quick_turn_speed_control = 1.f; float quick_turn_speed_mkb_control = 1.f; inline void save(int index); @@ -636,6 +640,8 @@ namespace MainMenu { float environment_volume = 100.f; float notification_volume = 100.f; float music_volume = 100.f; + bool set_instrument_bg_enabled = true; + bool set_instrument_fg_enabled = true; bool minimap_pings_enabled = true; bool player_monster_sounds_enabled = true; bool out_of_focus_audio_enabled = true; @@ -2029,9 +2035,15 @@ namespace MainMenu { monoPrompt(text, Language::get(5009), [](Button&){ soundActivate(); closeMono(); - auto buttons = main_menu_frame->findFrame("buttons"); assert(buttons); - auto play = buttons->findButton("Play Game"); - play->select(); + auto buttons = main_menu_frame->findFrame("buttons"); + if ( buttons ) + { + if ( auto play = buttons->findButton("Play Game") ) + { + play->select(); + } + } + //assert(buttons); }); }; @@ -2065,6 +2077,22 @@ namespace MainMenu { fp->write(text.c_str(), sizeof(char), text.size()); FileIO::close(fp); } + } + else if ( DLCHash == 121449 ) { + playSound(402, 92); + printlog("[LICENSE]: Deserters and Disciples DLC license key found."); + prompt(Language::get(6989)); + enabledDLCPack3 = true; + + char path[PATH_MAX] = ""; + completePath(path, "desertersanddisciples.key", outputdir); + + // write the serial file + File* fp = nullptr; + if ( fp = FileIO::open(path, "wb") ) { + fp->write(text.c_str(), sizeof(char), text.size()); + FileIO::close(fp); + } } else { printlog("[LICENSE]: DLC license key invalid."); errorPrompt(Language::get(5017), Language::get(5009), [](Button&){ @@ -2774,6 +2802,7 @@ namespace MainMenu { settings.mkb_world_tooltips_enabled = mkb_world_tooltips_enabled; settings.gamepad_facehotbar = gamepad_facehotbar; settings.hotbar_numkey_quick_add = numkeys_in_inventory_enabled; + settings.hotbar_numkey_change_slot = numkeys_change_hotbar_slot_enabled; settings.mousespeed = std::min(std::max(0.f, mouse_sensitivity), 100.f); settings.reversemouse = reverse_mouse_enabled; settings.smoothmouse = smooth_mouse_enabled; @@ -2781,6 +2810,8 @@ namespace MainMenu { settings.gamepad_righty_sensitivity = std::min(std::max(25.f / 32768.f, turn_sensitivity_y / 32768.f), 200.f / 32768.f); settings.gamepad_rightx_invert = gamepad_camera_invert_x; settings.gamepad_righty_invert = gamepad_camera_invert_y; + settings.leftStickDeadzone = (32000.f / 100.f) * std::min(50.f, std::max(5.f, gamepad_deadzone_left)); + settings.rightStickDeadzone = (32000.f / 100.f) * std::min(50.f, std::max(5.f, gamepad_deadzone_right)); settings.quick_turn_speed = std::max(1.f, std::min(5.f, quick_turn_speed_control)); settings.quick_turn_speed_mkb = std::max(1.f, std::min(5.f, quick_turn_speed_mkb_control)); } @@ -2792,6 +2823,7 @@ namespace MainMenu { controls.mkb_world_tooltips_enabled = settings.mkb_world_tooltips_enabled; controls.gamepad_facehotbar = settings.gamepad_facehotbar; controls.numkeys_in_inventory_enabled = settings.hotbar_numkey_quick_add; + controls.numkeys_change_hotbar_slot_enabled = settings.hotbar_numkey_change_slot; controls.mouse_sensitivity = settings.mousespeed; controls.reverse_mouse_enabled = settings.reversemouse; controls.smooth_mouse_enabled = settings.smoothmouse; @@ -2799,6 +2831,8 @@ namespace MainMenu { controls.turn_sensitivity_y = settings.gamepad_righty_sensitivity * 32768.0; controls.gamepad_camera_invert_x = settings.gamepad_rightx_invert; controls.gamepad_camera_invert_y = settings.gamepad_righty_invert; + controls.gamepad_deadzone_left = 100.f * settings.leftStickDeadzone / 32000.f; + controls.gamepad_deadzone_right = 100.f * settings.rightStickDeadzone / 32000.f; controls.quick_turn_speed_control = settings.quick_turn_speed; controls.quick_turn_speed_mkb_control = settings.quick_turn_speed_mkb; return controls; @@ -2809,11 +2843,12 @@ namespace MainMenu { } bool Controls::serialize(FileInterface* file) { - int version = 1; + int version = 3; file->property("version", version); file->property("mkb_world_tooltips_enabled", mkb_world_tooltips_enabled); file->property("gamepad_facehotbar", gamepad_facehotbar); file->property("numkeys_in_inventory_enabled", numkeys_in_inventory_enabled); + file->propertyVersion("numkeys_change_hotbar_slot_enabled", version >= 3, numkeys_change_hotbar_slot_enabled); file->property("mouse_sensitivity", mouse_sensitivity); file->property("reverse_mouse_enabled", reverse_mouse_enabled); file->property("smooth_mouse_enabled", smooth_mouse_enabled); @@ -2821,6 +2856,8 @@ namespace MainMenu { file->property("turn_sensitivity_y", turn_sensitivity_y); file->property("gamepad_camera_invert_x", gamepad_camera_invert_x); file->property("gamepad_camera_invert_y", gamepad_camera_invert_y); + file->propertyVersion("gamepad_deadzone_left", version >= 2, gamepad_deadzone_left); + file->propertyVersion("gamepad_deadzone_right", version >= 2, gamepad_deadzone_right); file->property("quick_turn_speed_control", quick_turn_speed_control); file->property("quick_turn_speed_mkb_control", quick_turn_speed_mkb_control); return true; @@ -2900,18 +2937,22 @@ namespace MainMenu { } current_recording_audio_device = recording_audio_device; current_audio_device = audio_device; +#ifdef USE_FMOD if (fmod_speakermode != speaker_mode) { fmod_speakermode = (FMOD_SPEAKERMODE)speaker_mode; if (initialized) { restartPromptRequired = true; } } +#endif MainMenu::master_volume = std::min(std::max(0.f, master_volume / 100.f), 1.f); sfxvolume = std::min(std::max(0.f, gameplay_volume / 100.f), 1.f); sfxAmbientVolume = std::min(std::max(0.f, ambient_volume / 100.f), 1.f); sfxEnvironmentVolume = std::min(std::max(0.f, environment_volume / 100.f), 1.f); sfxNotificationVolume = std::min(std::max(0.f, notification_volume / 100.f), 1.f); musvolume = std::min(std::max(0.f, music_volume / 100.f), 1.f); + instrument_bg_enabled = set_instrument_bg_enabled; + instrument_fg_enabled = set_instrument_fg_enabled; minimapPingMute = !minimap_pings_enabled; mute_player_monster_sounds = !player_monster_sounds_enabled; mute_audio_on_focus_lost = !out_of_focus_audio_enabled; @@ -3029,14 +3070,16 @@ namespace MainMenu { settings.enable_voice_receive = VoiceChat.activeSettings.enable_voice_receive; settings.use_voice_custom_rolloff = VoiceChat.activeSettings.use_custom_rolloff; VoiceChat.mainmenuSettings = VoiceChat.activeSettings; -#endif settings.speaker_mode = (int)fmod_speakermode; +#endif settings.master_volume = MainMenu::master_volume * 100.f; settings.gameplay_volume = (float)sfxvolume * 100.f; settings.ambient_volume = (float)sfxAmbientVolume * 100.f; settings.environment_volume = (float)sfxEnvironmentVolume * 100.f; settings.notification_volume = (float)sfxNotificationVolume * 100.f; settings.music_volume = (float)musvolume * 100.f; + settings.set_instrument_bg_enabled = instrument_bg_enabled; + settings.set_instrument_fg_enabled = instrument_fg_enabled; settings.minimap_pings_enabled = !minimapPingMute; settings.player_monster_sounds_enabled = !mute_player_monster_sounds; settings.out_of_focus_audio_enabled = !mute_audio_on_focus_lost; @@ -3074,7 +3117,7 @@ namespace MainMenu { } bool AllSettings::serialize(FileInterface* file) { - int version = 22; + int version = 23; file->property("version", version); file->property("mods", mods); file->property("crossplay_enabled", crossplay_enabled); @@ -3166,6 +3209,8 @@ namespace MainMenu { file->property("notification_volume", notification_volume); } file->property("music_volume", music_volume); + file->propertyVersion("instrument_fg_enabled", version >= 23, set_instrument_fg_enabled); + file->propertyVersion("instrument_bg_enabled", version >= 23, set_instrument_bg_enabled); file->property("minimap_pings_enabled", minimap_pings_enabled); file->property("player_monster_sounds_enabled", player_monster_sounds_enabled); file->property("out_of_focus_audio_enabled", out_of_focus_audio_enabled); @@ -3559,7 +3604,7 @@ namespace MainMenu { auto textbox2 = textbox1->findFrame("story_text_box"); assert(textbox2); auto size = textbox2->getActualSize(); - ++size.y; + size.y += std::max(0, (old_story_text_scroll - (int)story_text_scroll)); textbox2->setActualSize(size); } } else { @@ -3722,7 +3767,7 @@ namespace MainMenu { auto font = Font::get(bigfont_outline); assert(font); static float credits_scroll = 0.f; - constexpr int num_credits_lines = 83; + constexpr int num_credits_lines = 90; auto credits = main_menu_frame->addFrame("credits"); credits->setSize(SDL_Rect{0, 0, Frame::virtualScreenX, Frame::virtualScreenY}); @@ -3775,6 +3820,9 @@ namespace MainMenu { u8"Additional writing\n" u8" \n" u8" \n \n \n \n \n" + u8"Additional music and sound design\n" + u8" \n" + u8" \n \n \n \n \n" u8"Special thanks\n" u8" \n" u8" \n" @@ -3823,6 +3871,9 @@ namespace MainMenu { u8"Frasier Panton\n" u8" \n \n \n \n \n" u8" \n" + u8"Garrett Williamson\n" + u8" \n \n \n \n \n" + u8" \n" u8"Our Kickstarter Backers\n" u8"Kevin White\n" u8"Julian Seeger\n" @@ -3908,10 +3959,18 @@ namespace MainMenu { messagePlayer(clientnum, MESSAGE_MISC, Language::get(276)); } + // update map HDR + if ( initialized ) + { + map.setMapHDRSettings(); + } + // set volume and sound driver if (initialized) { setAudioDevice(current_audio_device); +#ifdef USE_FMOD setRecordDevice(current_recording_audio_device); +#endif setGlobalVolume(master_volume, musvolume, sfxvolume, sfxAmbientVolume, sfxEnvironmentVolume, sfxNotificationVolume); } } @@ -5659,7 +5718,7 @@ namespace MainMenu { } } - auto prompt = binaryPrompt(Language::get(5052), + auto prompt = binaryPrompt(Language::get(5029), Language::get(5030), Language::get(5008), [](Button& button) { closeBinary(); @@ -6478,9 +6537,9 @@ namespace MainMenu { } int y = 0; + int num_drivers = 0; #if !defined(NINTENDO) && defined(USE_FMOD) int selected_device = 0; - int num_drivers = 0; (void)fmod_system->getNumDrivers(&num_drivers); audio_drivers.clear(); audio_drivers.reserve(num_drivers); @@ -6758,6 +6817,8 @@ namespace MainMenu { allSettings.minimap_pings_enabled, [](Button& button){soundToggleSetting(button); allSettings.minimap_pings_enabled = button.isPressed();}); y += settingsAddBooleanOption(*settings_subwindow, y, "player_monster_sounds", Language::get(5201), Language::get(5202), allSettings.player_monster_sounds_enabled, [](Button& button){soundToggleSetting(button); allSettings.player_monster_sounds_enabled = button.isPressed();}); + y += settingsAddBooleanOption(*settings_subwindow, y, "instrument_bg_enabled", Language::get(6870), Language::get(6871), + allSettings.set_instrument_bg_enabled, [](Button& button) {soundToggleSetting(button); allSettings.set_instrument_bg_enabled = button.isPressed(); }); #ifndef NINTENDO y += settingsAddBooleanOption(*settings_subwindow, y, "out_of_focus_audio", Language::get(5203), Language::get(5204), @@ -6795,6 +6856,7 @@ namespace MainMenu { #endif {Setting::Type::Boolean, "minimap_pings"}, {Setting::Type::Boolean, "player_monster_sounds"}, + {Setting::Type::Boolean, "instrument_bg_enabled"}, {Setting::Type::Boolean, "out_of_focus_audio"}, {Setting::Type::Boolean, "music_preload"} }); } @@ -6819,6 +6881,7 @@ namespace MainMenu { #endif {Setting::Type::Boolean, "minimap_pings"}, {Setting::Type::Boolean, "player_monster_sounds"}, + {Setting::Type::Boolean, "instrument_bg_enabled"}, {Setting::Type::Boolean, "out_of_focus_audio"}, {Setting::Type::Boolean, "music_preload"}}); } @@ -6839,6 +6902,7 @@ namespace MainMenu { #endif {Setting::Type::Boolean, "minimap_pings"}, {Setting::Type::Boolean, "player_monster_sounds"}, + {Setting::Type::Boolean, "instrument_bg_enabled"}, {Setting::Type::Boolean, "out_of_focus_audio"}, {Setting::Type::Boolean, "music_preload"}}); } @@ -6856,7 +6920,9 @@ namespace MainMenu { {Setting::Type::Boolean, "use_custom_rolloff"}, #endif {Setting::Type::Boolean, "minimap_pings"}, - {Setting::Type::Boolean, "player_monster_sounds"}}); + {Setting::Type::Boolean, "player_monster_sounds"}, + {Setting::Type::Boolean, "instrument_bg_enabled"}, + }); #endif #if !defined(NINTENDO) && defined(USE_FMOD) @@ -7279,9 +7345,6 @@ namespace MainMenu { y += settingsAddSlider(*settings_subwindow, y, "mouse_sensitivity", Language::get(5218), Language::get(5219), allSettings.controls[bound_player].mouse_sensitivity, 0, 100, nullptr, [](Slider& slider) {soundSliderSetting(slider, true); allSettings.controls[bound_player].mouse_sensitivity = slider.getValue();}); - y += settingsAddBooleanOption(*settings_subwindow, y, "numkeys_in_inventory", Language::get(5220), Language::get(5221), - allSettings.controls[bound_player].numkeys_in_inventory_enabled, [](Button& button) - {soundToggleSetting(button); allSettings.controls[bound_player].numkeys_in_inventory_enabled = button.isPressed();}); y += settingsAddBooleanOption(*settings_subwindow, y, "reverse_mouse", Language::get(5222), Language::get(5223), allSettings.controls[bound_player].reverse_mouse_enabled, [](Button& button) {soundToggleSetting(button); allSettings.controls[bound_player].reverse_mouse_enabled = button.isPressed();}); @@ -7291,6 +7354,12 @@ namespace MainMenu { y += settingsAddBooleanOption(*settings_subwindow, y, "mkb_world_tooltips", Language::get(5226), Language::get(5227), allSettings.controls[bound_player].mkb_world_tooltips_enabled, [](Button& button) {soundToggleSetting(button); allSettings.controls[bound_player].mkb_world_tooltips_enabled = button.isPressed();}); + y += settingsAddBooleanOption(*settings_subwindow, y, "numkeys_in_inventory", Language::get(5220), Language::get(5221), + allSettings.controls[bound_player].numkeys_in_inventory_enabled, [](Button& button) + {soundToggleSetting(button); allSettings.controls[bound_player].numkeys_in_inventory_enabled = button.isPressed();}); + y += settingsAddBooleanOption(*settings_subwindow, y, "numkeys_change_slot", Language::get(6880), Language::get(6881), + allSettings.controls[bound_player].numkeys_change_hotbar_slot_enabled, [](Button& button) + {soundToggleSetting(button); allSettings.controls[bound_player].numkeys_change_hotbar_slot_enabled = button.isPressed(); }); y += settingsAddSlider(*settings_subwindow, y, "quick_turn_speed_mkb_control", Language::get(6310), Language::get(6311), allSettings.controls[bound_player].quick_turn_speed_mkb_control, 1.f, 5.f, sliderFloorInt, [](Slider& slider) {soundSliderSetting(slider, true); allSettings.controls[bound_player].quick_turn_speed_mkb_control = slider.getValue(); }); @@ -7298,10 +7367,11 @@ namespace MainMenu { hookSettings(*settings_subwindow, {{Setting::Type::Customize, "bindings"}, {Setting::Type::Slider, "mouse_sensitivity"}, - {Setting::Type::Boolean, "numkeys_in_inventory"}, {Setting::Type::Boolean, "reverse_mouse"}, {Setting::Type::Boolean, "smooth_mouse"}, {Setting::Type::Boolean, "mkb_world_tooltips"}, + {Setting::Type::Boolean, "numkeys_in_inventory"}, + {Setting::Type::Boolean, "numkeys_change_slot"}, {Setting::Type::Slider, "quick_turn_speed_mkb_control"}, }); } @@ -7319,6 +7389,13 @@ namespace MainMenu { allSettings.controls[bound_player].turn_sensitivity_y, 25.f, 200.f, sliderPercent, [](Slider& slider) {soundSliderSetting(slider, true); allSettings.controls[bound_player].turn_sensitivity_y = slider.getValue();}); + y += settingsAddSlider(*settings_subwindow, y, "gamepad_deadzone_left", Language::get(6846), Language::get(6847), + allSettings.controls[bound_player].gamepad_deadzone_left, 5.f, 50.f, sliderPercent, [](Slider& slider) + {soundSliderSetting(slider, true); allSettings.controls[bound_player].gamepad_deadzone_left = slider.getValue(); }); + y += settingsAddSlider(*settings_subwindow, y, "gamepad_deadzone_right", Language::get(6848), Language::get(6849), + allSettings.controls[bound_player].gamepad_deadzone_right, 5.f, 50.f, sliderPercent, [](Slider& slider) + {soundSliderSetting(slider, true); allSettings.controls[bound_player].gamepad_deadzone_right = slider.getValue(); }); + y += settingsAddBooleanOption(*settings_subwindow, y, "gamepad_camera_invert_x", Language::get(5237), Language::get(5238), allSettings.controls[bound_player].gamepad_camera_invert_x, [](Button& button) {soundToggleSetting(button); allSettings.controls[bound_player].gamepad_camera_invert_x = button.isPressed();}); @@ -7335,6 +7412,8 @@ namespace MainMenu { {Setting::Type::Dropdown, "gamepad_facehotbar"}, {Setting::Type::Slider, "turn_sensitivity_x"}, {Setting::Type::Slider, "turn_sensitivity_y"}, + {Setting::Type::Slider, "gamepad_deadzone_left"}, + {Setting::Type::Slider, "gamepad_deadzone_right"}, {Setting::Type::Boolean, "gamepad_camera_invert_x"}, {Setting::Type::Boolean, "gamepad_camera_invert_y"}, {Setting::Type::Slider, "quick_turn_speed_control"}, @@ -8476,7 +8555,35 @@ namespace MainMenu { } #endif } + + // number of player selectable races + constexpr int num_races = 14; + /******************************************************************************/ + int getLangEntryForMainMenuRaceName(int race) + { + if ( race >= 9 && race < num_races ) + { + return (race - 9) + 6779; + } + else if ( race >= 0 && race < 9 ) + { + return 5369 + race; + } + return 5369; + } + int getLangEntryForPlayerRaceName(int race) + { + if ( race > RACE_IMP && race < NUMRACES ) + { + return (race - RACE_IMP - 1) + 6779; + } + else if ( race >= 0 && race < 9 ) + { + return 5369 + race; + } + return 5369; + } static void createLeaderboards(std::string leaderboard_type) { assert(main_menu_frame); @@ -8801,7 +8908,7 @@ namespace MainMenu { char buf[1024]; if ( kills_show_proficiencies ) { - size_t numEntries = Player::SkillSheet_t::skillSheetData.skillEntries.size(); + size_t numEntries = std::min((size_t)NUMPROFICIENCIES, Player::SkillSheet_t::skillSheetData.skillEntries.size()); for ( size_t index = 0; index < NUMPROFICIENCIES / 2; ++index ) { int loops = 1; @@ -8814,7 +8921,7 @@ namespace MainMenu { } int c = Player::SkillSheet_t::skillSheetData.skillEntries[index + loops * (NUMPROFICIENCIES / 2)].skillId; int val = score->stats->getProficiency(c); - snprintf(buf, sizeof(buf), "%3d %s", val, Player::SkillSheet_t::skillSheetData.skillEntries[index + loops * (NUMPROFICIENCIES / 2)].name.c_str()); + snprintf(buf, sizeof(buf), "%3d %s", val, Player::SkillSheet_t::skillSheetData.skillEntries[index + loops * (NUMPROFICIENCIES / 2)].getSkillName(true).c_str()); auto skill = kills->addEntry(buf, true); //skill->color = makeColor(203, 171, 101, 255); if ( val >= SKILL_LEVEL_LEGENDARY ) @@ -9150,6 +9257,15 @@ namespace MainMenu { case KilledBy::BELL: cause_of_death = Language::get(6278); break; + case KilledBy::MUSHROOM: + cause_of_death = Language::get(6754); + break; + case KilledBy::LEAVES: + cause_of_death = Language::get(6759); + break; + case KilledBy::DEATH_KNOCKBACK: + cause_of_death = Language::get(6854); + break; default: { cause_of_death = Language::get(5794 + (int)score->stats->killer); @@ -9262,7 +9378,7 @@ namespace MainMenu { assert(character_title); snprintf(buf, sizeof(buf), Language::get(5298), score->stats->LVL, - Language::get(5369 + score->stats->playerRace), + Language::get(getLangEntryForPlayerRaceName(score->stats->playerRace)), playerClassLangEntry(score->classnum, 0)); character_title->setText(buf); @@ -9350,7 +9466,8 @@ namespace MainMenu { } statChecks.clear(); }); - + static ConsoleVariable cvar_leaderboard_show_id("/leaderboard_show_id", false); + static ConsoleVariable cvar_leaderboard_copy_id("/leaderboard_copy_id", false); static auto add_score = [](score_t* score, const char* name, const char* prev, const char* next, int index, int rank, int selectIndex){ auto window = main_menu_frame->findFrame("leaderboards"); assert(window); @@ -9417,6 +9534,15 @@ namespace MainMenu { selectedScore = score; updateStats(button, score); loadScore(score); +#ifndef NDEBUG + if ( *cvar_leaderboard_copy_id && *cvar_leaderboard_show_id ) + { + if ( score && score->stats ) + { + SDL_SetClipboardText(score->stats->name); + } + } +#endif }); button->setTickCallback([](Widget& widget){ auto button = static_cast(&widget); @@ -9500,7 +9626,7 @@ namespace MainMenu { if (boardType == BoardType::LOCAL_SINGLE || boardType == BoardType::LOCAL_MULTI) { auto scores = boardType == BoardType::LOCAL_SINGLE ? - &topscores : &topscoresMultiplayer; + &topscores_json : &topscoresMultiplayer_json; if (scores->first) { (void)window->remove("wait_message"); int index = 0; @@ -9671,7 +9797,6 @@ namespace MainMenu { // poll for downloaded scores #ifdef USE_PLAYFAB - static ConsoleVariable cvar_leaderboard_show_id("/leaderboard_show_id", false); list->setTickCallback([](Widget& widget) { bool tryRefresh = false; if ( playfabUser.leaderboardSearch.requiresRefresh ) @@ -10136,7 +10261,7 @@ namespace MainMenu { delete_entry->setTickCallback([](Widget& widget){ if (boardType == BoardType::LOCAL_SINGLE || boardType == BoardType::LOCAL_MULTI) { auto scores = boardType == BoardType::LOCAL_SINGLE ? - &topscores : &topscoresMultiplayer; + &topscores_json : &topscoresMultiplayer_json; widget.setInvisible(scores->first == nullptr); } else @@ -10189,7 +10314,7 @@ namespace MainMenu { } }); auto scores = boardType == BoardType::LOCAL_SINGLE ? - &topscores : &topscoresMultiplayer; + &topscores_json : &topscoresMultiplayer_json; } else { errorPrompt(Language::get(5316), Language::get(5317), [](Button& button){ @@ -13011,9 +13136,6 @@ namespace MainMenu { /******************************************************************************/ - // number of player selectable races - constexpr int num_races = 9; - bool ClassDescriptions::init = false; std::unordered_map ClassDescriptions::data; @@ -13362,7 +13484,7 @@ namespace MainMenu { std::vector reducedClassList(int index) { std::vector result; result.reserve(num_classes); - for (int c = CLASS_BARBARIAN; c <= CLASS_HUNTER; ++c) { + for (int c = CLASS_BARBARIAN; c <= CLASS_PALADIN; ++c) { if (isCharacterValidFromDLC(*stats[index], c) == VALID_OK_CHARACTER) { result.emplace_back(classes_in_order[c]); } @@ -13559,6 +13681,11 @@ namespace MainMenu { break; } + if ( ClassDescriptions::data.find(classnum) == ClassDescriptions::data.end() ) + { + return; + } + for ( int c = 0; c < num_class_stats; ++c ) { static char buf[16]; @@ -13629,6 +13756,9 @@ namespace MainMenu { else if (race >= RACE_AUTOMATON && race <= RACE_INSECTOID) { color_race = color_dlc2; } + else if ( race > RACE_IMP && race < RACE_ENUM_END ) { + color_race = hudColors.characterDLC3ClassText; + } else { color_race = color_dlc0; } @@ -13707,6 +13837,21 @@ namespace MainMenu { case CLASS_HUNTER: achName = Compendium_t::achievements["BARONY_ACH_BUGGAR_BARON"].name; break; + case CLASS_BARD: + achName = Compendium_t::achievements["BARONY_ACH_BITTY_BARON"].name; + break; + case CLASS_SAPPER: + achName = Compendium_t::achievements["BARONY_ACH_BONKERS_BARON"].name; + break; + case CLASS_SCION: + achName = Compendium_t::achievements["BARONY_ACH_BARKSKIN_BARON"].name; + break; + case CLASS_HERMIT: + achName = Compendium_t::achievements["BARONY_ACH_BOLETE_BARON"].name; + break; + case CLASS_PALADIN: + achName = Compendium_t::achievements["BARONY_ACH_BURNINATION_BARON"].name; + break; default: break; } @@ -13768,6 +13913,26 @@ namespace MainMenu { { allowPick = isAchievementUnlockedForClassUnlock(RACE_INSECTOID); } + else if ( race != RACE_GNOME && challengeClass == CLASS_BARD ) + { + allowPick = isAchievementUnlockedForClassUnlock(RACE_GNOME); + } + else if ( race != RACE_GREMLIN && challengeClass == CLASS_SAPPER ) + { + allowPick = isAchievementUnlockedForClassUnlock(RACE_GREMLIN); + } + else if ( race != RACE_DRYAD && challengeClass == CLASS_SCION ) + { + allowPick = isAchievementUnlockedForClassUnlock(RACE_DRYAD); + } + else if ( race != RACE_MYCONID && challengeClass == CLASS_HERMIT ) + { + allowPick = isAchievementUnlockedForClassUnlock(RACE_MYCONID); + } + else if ( race != RACE_SALAMANDER && challengeClass == CLASS_PALADIN ) + { + allowPick = isAchievementUnlockedForClassUnlock(RACE_SALAMANDER); + } return allowPick; } @@ -13782,7 +13947,7 @@ namespace MainMenu { && gameModeManager.currentSession.challengeRun.classnum >= 0 && gameModeManager.currentSession.challengeRun.classnum <= NUMCLASSES; for (int c = 0; c < num_races; ++c) { - auto race = Language::get(5369 + c); + auto race = Language::get(getLangEntryForMainMenuRaceName(c)); if (strcmp(button.getName(), race) == 0) { if ( fixedRace && !override_dlc && gameModeManager.currentSession.challengeRun.race != c ) { @@ -13797,10 +13962,11 @@ namespace MainMenu { } else if ( !override_dlc && !fixedRace && ((!enabledDLCPack1 && c >= 1 && c <= 4) || - (!enabledDLCPack2 && c >= 5 && c <= 8)) ) { + (!enabledDLCPack2 && c >= 5 && c <= 8) || + (!enabledDLCPack3 && c >= 9 && c <= 13)) ) { // this class is not available to the player button.setPressed(false); - openDLCPrompt(c >= 5 ? 1 : 0); + openDLCPrompt(c >= 9 ? 2 : (c >= 5 ? 1 : 0)); return; } else if ( fixedClass && !override_dlc && c >= 1 @@ -13813,8 +13979,14 @@ namespace MainMenu { else { success = true; - if (stats[index]->playerRace != c) { - stats[index]->playerRace = c; + + int pickedRace = RACE_HUMAN + c; + if ( pickedRace > RACE_INSECTOID ) + { + pickedRace += 4; + } + if (stats[index]->playerRace != pickedRace ) { + stats[index]->playerRace = pickedRace; if (!inputs.hasController(index)) { soundToggle(); } @@ -13884,7 +14056,7 @@ namespace MainMenu { } for (int c = 0; c < num_races; ++c) { // clear other buttons - auto race = Language::get(5369 + c); + auto race = Language::get(getLangEntryForMainMenuRaceName(c)); auto other_button = frame->findButton(race); if (other_button != &button) { other_button->setPressed(false); @@ -15474,10 +15646,10 @@ namespace MainMenu { (svFlags & SV_FLAG_LIFESAVING) || Mods::disableSteamAchievements ) { achievements->setColor(makeColor(180, 37, 37, 255)); - achievements->setText("ACHIEVEMENTS DISABLED"); + achievements->setText(Language::get(5389)); } else { achievements->setColor(makeColor(37, 90, 255, 255)); - achievements->setText("ACHIEVEMENTS ENABLED"); + achievements->setText(Language::get(5390)); } }); (*achievements->getTickCallback())(*achievements); @@ -15737,7 +15909,7 @@ namespace MainMenu { gradient->ontop = true; for (int c = 0; c < num_races; ++c) { - auto race = subframe->addButton(Language::get(5369 + c)); + auto race = subframe->addButton(Language::get(getLangEntryForMainMenuRaceName(c))); race->setSize(SDL_Rect{0, c * 36 + 2, 30, 30}); bool fixedRace = gameModeManager.currentSession.challengeRun.isActive() @@ -15763,6 +15935,9 @@ namespace MainMenu { else if (!enabledDLCPack2 && c >= 5 && c <= 8) { race->setBackground("*#images/ui/Main Menus/sublist_item-locked.png"); } + else if ( !enabledDLCPack3 && c >= 9 && c <= 13 ) { + race->setBackground("*#images/ui/Main Menus/sublist_item-locked.png"); + } else if ( fixedClass && c >= 1 && !race_button_challenge_check_fn(c, gameModeManager.currentSession.challengeRun.classnum) ) { race->setBackground("*#images/ui/Main Menus/sublist_item-locked.png"); @@ -15791,13 +15966,13 @@ namespace MainMenu { race->addWidgetAction("MenuPageLeftAlt", "privacy"); race->setWidgetBack("back_button"); if (c < num_races - 1) { - race->setWidgetDown(Language::get(5369 + c + 1)); + race->setWidgetDown(Language::get(getLangEntryForMainMenuRaceName(c + 1))); } /*else { race->setWidgetDown("disable_abilities"); }*/ if (c > 0) { - race->setWidgetUp(Language::get(5369 + c - 1)); + race->setWidgetUp(Language::get(getLangEntryForMainMenuRaceName(c - 1))); } race->setGlyphPosition(Widget::glyph_position_t::CENTERED); race->addWidgetAction("MenuPageLeft", "male"); @@ -15807,10 +15982,16 @@ namespace MainMenu { race->setCallback([](Button& button){ race_button_fn(button, false); }); - if (stats[index]->playerRace == c) { + + int pickedRace = RACE_HUMAN + c; + if ( pickedRace > RACE_INSECTOID ) + { + pickedRace += 4; + } + if (stats[index]->playerRace == pickedRace ) { race->setPressed(true); } - if ((stats[index]->playerRace == c && selection == -1) || + if ((stats[index]->playerRace == pickedRace && selection == -1) || (selection >= 0 && selection == c)) { race->select(); race->scrollParent(); @@ -15842,17 +16023,26 @@ namespace MainMenu { } }); - auto label = subframe->addField((std::string(Language::get(5369 + c)) + "_label").c_str(), 64); - if (c >= 1 && c <= 4) { + auto label = subframe->addField((std::string(Language::get(getLangEntryForMainMenuRaceName(c))) + "_label").c_str(), 64); + if (c >= 1 && c <= 4) + { label->setColor(color_dlc1); - } else if (c >= 5 && c <= 8) { + } + else if (c >= 5 && c <= 8) + { label->setColor(color_dlc2); - } else { + } + else if ( c >= 9 && c <= 13 ) + { + label->setColor(hudColors.characterDLC3ClassText); + } + else + { label->setColor(color_dlc0); } - label->setText(Language::get(5369 + c)); + label->setText(Language::get(getLangEntryForMainMenuRaceName(c))); label->setFont(smallfont_outline); - label->setSize(SDL_Rect{32, c * 36, 96, 36}); + label->setSize(SDL_Rect{32, c * 36, 108, 36}); label->setHJustify(Field::justify_t::LEFT); label->setVJustify(Field::justify_t::CENTER); } @@ -16120,7 +16310,7 @@ namespace MainMenu { disable_abilities->addWidgetAction("MenuPageLeftAlt", "privacy"); disable_abilities->setWidgetBack("back_button"); disable_abilities->setWidgetDown("show_race_info"); - disable_abilities->setWidgetUp(Language::get(5369 + num_races - 1)); + disable_abilities->setWidgetUp(Language::get(getLangEntryForMainMenuRaceName(num_races - 1))); if (stats[index]->playerRace != RACE_HUMAN) { disable_abilities->setPressed(stats[index]->stat_appearance != 0); } @@ -16169,7 +16359,23 @@ namespace MainMenu { male_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAuto_00.png"); male_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoHigh_00.png"); male_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoPress_00.png"); - } else { + } + else if ( stats[index]->playerRace == RACE_MYCONID ) + { + male_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortOn_00.png"); + male_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShort_00.png"); + male_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortHigh_00.png"); + male_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortPress_00.png"); + } + else if ( stats[index]->playerRace == RACE_DRYAD ) + { + male_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallOn_00.png"); + male_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTall_00.png"); + male_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallHigh_00.png"); + male_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallPress_00.png"); + } + else + { male_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMaleOn_00.png"); male_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMale_00.png"); male_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMaleHigh_00.png"); @@ -16203,12 +16409,29 @@ namespace MainMenu { male_button->setTickCallback([](Widget& widget){ const int index = widget.getOwner(); auto button = static_cast(&widget); assert(button); - if (stats[index]->playerRace == RACE_AUTOMATON) { + if (stats[index]->playerRace == RACE_AUTOMATON) + { button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoOn_00.png"); button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAuto_00.png"); button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoHigh_00.png"); button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoPress_00.png"); - } else { + } + else if ( stats[index]->playerRace == RACE_MYCONID ) + { + button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortOn_00.png"); + button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShort_00.png"); + button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortHigh_00.png"); + button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortPress_00.png"); + } + else if ( stats[index]->playerRace == RACE_DRYAD ) + { + button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallOn_00.png"); + button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTall_00.png"); + button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallHigh_00.png"); + button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallPress_00.png"); + } + else + { button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMaleOn_00.png"); button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMale_00.png"); button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMaleHigh_00.png"); @@ -16221,12 +16444,28 @@ namespace MainMenu { female_button->setColor(stats[index]->sex == FEMALE ? makeColorRGB(255, 255, 255) : makeColorRGB(127, 127, 127)); female_button->setHighlightColor(stats[index]->sex == FEMALE ? makeColorRGB(255, 255, 255) : makeColorRGB(127, 127, 127)); female_button->setStyle(Button::style_t::STYLE_RADIO); - if (stats[index]->playerRace == RACE_AUTOMATON) { + if ( stats[index]->playerRace == RACE_AUTOMATON ) { female_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoOn_00.png"); female_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAuto_00.png"); female_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoHigh_00.png"); female_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoPress_00.png"); - } else { + } + else if ( stats[index]->playerRace == RACE_MYCONID ) + { + female_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallOn_00.png"); + female_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTall_00.png"); + female_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallHigh_00.png"); + female_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallPress_00.png"); + } + else if ( stats[index]->playerRace == RACE_DRYAD ) + { + female_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortOn_00.png"); + female_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShort_00.png"); + female_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortHigh_00.png"); + female_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortPress_00.png"); + } + else + { female_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemaleOn_00.png"); female_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemale_00.png"); female_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemaleHigh_00.png"); @@ -16266,7 +16505,23 @@ namespace MainMenu { button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAuto_00.png"); button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoHigh_00.png"); button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoPress_00.png"); - } else { + } + else if ( stats[index]->playerRace == RACE_MYCONID ) + { + button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallOn_00.png"); + button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTall_00.png"); + button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallHigh_00.png"); + button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallPress_00.png"); + } + else if ( stats[index]->playerRace == RACE_DRYAD ) + { + button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortOn_00.png"); + button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShort_00.png"); + button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortHigh_00.png"); + button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortPress_00.png"); + } + else + { button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemaleOn_00.png"); button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemale_00.png"); button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemaleHigh_00.png"); @@ -16321,6 +16576,19 @@ namespace MainMenu { confirm->setCallback([](Button& button){soundActivate(); back_fn(button.getOwner());});*/ } + int playerClassLangEntryCapitalized(int classnum) + { + if ( classnum >= CLASS_BARBARIAN && classnum <= CLASS_HUNTER ) + { + return 5348 + classnum; + } + else if ( classnum >= CLASS_BARD && classnum <= CLASS_PALADIN ) + { + return 6789 + (classnum - CLASS_BARD); + } + return 5348; + } + static void characterCardClassMenu(int index, bool details, int selection) { static int class_selection[MAXPLAYERS]; @@ -16370,9 +16638,13 @@ namespace MainMenu { field.addColorToLine(0, color_dlc0); } else if (i < CLASS_MACHINIST) { field.addColorToLine(0, color_dlc1); - } else { + } else if (i <= CLASS_HUNTER ) { field.addColorToLine(0, color_dlc2); } + else + { + field.addColorToLine(0, hudColors.characterDLC3ClassText); + } field.clearIndividualLinePadding(); for ( auto line = 0; line < ClassDescriptions::data[i].linePaddings.size(); ++line ) @@ -16567,14 +16839,18 @@ namespace MainMenu { } else { static auto class_name_fn = [](Field& field, int index){ const int i = std::min(std::max(0, client_classes[index]), num_classes - 1); - field.setText(Language::get(5348 + i)); + field.setText(Language::get(playerClassLangEntryCapitalized(i))); if (i < CLASS_CONJURER) { field.setColor(color_dlc0); } else if (i < CLASS_MACHINIST) { field.setColor(color_dlc1); - } else { + } else if (i <= CLASS_HUNTER ){ field.setColor(color_dlc2); } + else + { + field.setColor(hudColors.characterDLC3ClassText); + } }; auto class_name = card->addField("class_name", 64); @@ -16759,6 +17035,11 @@ namespace MainMenu { button->setBackgroundHighlighted((prefix + "ClassSelect_IconBGLegendsHigh_00.png").c_str()); button->setBackgroundActivated((prefix + "ClassSelect_IconBGLegendsPress_00.png").c_str()); break; + case DLC::DesertersAndDisciples: + button->setBackground((prefix + "ClassSelect_IconBGDeserters_00.png").c_str()); + button->setBackgroundHighlighted((prefix + "ClassSelect_IconBGDesertersHigh_00.png").c_str()); + button->setBackgroundActivated((prefix + "ClassSelect_IconBGDesertersPress_00.png").c_str()); + break; } if (isCharacterValidFromDLC(*stats[index], c) == VALID_OK_CHARACTER) { if (strcmp(name, current_class_name) == 0) { @@ -16879,6 +17160,9 @@ namespace MainMenu { case INVALID_REQUIREDLC2: openDLCPrompt(1); break; + case INVALID_REQUIREDLC3: + openDLCPrompt(2); + break; } } else { success = true; @@ -17264,7 +17548,23 @@ namespace MainMenu { male_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAuto_00.png"); male_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoHigh_00.png"); male_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoPress_00.png"); - } else { + } + else if ( stats[index]->playerRace == RACE_MYCONID ) + { + male_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortOn_00.png"); + male_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShort_00.png"); + male_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortHigh_00.png"); + male_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortPress_00.png"); + } + else if ( stats[index]->playerRace == RACE_DRYAD ) + { + male_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallOn_00.png"); + male_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTall_00.png"); + male_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallHigh_00.png"); + male_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallPress_00.png"); + } + else + { male_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMaleOn_00.png"); male_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMale_00.png"); male_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMaleHigh_00.png"); @@ -17299,7 +17599,23 @@ namespace MainMenu { button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAuto_00.png"); button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoHigh_00.png"); button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMAutoPress_00.png"); - } else { + } + else if ( stats[index]->playerRace == RACE_MYCONID ) + { + button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortOn_00.png"); + button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShort_00.png"); + button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortHigh_00.png"); + button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortPress_00.png"); + } + else if ( stats[index]->playerRace == RACE_DRYAD ) + { + button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallOn_00.png"); + button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTall_00.png"); + button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallHigh_00.png"); + button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallPress_00.png"); + } + else + { button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMaleOn_00.png"); button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMale_00.png"); button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonMaleHigh_00.png"); @@ -17317,7 +17633,23 @@ namespace MainMenu { female_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAuto_00.png"); female_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoHigh_00.png"); female_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoPress_00.png"); - } else { + } + else if ( stats[index]->playerRace == RACE_MYCONID ) + { + female_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallOn_00.png"); + female_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTall_00.png"); + female_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallHigh_00.png"); + female_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallPress_00.png"); + } + else if ( stats[index]->playerRace == RACE_DRYAD ) + { + female_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortOn_00.png"); + female_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShort_00.png"); + female_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortHigh_00.png"); + female_button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortPress_00.png"); + } + else + { female_button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemaleOn_00.png"); female_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemale_00.png"); female_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemaleHigh_00.png"); @@ -17353,7 +17685,23 @@ namespace MainMenu { button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAuto_00.png"); button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoHigh_00.png"); button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFAutoPress_00.png"); - } else { + } + else if ( stats[index]->playerRace == RACE_MYCONID ) + { + button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallOn_00.png"); + button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTall_00.png"); + button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallHigh_00.png"); + button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteTallPress_00.png"); + } + else if ( stats[index]->playerRace == RACE_DRYAD ) + { + button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortOn_00.png"); + button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShort_00.png"); + button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortHigh_00.png"); + button->setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonDendriteShortPress_00.png"); + } + else + { button->setIcon("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemaleOn_00.png"); button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemale_00.png"); button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/RaceSelection/UI_RaceSelection_ButtonFemaleHigh_00.png"); @@ -17365,7 +17713,7 @@ namespace MainMenu { race_button->setColor(makeColor(255, 255, 255, 255)); race_button->setHighlightColor(makeColor(255, 255, 255, 255)); race_button->setSize(SDL_Rect{166, 166, 108, 52}); - race_button->setText(Language::get(5369 + stats[index]->playerRace)); + race_button->setText(Language::get(getLangEntryForPlayerRaceName(stats[index]->playerRace))); race_button->setFont(smallfont_outline); race_button->setBackground("*images/ui/Main Menus/Play/PlayerCreation/Finalize_Button_RaceBase_00.png"); race_button->setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/Finalize_Button_RaceBaseHigh_00.png"); @@ -17414,14 +17762,19 @@ namespace MainMenu { if ( forcedClass ) { std::vector chances; - chances.resize(RACE_INSECTOID + 1); + chances.resize(NUMRACES); auto oldRace = stats[index]->playerRace; Uint32 oldAppearance = stats[index]->stat_appearance; stats[index]->stat_appearance = 0; bool chanceFound = false; - for ( int race = RACE_HUMAN; race <= RACE_INSECTOID; ++race ) + for ( int race = RACE_HUMAN; race < RACE_ENUM_END; ++race ) { + if ( race > RACE_INSECTOID && race <= RACE_IMP ) + { + chances[race] = 0; + continue; + } stats[index]->playerRace = race; chances[race] = 0; if ( isCharacterValidFromDLC(*stats[index], index) == VALID_OK_CHARACTER ) @@ -17443,24 +17796,38 @@ namespace MainMenu { else { // select a random race - // there are 9 legal races that the player can select from the start. - if (enabledDLCPack1 && enabledDLCPack2) { - stats[index]->playerRace = RNG.uniform(0, NUMPLAYABLERACES - 1); - } else if (enabledDLCPack1) { - stats[index]->playerRace = RNG.uniform(0, 4); - } else if (enabledDLCPack2) { - stats[index]->playerRace = RNG.uniform(0, 4); - if (stats[index]->playerRace > 0) { - stats[index]->playerRace += 4; - } - } else { - stats[index]->playerRace = RACE_HUMAN; + // there are x legal races that the player can select from the start. + std::vector chances; + chances.resize(NUMRACES); + chances[RACE_HUMAN] = 1; + if ( enabledDLCPack1 ) + { + chances[RACE_SKELETON] = 1; + chances[RACE_VAMPIRE] = 1; + chances[RACE_GOATMAN] = 1; + chances[RACE_SUCCUBUS] = 1; + } + if ( enabledDLCPack2 ) + { + chances[RACE_INSECTOID] = 1; + chances[RACE_INCUBUS] = 1; + chances[RACE_INSECTOID] = 1; + chances[RACE_AUTOMATON] = 1; } + if ( enabledDLCPack3 ) + { + chances[RACE_GREMLIN] = 1; + chances[RACE_MYCONID] = 1; + chances[RACE_DRYAD] = 1; + chances[RACE_GNOME] = 1; + chances[RACE_SALAMANDER] = 1; + } + stats[index]->playerRace = RNG.discrete(chances.data(), chances.size()); } } auto race_button = card->findButton("race"); - race_button->setText(Language::get(5369 + stats[index]->playerRace)); + race_button->setText(Language::get(getLangEntryForPlayerRaceName(stats[index]->playerRace))); // choose a random appearance const int appearance_choice = RNG.uniform(0, NUMAPPEARANCES - 1); @@ -17543,7 +17910,7 @@ namespace MainMenu { class_text->setSize(SDL_Rect{96, 236, 138, 32}); static auto class_text_fn = [](Field& field, int index){ int i = std::min(std::max(0, client_classes[index]), num_classes - 1); - field.setText(Language::get(5348 + i)); + field.setText(Language::get(playerClassLangEntryCapitalized(i))); }; class_text->setFont(smallfont_outline); class_text->setJustify(Field::justify_t::CENTER); @@ -17571,6 +17938,11 @@ namespace MainMenu { button.setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/ClassSelection/ClassSelect_IconBGLegendsHigh_00.png"); button.setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/ClassSelection/ClassSelect_IconBGLegendsPress_00.png"); break; + case DLC::DesertersAndDisciples: + button.setBackground("*images/ui/Main Menus/Play/PlayerCreation/ClassSelection/ClassSelect_IconBGDeserters_00.png"); + button.setBackgroundHighlighted("*images/ui/Main Menus/Play/PlayerCreation/ClassSelection/ClassSelect_IconBGDesertersHigh_00.png"); + button.setBackgroundActivated("*images/ui/Main Menus/Play/PlayerCreation/ClassSelection/ClassSelect_IconBGDesertersPress_00.png"); + break; } button.setIcon((std::string("*images/ui/Main Menus/Play/PlayerCreation/ClassSelection/") + find->second.image_highlighted).c_str()); } @@ -18408,6 +18780,8 @@ namespace MainMenu { uniqueLobbyKey = local_rng.getU32(); net_rng.seedBytes(&uniqueGameKey, sizeof(uniqueGameKey)); + printlog("Starting game, game seed: %lu", uniqueGameKey); + // send start signal to each player if (multiplayer == SERVER) { for (int c = 1; c < MAXPLAYERS; c++) { @@ -25003,7 +25377,9 @@ namespace MainMenu { { soundCancel(); setAudioDevice(current_audio_device); +#ifdef USE_FMOD setRecordDevice(current_recording_audio_device); +#endif setGlobalVolume(master_volume, musvolume, sfxvolume, sfxAmbientVolume, sfxEnvironmentVolume, sfxNotificationVolume); if (main_menu_frame) { auto buttons = main_menu_frame->findFrame("buttons"); assert(buttons); @@ -25028,7 +25404,9 @@ namespace MainMenu { soundActivate(); setAudioDevice(current_audio_device); +#ifdef USE_FMOD setRecordDevice(current_recording_audio_device); +#endif setGlobalVolume(master_volume, musvolume, sfxvolume, sfxAmbientVolume, sfxEnvironmentVolume, sfxNotificationVolume); if ( main_menu_frame ) { auto buttons = main_menu_frame->findFrame("buttons"); assert(buttons); @@ -25955,10 +26333,21 @@ namespace MainMenu { #ifdef NINTENDO enabledDLCPack1 = nxCheckDLC(0); enabledDLCPack2 = nxCheckDLC(1); + enabledDLCPack3 = nxCheckDLC(2); #endif #ifdef STEAMWORKS - enabledDLCPack1 = SteamApps()->BIsDlcInstalled(1010820); - enabledDLCPack2 = SteamApps()->BIsDlcInstalled(1010821); + if ( !enabledDLCPack1 ) + { + enabledDLCPack1 = SteamApps()->BIsDlcInstalled(1010820); + } + if ( !enabledDLCPack2 ) + { + enabledDLCPack2 = SteamApps()->BIsDlcInstalled(1010821); + } + if ( !enabledDLCPack3 ) + { + enabledDLCPack3 = SteamApps()->BIsDlcInstalled(1010822); + } #endif if (!ingame) { @@ -26272,6 +26661,78 @@ namespace MainMenu { } }; static GetPlayersOnline getPlayersOnline; + + void MainMenu::RichPresence::process() + { + if ( loading ) + { + return; + } + if ( !init ) + { + needsUpdate = true; + } + if ( ticks - lastUpdate >= 10 * TICKS_PER_SECOND ) + { + needsUpdate = true; + lastUpdate = ticks; + } + if ( _intro != intro ) + { + _intro = intro; + needsUpdate = true; + } + if ( levelStr != map.name ) + { + levelStr = map.name; + needsUpdate = true; + } + if ( clientnum >= 0 && clientnum < MAXPLAYERS && stats ) + { + if ( _classnum != client_classes[clientnum] ) + { + _classnum = client_classes[clientnum]; + needsUpdate = true; + } + if ( _level != stats[clientnum]->LVL ) + { + _level = stats[clientnum]->LVL; + needsUpdate = true; + } + } + + if ( needsUpdate ) + { + bool result = false; + if ( intro == true ) + { + result = SteamFriends()->SetRichPresence("steam_display", "#Status_AtMainMenu"); + } + else if ( clientnum >= 0 && clientnum < MAXPLAYERS && stats ) + { + auto find = Player::CharacterSheet_t::mapDisplayNamesDescriptions.find(map.name); + if ( find == Player::CharacterSheet_t::mapDisplayNamesDescriptions.end() ) + { + result = SteamFriends()->SetRichPresence("steam_display", "#Status_Nolocation"); + } + else + { + trimmedLevelStr = map.name; + trimmedLevelStr.erase(std::remove(trimmedLevelStr.begin(), trimmedLevelStr.end(), ' '), trimmedLevelStr.end()); // trim whitespace + result = SteamFriends()->SetRichPresence("location", trimmedLevelStr.c_str()); + result = SteamFriends()->SetRichPresence("class", std::to_string(client_classes[clientnum]).c_str()); + result = SteamFriends()->SetRichPresence("level", std::to_string(stats[clientnum]->LVL).c_str()); + result = SteamFriends()->SetRichPresence("steam_display", "#Status_Ingame"); + } + } + else + { + SteamFriends()->ClearRichPresence(); + } + needsUpdate = false; + } + init = true; + } #endif std::string MainMenuBanners_t::updateBannerImg = ""; @@ -26571,7 +27032,7 @@ namespace MainMenu { button->setWidgetUp(options[back].name); } #ifdef NINTENDO - if (ingame || c + 1 < num_options || (enabledDLCPack1 && enabledDLCPack2)) { + if (ingame || c + 1 < num_options || (enabledDLCPack1 && enabledDLCPack2 && enabledDLCPack3)) { button->setWidgetDown(options[forward].name); } else { button->setWidgetDown("banner1"); @@ -26751,9 +27212,21 @@ namespace MainMenu { // customize DLC banner. { if (!enabledDLCPack1 && !enabledDLCPack2) { - banner_images[0][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_ComboBanner1_base.png"; - banner_images[0][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_ComboBanner1_high.png"; + if ( !enabledDLCPack3 ) + { + banner_images[0][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_DnDBanner1_base.png"; + banner_images[0][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_DnDBanner1_high.png"; + } + else + { + banner_images[0][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_ComboBanner1_base.png"; + banner_images[0][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_ComboBanner1_high.png"; + } } + else if ( !enabledDLCPack3 ) { + banner_images[0][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_DnDBanner1_base.png"; + banner_images[0][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_DnDBanner1_high.png"; + } else if (!enabledDLCPack1) { banner_images[0][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_MnOBanner1_base.png"; banner_images[0][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_MnOBanner1_high.png"; @@ -26770,7 +27243,7 @@ namespace MainMenu { } }; - const int num_banners = (enabledDLCPack1 && enabledDLCPack2) ? + const int num_banners = (enabledDLCPack1 && enabledDLCPack2 && enabledDLCPack3) ? 0 : 1; #else const char* banner_images[][2] = { @@ -26795,9 +27268,21 @@ namespace MainMenu { // customize DLC banner. { if (!enabledDLCPack1 && !enabledDLCPack2) { - banner_images[1][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_ComboBanner1_base.png"; - banner_images[1][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_ComboBanner1_high.png"; + if ( !enabledDLCPack3 ) + { + banner_images[1][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_DnDBanner1_base.png"; + banner_images[1][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_DnDBanner1_high.png"; + } + else + { + banner_images[1][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_ComboBanner1_base.png"; + banner_images[1][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_ComboBanner1_high.png"; + } } + else if ( !enabledDLCPack3 ) { + banner_images[1][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_DnDBanner1_base.png"; + banner_images[1][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_DnDBanner1_high.png"; + } else if (!enabledDLCPack1) { banner_images[1][0] = "*#images/ui/Main Menus/Banners/UI_MainMenu_MnOBanner1_base.png"; banner_images[1][1] = "*#images/ui/Main Menus/Banners/UI_MainMenu_MnOBanner1_high.png"; @@ -26817,7 +27302,7 @@ namespace MainMenu { }, }; - const int num_banners = (enabledDLCPack1 && enabledDLCPack2) ? 1 : sizeof(banner_funcs) / sizeof(banner_funcs[0]); + const int num_banners = (enabledDLCPack1 && enabledDLCPack2 && enabledDLCPack3) ? 1 : sizeof(banner_funcs) / sizeof(banner_funcs[0]); #endif auto banners = main_menu_frame->addFrame("banners"); banners->setSize(SDL_Rect{(Frame::virtualScreenX - 472) / 2, y, 472, Frame::virtualScreenY - y}); @@ -26947,6 +27432,7 @@ namespace MainMenu { auto online_players = static_cast(&widget); if (ticks % (TICKS_PER_SECOND * 5) == 0) { getPlayersOnline(); + richPresence.process(); } int players = getPlayersOnline.current(); if (players == 0) { @@ -27072,7 +27558,7 @@ namespace MainMenu { int placement = 1; score_t* score = scoreConstructor(player); Uint32 total = totalScore(score); - list_t* scoresPtr = multiplayer == SINGLE ? &topscores : &topscoresMultiplayer; + list_t* scoresPtr = multiplayer == SINGLE ? &topscores_json : &topscoresMultiplayer_json; for (auto node = scoresPtr->first; node != nullptr; node = node->next) { if (total > totalScore((score_t*)node->element)) { break; @@ -27190,6 +27676,15 @@ namespace MainMenu { case KilledBy::BELL: cause_of_death = Language::get(6278); break; + case KilledBy::MUSHROOM: + cause_of_death = Language::get(6754); + break; + case KilledBy::LEAVES: + cause_of_death = Language::get(6759); + break; + case KilledBy::DEATH_KNOCKBACK: + cause_of_death = Language::get(6854); + break; default: { cause_of_death = Language::get(5794 + (int)stats[player]->killer); break; @@ -31845,6 +32340,16 @@ namespace MainMenu { check = INVALID_REQUIREDLC2; } break; + case CLASS_BARD: + case CLASS_SAPPER: + case CLASS_SCION: + case CLASS_HERMIT: + case CLASS_PALADIN: + if ( !enabledDLCPack3 ) + { + check = INVALID_REQUIREDLC3; + } + break; default: break; } @@ -31868,6 +32373,16 @@ namespace MainMenu { check = INVALID_REQUIREDLC2; } break; + case RACE_GREMLIN: + case RACE_DRYAD: + case RACE_MYCONID: + case RACE_SALAMANDER: + case RACE_GNOME: + if ( !enabledDLCPack3 ) + { + check = INVALID_REQUIREDLC3; + } + break; default: break; } @@ -31887,12 +32402,17 @@ namespace MainMenu { txt = Language::get(6127); txt += Language::get(5002); } + else if ( check == INVALID_REQUIREDLC3 ) + { + txt = Language::get(6778); + txt += Language::get(5002); + } else { txt = Language::get(6125); } - if ( check == INVALID_REQUIREDLC1 || check == INVALID_REQUIREDLC2 ) + if ( check == INVALID_REQUIREDLC1 || check == INVALID_REQUIREDLC2 || check == INVALID_REQUIREDLC3 ) { auto prompt = binaryPromptXL( txt.c_str(), @@ -34130,7 +34650,11 @@ namespace MainMenu { auto find = ItemTooltips.spellItems.find(spellID); if ( find != ItemTooltips.spellItems.end() && spellID > SPELL_NONE ) { - if ( find->second.spellbookId == entryType ) + if ( spellID == SPELL_STRIKE ) + { + // ignore damage portion as cant track + } + else if ( find->second.spellbookId == entryType ) { if ( find->second.spellTags.find(ItemTooltips_t::SpellTagTypes::SPELL_TAG_HEALING) != find->second.spellTags.end() ) @@ -34148,6 +34672,21 @@ namespace MainMenu { displayedEvents.push_back(Compendium_t::EventTags::CPDM_SPELL_TARGETS); } } + + if ( spellID == SPELL_SHATTER_OBJECTS ) + { + if ( displayedEvents.size() && displayedEvents.back() == Compendium_t::EventTags::CPDM_SPELL_DMG ) + { + displayedEvents.back() = Compendium_t::EventTags::CPDM_SPELL_TARGETS; + } + } + else if ( spellID == SPELL_HOLY_BEAM ) + { + if ( displayedEvents.size() && displayedEvents.back() == Compendium_t::EventTags::CPDM_SPELL_HEAL ) + { + displayedEvents.back() = Compendium_t::EventTags::CPDM_SPELL_DMG; + } + } } displayedEvents.push_back(Compendium_t::EventTags::CPDM_SPELLBOOK_CASTS); displayedEvents.push_back(Compendium_t::EventTags::CPDM_SPELLBOOK_CAST_DEGRADES); @@ -34218,6 +34757,21 @@ namespace MainMenu { { displayedEvents.push_back(Compendium_t::EventTags::CPDM_SPELL_TARGETS); } + + if ( spellID == SPELL_SHATTER_OBJECTS ) + { + if ( displayedEvents.size() && displayedEvents.back() == Compendium_t::EventTags::CPDM_SPELL_DMG ) + { + displayedEvents.back() = Compendium_t::EventTags::CPDM_SPELL_TARGETS; + } + } + else if ( spellID == SPELL_HOLY_BEAM ) + { + if ( displayedEvents.size() && displayedEvents.back() == Compendium_t::EventTags::CPDM_SPELL_HEAL ) + { + displayedEvents.back() = Compendium_t::EventTags::CPDM_SPELL_DMG; + } + } } displayedEvents.push_back(Compendium_t::EventTags::CPDM_SPELL_CASTS); displayedEvents.push_back(Compendium_t::EventTags::CPDM_SPELL_FAILURES); @@ -34960,6 +35514,7 @@ namespace MainMenu { if ( i.spellID >= 0 ) { itemLookupIndex = Compendium_t::Events_t::kEventSpellOffset + i.spellID; + appearance = i.spellID; } } break; @@ -35738,6 +36293,14 @@ namespace MainMenu { if ( frame->getUserData() ) { Compendium_t::compendiumItem.appearance = (reinterpret_cast(frame->getUserData()) & 0x7F); + if ( itemType == SPELL_ITEM ) + { + auto find = ItemTooltips.spellNameStringToSpellID.find(widget.getName()); + if ( find != ItemTooltips.spellNameStringToSpellID.end() ) + { + Compendium_t::compendiumItem.appearance = find->second; + } + } } if ( Compendium_t::compendiumItem.type == SPELL_ITEM ) @@ -35959,7 +36522,40 @@ namespace MainMenu { char buf[128]; if ( items[id].level >= 0 ) { - snprintf(buf, sizeof(buf), "\n%s %d", Language::get(6173), items[id].level); + if ( items[id].category == SPELLBOOK ) + { + snprintf(buf, sizeof(buf), "\n%s ???", Language::get(6173)); + int spellID = getSpellIDFromSpellbook(id); + if ( spellID > SPELL_NONE ) + { + if ( auto spell = getSpellFromID(spellID) ) + { + if ( spell->drop_table >= 0 ) + { + if ( spell->difficulty <= 20 ) + { + snprintf(buf, sizeof(buf), "\n%s %d", Language::get(6173), 1); + } + else if ( spell->difficulty <= 40 ) + { + snprintf(buf, sizeof(buf), "\n%s %d", Language::get(6173), 6); + } + else if ( spell->difficulty <= 60 ) + { + snprintf(buf, sizeof(buf), "\n%s %d", Language::get(6173), 11); + } + else if ( spell->difficulty >= 80 ) + { + snprintf(buf, sizeof(buf), "\n%s %d", Language::get(6173), 16); + } + } + } + } + } + else + { + snprintf(buf, sizeof(buf), "\n%s %d", Language::get(6173), items[id].level); + } } else { @@ -35995,14 +36591,31 @@ namespace MainMenu { Compendium_t::compendiumItem.type = (ItemType)id; Compendium_t::compendiumItem.appearance = (modelRNGCycle + Compendium_t::compendiumEntityCurrent.modelRNG) % items[id].variations; - if ( id == TOOL_PLAYER_LOOT_BAG ) + if ( id == MAGICSTAFF_SCEPTER ) + { + Compendium_t::compendiumItem.appearance = 0; + } + else if ( id == TOOL_PLAYER_LOOT_BAG ) { Compendium_t::compendiumItem.appearance = (modelRNGCycle + Compendium_t::compendiumEntityCurrent.modelRNG) % 4; } + else if ( id == TOOL_DUCK ) + { + Compendium_t::compendiumItem.appearance = (modelRNGCycle + Compendium_t::compendiumEntityCurrent.modelRNG * 4) % items[id].variations; + Compendium_t::compendiumItem.appearance /= MAXPLAYERS; + } else if ( id == READABLE_BOOK ) { Compendium_t::compendiumItem.appearance = getBook("My Journal") % items[id].variations; } + else if ( itemCategory(&Compendium_t::compendiumItem) == SPELLBOOK || itemCategory(&Compendium_t::compendiumItem) == TOME_SPELL ) + { + int variation = getItemVariationFromSpellbookOrTome(Compendium_t::compendiumItem); + if ( variation >= 0 ) + { + Compendium_t::compendiumItem.appearance = variation; + } + } if ( unlockStatus == Compendium_t::CompendiumUnlockStatus::LOCKED_UNKNOWN ) { @@ -37755,7 +38368,7 @@ namespace MainMenu { myStats->setAttribute("monster_portrait", "true"); (void)actMonster(monster); monster->yaw = 0.0; - myStats->EFFECTS[EFF_ASLEEP] = false; + myStats->clearEffect(EFF_ASLEEP); monsterAnimate(compendiumMonster, myStats, 0.0); monsterAnimate(compendiumMonster, myStats, 0.0); } @@ -38243,6 +38856,18 @@ namespace MainMenu { "*#images/ui/Main Menus/AdventureArchives/A_Icon_Legends_Grey_00.png" } }, + { + Compendium_t::AchievementData_t::AchievementDLCType::ACH_TYPE_DLC3, + { + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_Locked_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_DLCCompleted_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_Locked_Deserters_Badge_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_Locked_Badge_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_Icon_Deserters_Colored_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_Icon_Deserters_Gold_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_Icon_Deserters_Grey_00.png" + } + }, { Compendium_t::AchievementData_t::AchievementDLCType::ACH_TYPE_NORMAL, { @@ -38267,6 +38892,18 @@ namespace MainMenu { "" } }, + { + Compendium_t::AchievementData_t::AchievementDLCType::ACH_TYPE_DLC1_DLC2_DLC3, + { + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_Locked_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_DLCCompleted_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_Locked_Badge_00.png", + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_Locked_Badge_00.png", + "", + "", + "" + } + }, { -1, { @@ -38498,6 +39135,11 @@ namespace MainMenu { { dlc_badge_2->disabled = true; } + auto dlc_badge_3 = ach->findImage("dlc_badge_3"); + if ( dlc_badge_3 ) + { + dlc_badge_3->disabled = true; + } auto dlc_badge_icon_1 = ach->findImage("dlc_badge_icon_1"); if ( dlc_badge_icon_1 ) { @@ -38508,6 +39150,11 @@ namespace MainMenu { { dlc_badge_icon_2->disabled = true; } + auto dlc_badge_icon_3 = ach->findImage("dlc_badge_icon_3"); + if ( dlc_badge_icon_3 ) + { + dlc_badge_icon_3->disabled = true; + } if ( auto dlc_badge = ach->findImage("dlc_badge") ) { dlc_badge->disabled = true; @@ -38515,7 +39162,34 @@ namespace MainMenu { { if ( !hiddenGroup ) { - if ( achData.dlcType == Compendium_t::AchievementData_t::ACH_TYPE_DLC1_DLC2 ) + if ( achData.dlcType == Compendium_t::AchievementData_t::ACH_TYPE_DLC1_DLC2_DLC3 ) + { + if ( achData.unlocked ) + { + dlc_badge_3->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC1)[2]; + dlc_badge_2->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC3)[2]; + dlc_badge_1->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC2)[2]; + } + else + { + dlc_badge_3->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC1)[3]; + dlc_badge_2->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC3)[3]; + dlc_badge_1->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC2)[3]; + } + if ( dlc_badge_1->path != "" ) + { + dlc_badge_1->disabled = false; + } + if ( dlc_badge_2->path != "" ) + { + dlc_badge_2->disabled = false; + } + if ( dlc_badge_3->path != "" ) + { + dlc_badge_3->disabled = false; + } + } + else if ( achData.dlcType == Compendium_t::AchievementData_t::ACH_TYPE_DLC1_DLC2 ) { if ( achData.unlocked ) { @@ -38561,7 +39235,34 @@ namespace MainMenu { { if ( !hiddenGroup ) { - if ( achData.dlcType == Compendium_t::AchievementData_t::ACH_TYPE_DLC1_DLC2 ) + if ( achData.dlcType == Compendium_t::AchievementData_t::ACH_TYPE_DLC1_DLC2_DLC3 ) + { + if ( achData.unlocked ) + { + dlc_badge_icon_3->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC1)[4]; + dlc_badge_icon_2->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC3)[4]; + dlc_badge_icon_1->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC2)[4]; + } + else + { + dlc_badge_icon_3->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC1)[6]; + dlc_badge_icon_2->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC3)[6]; + dlc_badge_icon_1->path = backingImgs.at(Compendium_t::AchievementData_t::ACH_TYPE_DLC2)[6]; + } + if ( dlc_badge_icon_1->path != "" ) + { + dlc_badge_icon_1->disabled = false; + } + if ( dlc_badge_icon_2->path != "" ) + { + dlc_badge_icon_2->disabled = false; + } + if ( dlc_badge_icon_3->path != "" ) + { + dlc_badge_icon_3->disabled = false; + } + } + else if ( achData.dlcType == Compendium_t::AchievementData_t::ACH_TYPE_DLC1_DLC2 ) { if ( achData.unlocked ) { @@ -38886,6 +39587,14 @@ namespace MainMenu { dlc_badge_icon->disabled = true; { + auto dlc_badge_stack_3 = ach->addImage(SDL_Rect{ bg->pos.x - 16, bg->pos.y - 4, 38, 38 }, 0xFFFFFFFF, + "*#images/ui/Main Menus/AdventureArchives/A_AchBox_Locked_Badge_00.png", "dlc_badge_3"); + dlc_badge_stack_3->disabled = true; + + auto dlc_badge_icon_3 = ach->addImage(SDL_Rect{ dlc_badge_stack_3->pos.x + 2, dlc_badge_stack_3->pos.y + 2, 34, 34 }, 0xFFFFFFFF, + "", "dlc_badge_icon_3"); + dlc_badge_icon_3->disabled = true; + auto dlc_badge_stack_1 = ach->addImage(SDL_Rect{ bg->pos.x + 4, bg->pos.y - 4, 38, 38 }, 0xFFFFFFFF, "*#images/ui/Main Menus/AdventureArchives/A_AchBox_Locked_Badge_00.png", "dlc_badge_1"); dlc_badge_stack_1->disabled = true; diff --git a/src/ui/MainMenu.hpp b/src/ui/MainMenu.hpp index fea772671..eda2fbd8d 100644 --- a/src/ui/MainMenu.hpp +++ b/src/ui/MainMenu.hpp @@ -182,6 +182,16 @@ namespace MainMenu { return "goblin"; case RACE_INSECTOID: return "insectoid"; + case RACE_GREMLIN: + return "gremlin"; + case RACE_DRYAD: + return "dryad"; + case RACE_MYCONID: + return "myconid"; + case RACE_SALAMANDER: + return "salamander"; + case RACE_GNOME: + return "gnome"; default: break; } @@ -206,7 +216,8 @@ namespace MainMenu { enum class DLC { Base, MythsAndOutcasts, - LegendsAndPariahs + LegendsAndPariahs, + DesertersAndDisciples }; struct Class { @@ -343,6 +354,36 @@ namespace MainMenu { "ClassSelect_Icon_HunterOn_00.png", "ClassSelect_Icon_HunterLocked_00.png", }}, + { "bard", { + DLC::DesertersAndDisciples, + "ClassSelect_Icon_Bard_00.png", + "ClassSelect_Icon_BardOn_00.png", + "ClassSelect_Icon_BardLocked_00.png", + }}, + { "sapper", { + DLC::DesertersAndDisciples, + "ClassSelect_Icon_Sapper_00.png", + "ClassSelect_Icon_SapperOn_00.png", + "ClassSelect_Icon_SapperLocked_00.png", + }}, + { "scion", { + DLC::DesertersAndDisciples, + "ClassSelect_Icon_Scion_00.png", + "ClassSelect_Icon_ScionOn_00.png", + "ClassSelect_Icon_ScionLocked_00.png", + }}, + { "hermit", { + DLC::DesertersAndDisciples, + "ClassSelect_Icon_Hermit_00.png", + "ClassSelect_Icon_HermitOn_00.png", + "ClassSelect_Icon_HermitLocked_00.png", + }}, + { "paladin", { + DLC::DesertersAndDisciples, + "ClassSelect_Icon_Paladin_00.png", + "ClassSelect_Icon_PaladinOn_00.png", + "ClassSelect_Icon_PaladinLocked_00.png", + }}, }; static const char* classes_in_order[] = { @@ -351,6 +392,26 @@ namespace MainMenu { "wizard", "arcanist", "joker", "sexton", "ninja", "monk", "conjurer", "accursed", "mesmer", "brewer", "mechanist", "punisher", - "shaman", "hunter" + "shaman", "hunter", "bard", "sapper", "scion", "hermit", "paladin" + }; + +#ifdef STEAMWORKS + class RichPresence + { + int _currentlevel = 0; + int _secretlevel = 0; + int _classnum = 0; + int _level = 0; + bool _intro = false; + Uint32 lastUpdate = 0; + std::string levelStr = ""; + std::string trimmedLevelStr = ""; + bool init = false; + bool needsUpdate = true; + bool enabled = true; + public: + void process(); }; + static RichPresence richPresence; +#endif } diff --git a/xcode/Barony/Barony.xcodeproj/project.pbxproj b/xcode/Barony/Barony.xcodeproj/project.pbxproj index 3f21e6146..80205be2d 100644 --- a/xcode/Barony/Barony.xcodeproj/project.pbxproj +++ b/xcode/Barony/Barony.xcodeproj/project.pbxproj @@ -69,6 +69,13 @@ 845F4BC52920302F00D6B9D7 /* SDL2_ttf.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84C3D8F3291F5DEB0084EAD5 /* SDL2_ttf.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 845F4BC62920302F00D6B9D7 /* SDL2.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84C3D8F1291F5D370084EAD5 /* SDL2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 845F4BCD292051C000D6B9D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 845F4BCC292051C000D6B9D7 /* Assets.xcassets */; }; + 846DBD0E2F2B1EEA0064BF2C /* monster_summons.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 846DBD0D2F2B1EEA0064BF2C /* monster_summons.cpp */; }; + 846DBD0F2F2B1EEA0064BF2C /* monster_duck.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 846DBD082F2B1EEA0064BF2C /* monster_duck.cpp */; }; + 846DBD102F2B1EEA0064BF2C /* monster_g.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 846DBD092F2B1EEA0064BF2C /* monster_g.cpp */; }; + 846DBD112F2B1EEA0064BF2C /* monster_s.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 846DBD0C2F2B1EEA0064BF2C /* monster_s.cpp */; }; + 846DBD122F2B1EEA0064BF2C /* monster_d.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 846DBD072F2B1EEA0064BF2C /* monster_d.cpp */; }; + 846DBD132F2B1EEA0064BF2C /* monster_moth.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 846DBD0B2F2B1EEA0064BF2C /* monster_moth.cpp */; }; + 846DBD142F2B1EEA0064BF2C /* monster_m.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 846DBD0A2F2B1EEA0064BF2C /* monster_m.cpp */; }; 847B3BF0296BF890002D8852 /* libpng16.16.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 847B3BEF296BF890002D8852 /* libpng16.16.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 847B3BF2296BF89F002D8852 /* libphysfs.1.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 847B3BF1296BF89F002D8852 /* libphysfs.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 847B3BF3296BF8B5002D8852 /* libpng16.16.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 847B3BEF296BF890002D8852 /* libpng16.16.dylib */; }; @@ -347,6 +354,13 @@ 8453695A29B09C36007AC397 /* imgui_impl_opengl3_loader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = imgui_impl_opengl3_loader.h; path = ../../src/imgui/imgui_impl_opengl3_loader.h; sourceTree = ""; }; 8453695B29B09C36007AC397 /* imgui_impl_opengl3.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = imgui_impl_opengl3.h; path = ../../src/imgui/imgui_impl_opengl3.h; sourceTree = ""; }; 845F4BCC292051C000D6B9D7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 846DBD072F2B1EEA0064BF2C /* monster_d.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = monster_d.cpp; path = /Users/sheridan/Work/Barony/src/monster_d.cpp; sourceTree = ""; }; + 846DBD082F2B1EEA0064BF2C /* monster_duck.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = monster_duck.cpp; path = /Users/sheridan/Work/Barony/src/monster_duck.cpp; sourceTree = ""; }; + 846DBD092F2B1EEA0064BF2C /* monster_g.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = monster_g.cpp; path = /Users/sheridan/Work/Barony/src/monster_g.cpp; sourceTree = ""; }; + 846DBD0A2F2B1EEA0064BF2C /* monster_m.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = monster_m.cpp; path = /Users/sheridan/Work/Barony/src/monster_m.cpp; sourceTree = ""; }; + 846DBD0B2F2B1EEA0064BF2C /* monster_moth.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = monster_moth.cpp; path = /Users/sheridan/Work/Barony/src/monster_moth.cpp; sourceTree = ""; }; + 846DBD0C2F2B1EEA0064BF2C /* monster_s.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = monster_s.cpp; path = /Users/sheridan/Work/Barony/src/monster_s.cpp; sourceTree = ""; }; + 846DBD0D2F2B1EEA0064BF2C /* monster_summons.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = monster_summons.cpp; path = /Users/sheridan/Work/Barony/src/monster_summons.cpp; sourceTree = ""; }; 847B3BEF296BF890002D8852 /* libpng16.16.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libpng16.16.dylib; path = ../../../lpng1639/libpng16.16.dylib; sourceTree = ""; }; 847B3BF1296BF89F002D8852 /* libphysfs.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libphysfs.1.dylib; path = "../../../physfs-3.0.2/build/libphysfs.1.dylib"; sourceTree = ""; }; 847C6CBD2DFC9D34003F2A37 /* libopus.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libopus.0.dylib; path = "../../../opus-1.5.2/lib/libopus.0.dylib"; sourceTree = ""; }; @@ -811,8 +825,11 @@ 84FDF0632CC9A9D500AE8639 /* monster_bugbear.cpp */, 84C3DA6F291F78F00084EAD5 /* monster_cockatrice.cpp */, 84C3DA81291F78F00084EAD5 /* monster_crystalgolem.cpp */, + 846DBD072F2B1EEA0064BF2C /* monster_d.cpp */, 84C3DA70291F78F00084EAD5 /* monster_demon.cpp */, 84C3DA73291F78F00084EAD5 /* monster_devil.cpp */, + 846DBD082F2B1EEA0064BF2C /* monster_duck.cpp */, + 846DBD092F2B1EEA0064BF2C /* monster_g.cpp */, 84C3DA65291F78EF0084EAD5 /* monster_ghoul.cpp */, 84C3DA6B291F78EF0084EAD5 /* monster_gnome.cpp */, 84C3DA6E291F78EF0084EAD5 /* monster_goatman.cpp */, @@ -825,9 +842,12 @@ 84C3DA62291F78EF0084EAD5 /* monster_lich.cpp */, 84C3DA69291F78EF0084EAD5 /* monster_lichfire.cpp */, 84C3DA75291F78F00084EAD5 /* monster_lichice.cpp */, + 846DBD0A2F2B1EEA0064BF2C /* monster_m.cpp */, 84B4ACC92B8E991B00027417 /* monster_mimic.cpp */, 84C3DA7F291F78F00084EAD5 /* monster_minotaur.cpp */, + 846DBD0B2F2B1EEA0064BF2C /* monster_moth.cpp */, 84C3DA6D291F78EF0084EAD5 /* monster_rat.cpp */, + 846DBD0C2F2B1EEA0064BF2C /* monster_s.cpp */, 84C3DA7E291F78F00084EAD5 /* monster_scarab.cpp */, 84C3DA67291F78EF0084EAD5 /* monster_scorpion.cpp */, 84C3DA7A291F78F00084EAD5 /* monster_sentrybot.cpp */, @@ -838,6 +858,7 @@ 84C3DA6A291F78EF0084EAD5 /* monster_slime.cpp */, 84C3DA68291F78EF0084EAD5 /* monster_spider.cpp */, 84C3DA80291F78F00084EAD5 /* monster_succubus.cpp */, + 846DBD0D2F2B1EEA0064BF2C /* monster_summons.cpp */, 84C3DA79291F78F00084EAD5 /* monster_troll.cpp */, 84C3DA82291F78F10084EAD5 /* monster_vampire.cpp */, 84C3DACA291F79310084EAD5 /* music.cpp */, @@ -1238,6 +1259,13 @@ 84C3DAEE291F79510084EAD5 /* updatecharactersheet.cpp in Sources */, 84C3DAEF291F79510084EAD5 /* ui_general.cpp in Sources */, 84C3DACC291F79320084EAD5 /* sound.cpp in Sources */, + 846DBD0E2F2B1EEA0064BF2C /* monster_summons.cpp in Sources */, + 846DBD0F2F2B1EEA0064BF2C /* monster_duck.cpp in Sources */, + 846DBD102F2B1EEA0064BF2C /* monster_g.cpp in Sources */, + 846DBD112F2B1EEA0064BF2C /* monster_s.cpp in Sources */, + 846DBD122F2B1EEA0064BF2C /* monster_d.cpp in Sources */, + 846DBD132F2B1EEA0064BF2C /* monster_moth.cpp in Sources */, + 846DBD142F2B1EEA0064BF2C /* monster_m.cpp in Sources */, 84C3DACD291F79320084EAD5 /* defines.cpp in Sources */, 84C3DACE291F79320084EAD5 /* init_audio.cpp in Sources */, 84C3DACF291F79320084EAD5 /* music.cpp in Sources */,